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/actions/build-docker-image-service/action.yml b/.github/actions/build-docker-image-service/action.yml new file mode 100644 index 000000000000..aba20175b940 --- /dev/null +++ b/.github/actions/build-docker-image-service/action.yml @@ -0,0 +1,69 @@ +name: 'Build Micro Services Docker image' +description: 'Build Rocket.Chat Micro Services Docker images' + +inputs: + docker-tag: + required: true + service: + required: true + username: + required: false + password: + required: false + +outputs: + image-name: + value: ${{ steps.build-image.outputs.image-name }} + +runs: + using: "composite" + steps: + # - shell: bash + # name: Free disk space + # run: | + # sudo swapoff -a + # sudo rm -f /swapfile + # sudo apt clean + # docker rmi $(docker image ls -aq) + # df -h + + - shell: bash + id: build-image + run: | + LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") + + IMAGE_TAG="${{ inputs.docker-tag }}" + + IMAGE_NAME="ghcr.io/${LOWERCASE_REPOSITORY}/${{ inputs.service }}-service:${IMAGE_TAG}" + + echo "Building Docker image for service: ${{ inputs.service }}:${IMAGE_TAG}" + + if [[ "${{ inputs.service }}" == "ddp-streamer" ]]; then + DOCKERFILE_PATH="./ee/apps/ddp-streamer/Dockerfile" + else + DOCKERFILE_PATH="./apps/meteor/ee/server/services/Dockerfile" + fi + + docker build \ + --build-arg SERVICE=${{ inputs.service }} \ + -t ${IMAGE_NAME} \ + -f ${DOCKERFILE_PATH} \ + . + + echo "::set-output name=image-name::${IMAGE_NAME}" + + - name: Login to GitHub Container Registry + if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ inputs.username }} + password: ${{ inputs.password }} + + - name: Publish image + shell: bash + if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' + run: | + echo "Push Docker image: ${{ steps.build-image.outputs.image-name }}" + + docker push ${{ steps.build-image.outputs.image-name }} diff --git a/.github/actions/build-docker-image/action.yml b/.github/actions/build-docker-image/action.yml new file mode 100644 index 000000000000..240c502efd1c --- /dev/null +++ b/.github/actions/build-docker-image/action.yml @@ -0,0 +1,88 @@ +name: 'Build Docker image' +description: 'Build Rocket.Chat Docker image' + +inputs: + root-dir: + required: true + docker-tag: + required: true + release: + required: true + username: + required: false + password: + required: false + +outputs: + image-name: + value: ${{ steps.build-image.outputs.image-name }} + +runs: + using: composite + steps: + # - shell: bash + # name: Free disk space + # run: | + # sudo swapoff -a + # sudo rm -f /swapfile + # sudo apt clean + # docker rmi $(docker image ls -aq) + # df -h + + - shell: bash + id: build-image + run: | + cd ${{ inputs.root-dir }} + + LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") + + IMAGE_NAME_BASE="ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${{ inputs.docker-tag }}" + + IMAGE_NAME="${IMAGE_NAME_BASE}.${{ inputs.release }}" + + echo "Build Docker image ${IMAGE_NAME}" + + DOCKER_PATH="${GITHUB_WORKSPACE}/apps/meteor/.docker" + if [[ '${{ inputs.release }}' = 'preview' ]]; then + DOCKER_PATH="${DOCKER_PATH}-mongo" + fi; + + DOCKERFILE_PATH="${DOCKER_PATH}/Dockerfile" + if [[ '${{ inputs.release }}' = 'alpine' ]]; then + DOCKERFILE_PATH="${DOCKERFILE_PATH}.${{ inputs.release }}" + fi; + + echo "Copy Dockerfile for release: ${{ inputs.release }}" + cp $DOCKERFILE_PATH ./Dockerfile + if [ -e ${DOCKER_PATH}/entrypoint.sh ]; then + cp ${DOCKER_PATH}/entrypoint.sh . + fi; + + echo "Build ${{ inputs.release }} Docker image" + docker build -t $IMAGE_NAME . + + echo "::set-output name=image-name-base::${IMAGE_NAME_BASE}" + echo "::set-output name=image-name::${IMAGE_NAME}" + + - name: Login to GitHub Container Registry + if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ inputs.username }} + password: ${{ inputs.password }} + + - name: Publish image + shell: bash + if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' + run: | + echo "Push Docker image: ${{ steps.build-image.outputs.image-name }}" + + docker push ${{ steps.build-image.outputs.image-name }} + + if [[ '${{ inputs.release }}' = 'official' ]]; then + echo "Push release official without variant" + + docker tag ${{ steps.build-image.outputs.image-name }} ${{ steps.build-image.outputs.image-name-base }} + docker push ${{ steps.build-image.outputs.image-name-base }} + fi; diff --git a/.github/auto-label-action-config.json b/.github/auto-label-action-config.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/.github/auto-label-action-config.json @@ -0,0 +1 @@ +{} diff --git a/.github/history.json b/.github/history.json index 35da7943e975..a711382653c8 100644 --- a/.github/history.json +++ b/.github/history.json @@ -77001,8 +77001,15 @@ ] }, "3.18.6": { - "node_version": "14.18.3", - "npm_version": "6.14.15", + "mongo_versions": [ + "3.4", + "3.6", + "4.0", + "4.2" + ], + "pull_requests": [] + }, + "4.1.6": { "mongo_versions": [ "3.6", "4.0", @@ -77010,9 +77017,32 @@ "4.4", "5.0" ], - "pull_requests": [] + "pull_requests": [ + { + "pr": "24553", + "title": "[FIX] Omnichannel managers can't join chats in progress", + "userLogin": "renatobecker", + "milestone": "4.5.0", + "contributors": [ + "renatobecker", + "murtaza98", + "web-flow" + ] + }, + { + "pr": "24592", + "title": "Regression: Fix in-correct room status shown to agents", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98" + ] + } + ] }, - "4.1.6": { + "4.4.4": { + "node_version": "14.18.3", + "npm_version": "6.14.15", "mongo_versions": [ "3.6", "4.0", @@ -77022,70 +77052,13605 @@ ], "pull_requests": [ { - "pr": "24553", - "title": "[FIX] Omnichannel managers can't join chats in progress", - "userLogin": "renatobecker", - "milestone": "4.5.0", + "pr": "25580", + "title": "Release 4.7.2", + "userLogin": "d-gubert", + "contributors": [ + "tiagoevanp", + "d-gubert", + "MartinSchoeler", + "ggazzo", + "cauefcr", + "geekgonecrazy" + ] + }, + { + "pr": "25544", + "title": "[FIX] Initial User not added to default channel", + "userLogin": "geekgonecrazy", + "description": "If injecting initial user. The user wasn’t added to the default General channel", + "milestone": "4.7.2", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "25520", + "title": "[FIX] User abandonment setting was not working doe to failing event hook", + "userLogin": "cauefcr", + "description": "A setting watcher and the query for grabbing abandoned chats were broken, now they're not.", + "milestone": "4.7.2", + "contributors": [ + "cauefcr", + "tiagoevanp" + ] + }, + { + "pr": "25495", + "title": "[FIX] Dynamic load matrix is enabled and handle failure ", + "userLogin": "ggazzo", + "milestone": "4.7.2", + "contributors": [ + "ggazzo", + "geekgonecrazy" + ] + }, + { + "pr": "25409", + "title": "[FIX] One of the triggers was not working correctly", + "userLogin": "MartinSchoeler", + "milestone": "4.7.2", + "contributors": [ + "MartinSchoeler", + "tiagoevanp" + ] + }, + { + "pr": "25407", + "title": "[FIX] UI/UX issues on Live Chat widget", + "userLogin": "MartinSchoeler", + "milestone": "4.7.2", + "contributors": [ + "MartinSchoeler", + "dougfabris" + ] + }, + { + "pr": "25312", + "title": "Chore: Add Livechat repo into Monorepo packages", + "userLogin": "tiagoevanp", + "milestone": "4.7.2", + "contributors": [ + "tiagoevanp", + "ggazzo", + "web-flow", + "MartinSchoeler" + ] + }, + { + "pr": "25510", + "title": "Release 4.7.1", + "userLogin": "d-gubert", + "contributors": [ + "felipe-menelau", + "d-gubert", + "pierre-lehnen-rc" + ] + }, + { + "pr": "25471", + "title": "[FIX] Spotlight results showing usernames instead of real names", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.7.1", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "25434", + "title": "[FIX] LDAP sync removing users from channels when multiple groups are mapped to it", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.7.1", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "25441", + "title": "[NEW] Use setting to determine if initial general channel is needed", + "userLogin": "felipe-menelau", + "description": "- Adds flag responsible for overwriting #general channel creation", + "milestone": "4.7.1", + "contributors": [ + "felipe-menelau", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "25390", + "title": "Release 4.7.0", + "userLogin": "d-gubert", + "contributors": [ + "sampaiodiego", + "web-flow", + "lingohub[bot]", + "dependabot[bot]", + "ggazzo", + "dougfabris", + "gabriellsh", + "tmontini", + "debdutdeb", + "Himanshu664", + "yash-rajpal", + "MartinSchoeler" + ] + }, + { + "pr": "25380", + "title": "Regression: Fix clicking on visitor's chat in the sidebar does not display the chat window", + "userLogin": "filipemarins", + "description": "Fix: livechat room not opening.", + "milestone": "4.7.0", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "25314", + "title": "Regression: Fix size of custom emoji and render emoji on thread message preview", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins", + "gabriellsh" + ] + }, + { + "pr": "25371", + "title": "Chore: Bump fuselage", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25336", + "title": "Chore: Add options to debug stdout and rate limiter", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25368", + "title": "Regression: Fix English i18n react text", + "userLogin": "d-gubert", + "description": "Incorrect text in reaction tooltip has been fixed", + "milestone": "4.7.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "25349", + "title": "Regression: Rocket.Chat Webapp not loading.", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc", + "gabriellsh" + ] + }, + { + "pr": "25317", + "title": "Regression: Fix multi line is not showing an empty line between lines", + "userLogin": "filipemarins", + "milestone": "4.7.0", + "contributors": [ + "filipemarins", + "gabriellsh" + ] + }, + { + "pr": "25320", + "title": "Regression: bump onboarding-ui version", + "userLogin": "guijun13", + "description": "- Bump to 'next' the onboarding-ui package from fuselage.\r\n- Update from 'companyEmail' to 'email' adminData usage types", + "contributors": [ + "guijun13" + ] + }, + { + "pr": "25335", + "title": "Chore: Create README.md for Rest Typings", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25327", + "title": "Regression: Messages in new message template Crashing.", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25323", + "title": "Regression: Better MongoDB connection management for micro services", + "userLogin": "sampaiodiego", + "milestone": "4.7.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25250", + "title": "Regression: Validate empty fields for Message template", + "userLogin": "gabriellsh", + "milestone": "4.8.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25319", + "title": "Regression: Fix the alpine image and dev UX installing matrix-rust-sdk-bindings", + "userLogin": "geekgonecrazy", + "description": "The package only included a few pre-built which caused all macs to have to compile every time they installed and also caused our alpine not to work.\r\n\r\nThis temporarily switches to a fork of the matrix-appservice-bridge package.\r\n\r\nMade changes to one of its child dependencies `matrix-rust-sdk-bindings` that adds pre-built binaries for mac and musl (for alpine).", + "milestone": "4.7.0", + "contributors": [ + "geekgonecrazy", + "web-flow", + "d-gubert" + ] + }, + { + "pr": "25255", + "title": "Regression: Change preference to be default legacy messages", + "userLogin": "gabriellsh", + "milestone": "4.8.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25306", + "title": "Regression: Fix reply button not working when hideFlexTab is enabled", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins", + "gabriellsh" + ] + }, + { + "pr": "25311", + "title": "Regression: Add eslint package to micro services Dockerfile", + "userLogin": "sampaiodiego", + "milestone": "4.7.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25218", + "title": "Chore: ensure scripts use cross-env and ignore some dirs (ROC-54)", + "userLogin": "souzaramon", + "description": "- data and test-failure should be ignored\r\n- ensure scripts use cross-env", + "contributors": [ + "souzaramon" + ] + }, + { + "pr": "25313", + "title": "Regression: Revert Bugsnag version", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25305", + "title": "Regression: eslint not running on packages", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc", + "ggazzo" + ] + }, + { + "pr": "25299", + "title": "Regression: Add `isPending` status to message", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "25301", + "title": "Regression: Shows error if micro service cannot connect to Mongo", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25287", + "title": "Regression: Use exact Node version on micro services Docker images", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25286", + "title": "Chore: Add root package.json to houston files", + "userLogin": "d-gubert", + "description": "See title", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "25284", + "title": "Chore: Sync with master", + "userLogin": "d-gubert", + "contributors": [ + "sampaiodiego", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "25269", + "title": "Chore: Minor dependency updates", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "25224", + "title": "Chore: Add yarn plugin to check node and yarn version", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25235", + "title": "Release 4.6.3", + "userLogin": "d-gubert", + "contributors": [ + "sampaiodiego", + "d-gubert" + ] + }, + { + "pr": "25220", + "title": "[FIX] Desktop notification on multi-instance environments", + "userLogin": "sampaiodiego", + "milestone": "4.6.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25280", + "title": "Chore: Remove package-lock.json from houston files", + "userLogin": "d-gubert", + "description": "Houston config in the `package.json` file still mentioned `package-lock.json`, but it doesn't exist anymore", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "25260", + "title": "[FIX] Adjust email label in Setup Wizard i18n files", + "userLogin": "guijun13", + "description": "- remove 'Company' label on onboarding email keys in certain languages", + "contributors": [ + "guijun13" + ] + }, + { + "pr": "25275", + "title": "Chore: Fix return type warnings", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23870", + "title": "[NEW] Expand Apps Engine's environment variable allowed list", + "userLogin": "cuonghuunguyen", + "milestone": "4.7.0", + "contributors": [ + null, + "debdutdeb", + "web-flow", + "cuonghuunguyen", + "dougfabris" + ] + }, + { + "pr": "25273", + "title": "Regression: Fix federation Matrix bridge startup", + "userLogin": "sampaiodiego", + "milestone": "4.7.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25092", + "title": "[FIX] Message preview not available for queued chats", + "userLogin": "murtaza98", + "milestone": "4.7.0", + "contributors": [ + "murtaza98", + "KevLehman" + ] + }, + { + "pr": "23688", + "title": "[NEW] Alpha Matrix Federation", + "userLogin": "alansikora", + "description": "Experimental support for Matrix Federation with a Bridge\r\n\r\nhttps://user-images.githubusercontent.com/51996/164530391-e8b17ecd-a4d0-4ef8-a8b7-81230c1773d3.mp4", + "milestone": "4.7.0", + "contributors": [ + "alansikora", + "geekgonecrazy", + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "25259", + "title": "Chore: Bump Fuselage packages", + "userLogin": "dougfabris", + "milestone": "4.7.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25261", + "title": "[FIX] Incorrect websocket url in livechat widget", + "userLogin": "debdutdeb", + "milestone": "4.7.0", + "contributors": [ + "debdutdeb" + ] + }, + { + "pr": "25007", + "title": "[FIX] Showing Blank Message Inside Report", + "userLogin": "nishant23122000", + "description": "https://user-images.githubusercontent.com/53515714/161038085-4a86c7ae-6751-4996-9767-b1c9e0331a6c.mp4", + "contributors": [ + "nishant23122000" + ] + }, + { + "pr": "25251", + "title": "Regression: Add select message to system message and thread preview and allow select on legacy template", + "userLogin": "filipemarins", + "milestone": "4.7.0", + "contributors": [ + "filipemarins", + "ggazzo", + "web-flow", + "gabriellsh", + "dougfabris" + ] + }, + { + "pr": "25239", + "title": "[FIX] Add katex render to new message react template", + "userLogin": "filipemarins", + "milestone": "4.7.0", + "contributors": [ + "filipemarins", + "ggazzo", + "dougfabris" + ] + }, + { + "pr": "25257", + "title": "Chore: Update Livechat to the last version", + "userLogin": "tiagoevanp", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "24515", + "title": "[FIX] Custom sound error toast messages", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664", + "dougfabris" + ] + }, + { + "pr": "25211", + "title": "Regression: Avatar not loading on first direct message", + "userLogin": "filipemarins", + "description": "fix avatar not loading on a first direct message", + "milestone": "4.7.0", + "contributors": [ + "filipemarins", + "ggazzo" + ] + }, + { + "pr": "25254", + "title": "Regression: Show username and real name on the message system", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "25217", + "title": "[IMPROVE] Performance for some Omnichannel features", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25200", + "title": "[FIX] room creation fails if app framework is disabled", + "userLogin": "debdutdeb", + "milestone": "4.7.0", + "contributors": [ + "debdutdeb" + ] + }, + { + "pr": "24565", + "title": "[IMPROVE] Add OTR Room States", + "userLogin": "yash-rajpal", + "description": "Earlier OTR room uses only 2 states, we need more states to support future features. \r\nThis adds more states for the OTR contextualBar.\r\n\r\n- Expired\r\n\"Screen\r\n\r\n- Declined\r\nScreen Shot 2022-04-20 at 13 49 28\r\n\r\n- Error\r\n\"Screen", + "milestone": "4.7.0", + "contributors": [ + "yash-rajpal", + "dougfabris" + ] + }, + { + "pr": "25170", + "title": "[FIX] Client disconnection on network loss", + "userLogin": "amolghode1981", + "description": "Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online\r\nunless agent explicitly logs off.\r\nAgent can stop receiving calls in multiple ways due to network loss. Network loss can happen in following ways.\r\n1. User tries to switch the network. User experiences a glitch of disconnectivity. This can be simulated by turning the network off\r\nin the network tab of chrome's dev tool. This can disconnect the UA if the disconnection happens just before the registration refresh.\r\n2. Second reason is when computer goes in sleep mode.\r\n3. Third reason is that when asterisk is crashed/in maintenance mode/explicitly stopped.\r\n\r\nSolution:\r\nThe idea is to detect the network disconnection and start the start the attempts to reconnect.\r\nThe detection of the disconnection does not happen in case#1. The SIPUA's UserAgent transport does not\r\ncall onDisconnected when network loss of such kind happens. To tackle this problem, window's online and offline event handlers are\r\nused.\r\n\r\nThe number of retries is configurable but ideally it is to be kept at -1. Whenever disconnection happens, it should keep on trying to\r\nreconnect with increasing backoff time. This behaviour is useful when the asterisk is stopped.\r\n\r\nWhen the server is disconnected, it should be indicated on the phone button.", + "contributors": [ + "amolghode1981", + "KevLehman" + ] + }, + { + "pr": "25244", + "title": "[FIX] Read receipts show with color gray when not read yet", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins", + "gabriellsh" + ] + }, + { + "pr": "25230", + "title": "[FIX] VoIP disabled/enabled sequence puts voip agent in error state", + "userLogin": "amolghode1981", + "description": "Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side)\r\n\r\nIt was realised that the race does not happen because of the database or server noticing the changes late. But because of the time taken to establish the AMI connection with Asterisk.\r\n\r\nSolution:\r\n\r\n1. Change apps/meteor/app/voip/server/startup.ts. When VoIP_Enabled is changed, await for Voip.init() to complete and then broadcast connector.statuschanged with changed value.\r\n2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance.\r\n3. in apps/meteor/client/providers/CallProvider/hooks/useVoipClient.ts add the event handler that receives this event. Change voipEnabled from constant to state. Change this state based on the 'value' that is received by the handler.", + "contributors": [ + "amolghode1981", + "KevLehman" + ] + }, + { + "pr": "25087", + "title": "[NEW] Add expire index to integration history", + "userLogin": "geekgonecrazy", + "milestone": "4.7.0", + "contributors": [ + "geekgonecrazy" + ] + }, + { + "pr": "24521", + "title": "Chore: update OTR icon", + "userLogin": "kibonusp", + "description": "I changed the shredder icon in OTR contextual bar to the stopwatch icon, recently added to the fuselage.", + "milestone": "4.7.0", + "contributors": [ + "kibonusp", + "yash-rajpal", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "25237", + "title": "[FIX] Toolbox hiding under contextual bar", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25231", + "title": "[IMPROVE] Added MaxNickNameLength and MaxBioLength constants", + "userLogin": "aakash-gitdev", + "contributors": [ + "aakash-gitdev", + "web-flow", + "gabriellsh" + ] + }, + { + "pr": "25220", + "title": "[FIX] Desktop notification on multi-instance environments", + "userLogin": "sampaiodiego", + "milestone": "4.6.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25175", + "title": "[FIX] Reply button behavior on broadcast channel", + "userLogin": "filipemarins", + "description": "Hide reply button for the user that sent the message", + "contributors": [ + "filipemarins", + "web-flow" + ] + }, + { + "pr": "25216", + "title": "[FIX] Read receipts showing before message read", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "25222", + "title": "[FIX] Add reaction not working in legacy messages", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25223", + "title": "Chore: Add error boundary to message component", + "userLogin": "gabriellsh", + "description": "Not crash the whole application if something goes wrong in the MessageList component.\r\n\r\n![image](https://user-images.githubusercontent.com/40830821/162269915-931c5c3c-c979-4234-b74c-371f67467ce0.png)", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25130", + "title": "Chore: Update Livechat version", + "userLogin": "tiagoevanp", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "25073", + "title": "[FIX] AgentOverview analytics wrong departmentId parameter", + "userLogin": "paulobernardoaf", + "description": "When filtering the analytics charts by department, data would not appear because the object:\r\n```js\r\n{\r\n value: \"department-id\",\r\n label: \"department-name\"\r\n}\r\n```\r\nwas being used in the `departmentId` parameter.\r\n\r\n- Before:\r\n![image](https://user-images.githubusercontent.com/30026625/161832057-d96ffd21-a7dd-421e-bfaa-3b9f4a9127b2.png)\r\n\r\n- After:\r\n![image](https://user-images.githubusercontent.com/30026625/161831092-9ee77b51-b083-4f45-9c48-ab2e0511c4d6.png)", + "milestone": "4.7.0", + "contributors": [ + "paulobernardoaf" + ] + }, + { + "pr": "25056", + "title": "[FIX] Close room when dismiss wrap up call modal", + "userLogin": "tiagoevanp", + "milestone": "4.7.0", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "25208", + "title": "Regression: yarn dev triggers build dependencies", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "24714", + "title": "[FIX] Added invalid password error message", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664", + "dougfabris" + ] + }, + { + "pr": "25196", + "title": "Chore: Tests with Playwright (task: ROC-28, 09-channels)", + "userLogin": "tmontini", + "contributors": [ + "tmontini" + ] + }, + { + "pr": "25174", + "title": "Chore: Template to generate packages", + "userLogin": "ggazzo", + "description": "```\r\nnpx hygen package new test\r\n```", + "contributors": [ + "ggazzo", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "25193", + "title": "Regression: Fix micro services Docker build", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25191", + "title": "Release 4.6.2", + "userLogin": "sampaiodiego", + "contributors": [ + "sidmohanty11", + "sampaiodiego" + ] + }, + { + "pr": "25101", + "title": "[FIX] Database indexes not being created", + "userLogin": "sampaiodiego", + "milestone": "4.6.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24933", + "title": "[FIX] Deactivating user breaks if user is the only room owner", + "userLogin": "sidmohanty11", + "description": "## Before\r\n\r\nhttps://user-images.githubusercontent.com/73601258/160000871-cfc2f2a5-2a59-4d27-8049-7754d003dd48.mp4\r\n\r\n\r\n\r\n## After\r\nhttps://user-images.githubusercontent.com/73601258/159998287-681ab475-ff33-4282-82ff-db751c59a392.mp4", + "milestone": "4.6.2", + "contributors": [ + "sidmohanty11", + "sampaiodiego" + ] + }, + { + "pr": "25095", + "title": "Release 4.6.1", + "userLogin": "sampaiodiego", + "contributors": [ + "dougfabris", + "sampaiodiego", + "gabriellsh", + "yash-rajpal", + "pierre-lehnen-rc" + ] + }, + { + "pr": "25022", + "title": "[FIX] Proxy settings being ignored", + "userLogin": "pierre-lehnen-rc", + "description": "Modify Meteor's `HTTP.call` to add back proxy support", + "milestone": "4.6.1", + "contributors": [ + "pierre-lehnen-rc", + "sampaiodiego" + ] + }, + { + "pr": "25082", + "title": "[FIX] Invitation links don't redirect to the registration form", + "userLogin": "yash-rajpal", + "milestone": "4.6.1", + "contributors": [ + "yash-rajpal" + ] + }, + { + "pr": "25069", + "title": "[FIX] FormData uploads not working", + "userLogin": "gabriellsh", + "milestone": "4.6.1", + "contributors": [ + "gabriellsh", + "dougfabris" + ] + }, + { + "pr": "25067", + "title": "[FIX] NPS never finishing sending results", + "userLogin": "sampaiodiego", + "milestone": "4.6.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25050", + "title": "[FIX] Upgrade Tab showing for a split second", + "userLogin": "gabriellsh", + "milestone": "4.6.1", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25055", + "title": "[FIX] UserAutoComplete not rendering UserAvatar correctly", + "userLogin": "dougfabris", + "description": "### before\r\n![Screen Shot 2022-04-04 at 16 50 21](https://user-images.githubusercontent.com/27704687/161620921-800bf66a-806d-4f83-b2e1-073c34215001.png)\r\n\r\n### after\r\n![Screen Shot 2022-04-04 at 16 49 00](https://user-images.githubusercontent.com/27704687/161620720-3e27774d-c241-46ca-b764-932a9295d709.png)", + "milestone": "4.6.1", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25180", + "title": "Chore: Remove duplicated useUserRoom", + "userLogin": "dougfabris", + "milestone": "4.7.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25167", + "title": "Chore: TS migration SortList", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "24933", + "title": "[FIX] Deactivating user breaks if user is the only room owner", + "userLogin": "sidmohanty11", + "description": "## Before\r\n\r\nhttps://user-images.githubusercontent.com/73601258/160000871-cfc2f2a5-2a59-4d27-8049-7754d003dd48.mp4\r\n\r\n\r\n\r\n## After\r\nhttps://user-images.githubusercontent.com/73601258/159998287-681ab475-ff33-4282-82ff-db751c59a392.mp4", + "milestone": "4.6.2", + "contributors": [ + "sidmohanty11", + "sampaiodiego" + ] + }, + { + "pr": "25181", + "title": "Regression: Fix services Docker build on CI", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25089", + "title": "[FIX] UserCard sanitization", + "userLogin": "dougfabris", + "description": "- Rewrites the component to TS\r\n- Fixes some visual issues\r\n\r\n### before\r\n![Screen Shot 2022-04-07 at 00 23 11](https://user-images.githubusercontent.com/27704687/162113925-5c9484d1-23e9-4623-8b86-3fbc71b461a1.png)\r\n\r\n### after\r\n![Screen Shot 2022-04-07 at 00 07 13](https://user-images.githubusercontent.com/27704687/162112353-afd6aac6-b27c-4470-a642-631b8080d59e.png)", + "milestone": "4.7.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25085", + "title": "Chore: move definitions to packages", + "userLogin": "ggazzo", + "milestone": "3.7.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25168", + "title": "Regression: CI playwright", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25125", + "title": "Chore: Convert NotificationStatus to TS", + "userLogin": "jeanfbrito", + "contributors": [ + "jeanfbrito", + "ggazzo" + ] + }, + { + "pr": "25148", + "title": "[FIX] Message menu action not working on legacy messages.", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25122", + "title": "Chore: Tests with Playwright (task: All works)", + "userLogin": "weslley543", + "contributors": [ + "weslley543" + ] + }, + { + "pr": "25129", + "title": "Chore: Remove old files from removed Omnichannel feature", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25128", + "title": "Chore: Convert admin custom sound to tsx", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25126", + "title": "Chore: Migrate oauth2server to typescript", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25123", + "title": "Chore: Convert LivechatAgentActivity to raw model and TS", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25124", + "title": "Chore: Remove unused Drone CI files", + "userLogin": "geekgonecrazy", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "25121", + "title": "Chore: Convert Mailer to TS", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti", + "sampaiodiego" + ] + }, + { + "pr": "25107", + "title": "Regression: Fix CI monorepo build", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25074", + "title": "Chore: Monorepo ", + "userLogin": "ggazzo", + "milestone": "3.7.0", + "contributors": [ + "sampaiodiego", + "ggazzo" + ] + }, + { + "pr": "25097", + "title": "[IMPROVE] Rename upgrade tab routes", + "userLogin": "guijun13", + "description": "Change 'upgrade tab' routes names from camelCase ('goFullyFeatured') to kebab-case ('go-fully-featured') due to URL naming consistency. Changed types, main function and test.", + "contributors": [ + "guijun13" + ] + }, + { + "pr": "25076", + "title": "Bump eslint-plugin-anti-trojan-source from 1.0.6 to 1.1.0", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24936", + "title": "[FIX] End call button disappearing when on-hold", + "userLogin": "tiagoevanp", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "24932", + "title": "[FIX] Use correct room property for call ended at", + "userLogin": "MartinSchoeler", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "25022", + "title": "[FIX] Proxy settings being ignored", + "userLogin": "pierre-lehnen-rc", + "description": "Modify Meteor's `HTTP.call` to add back proxy support", + "milestone": "4.6.1", + "contributors": [ + "pierre-lehnen-rc", + "sampaiodiego" + ] + }, + { + "pr": "25082", + "title": "[FIX] Invitation links don't redirect to the registration form", + "userLogin": "yash-rajpal", + "milestone": "4.6.1", + "contributors": [ + "yash-rajpal" + ] + }, + { + "pr": "23971", + "title": "[NEW] Message Template React Component", + "userLogin": "ggazzo", + "description": "Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template).\r\n\r\n\r\n\r\n![Screen Shot 2022-04-05 at 11 14 18](https://user-images.githubusercontent.com/27704687/161774027-38dd9c7b-eeeb-45e2-b9d8-ea2a9be8486d.png)\r\nIn case you encounter any problems, or want to compare, temporarily it is possible to use the old version\r\n\r\n\"image\"", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "25069", + "title": "[FIX] FormData uploads not working", + "userLogin": "gabriellsh", + "milestone": "4.6.1", + "contributors": [ + "gabriellsh", + "dougfabris" + ] + }, + { + "pr": "19866", + "title": "[FIX] Video and Audio not skipping forward", + "userLogin": "MartinSchoeler", + "contributors": [ + "MartinSchoeler", + "tassoevan", + "web-flow", + "dougfabris" + ] + }, + { + "pr": "25067", + "title": "[FIX] NPS never finishing sending results", + "userLogin": "sampaiodiego", + "milestone": "4.6.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24405", + "title": "[IMPROVE] Add tooltip to sidebar room menu", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664", + "web-flow", + "dougfabris" + ] + }, + { + "pr": "24431", + "title": "[IMPROVE] Added tooltip options for message menu", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664", + "dougfabris" + ] + }, + { + "pr": "24166", + "title": "[FIX] Replace encrypted text to Encrypted Message Placeholder", + "userLogin": "yash-rajpal", + "description": "### before \r\n![image](https://user-images.githubusercontent.com/27704687/150807900-154a9cdb-ee13-4333-8628-f287ab914b40.png)\r\n\r\n### after\r\n\"Screenshot", + "milestone": "4.7.0", + "contributors": [ + "yash-rajpal", + "albuquerquefabio", + "web-flow" + ] + }, + { + "pr": "24984", + "title": "[FIX] Prevent sequential messages edited icon to hide on hover", + "userLogin": "dougfabris", + "description": "### before\r\n\"Screen\r\n\r\n### after\r\n\"Screen", + "milestone": "4.7.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25024", + "title": "[IMPROVE] Improve active/hover colors in account sidebar", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664" + ] + }, + { + "pr": "24856", + "title": "[FIX] Full error message is visible", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664", + "tassoevan" + ] + }, + { + "pr": "24708", + "title": "Chore: Cancel running jobs if PR is updated", + "userLogin": "debdutdeb", + "contributors": [ + "debdutdeb", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24900", + "title": "Chore: organize test files and fix code coverage", + "userLogin": "tmontini", + "contributors": [ + null, + "tmontini", + "rodrigok" + ] + }, + { + "pr": "24464", + "title": "Chore: Missing keys in APIsDisplay", + "userLogin": "dougfabris", + "milestone": "4.7.0", + "contributors": [ + "dougfabris", + "tassoevan" + ] + }, + { + "pr": "25057", + "title": "Bump ejson from 2.2.1 to 2.2.2", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25053", + "title": "Chore: Remove Alpine image deps after using them", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25052", + "title": "Bump pino and pino-pretty", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25050", + "title": "[FIX] Upgrade Tab showing for a split second", + "userLogin": "gabriellsh", + "milestone": "4.6.1", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25055", + "title": "[FIX] UserAutoComplete not rendering UserAvatar correctly", + "userLogin": "dougfabris", + "description": "### before\r\n![Screen Shot 2022-04-04 at 16 50 21](https://user-images.githubusercontent.com/27704687/161620921-800bf66a-806d-4f83-b2e1-073c34215001.png)\r\n\r\n### after\r\n![Screen Shot 2022-04-04 at 16 49 00](https://user-images.githubusercontent.com/27704687/161620720-3e27774d-c241-46ca-b764-932a9295d709.png)", + "milestone": "4.6.1", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25031", + "title": "Chore: TS conversion folder client", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "24991", + "title": "Bump minimist from 1.2.5 to 1.2.6 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25002", + "title": "Bump template-file from 6.0.0 to 6.0.1", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25042", + "title": "Bump body-parser from 1.19.2 to 1.20.0 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25043", + "title": "i18n: Language update from LingoHub 🤖 on 2022-04-04Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null + ] + }, + { + "pr": "25028", + "title": "Merge master into develop & Set version to 4.7.0-develop", + "userLogin": "sampaiodiego", + "contributors": [ + "AllanPazRibeiro", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "25027", + "title": "Release 4.6.0", + "userLogin": "sampaiodiego", + "contributors": [ + "pierre-lehnen-rc", + "aswinidev", + "web-flow", + "renatobecker", + "sampaiodiego", + "dependabot[bot]", + "lingohub[bot]", + "matheusbsilva137", + "amolghode1981", + "debdutdeb", + "eduardofcabrera", + "juliajforesti", + "tiagoevanp", + "KevLehman" + ] + }, + { + "pr": "25021", + "title": "Bump @rocket.chat/emitter from 0.31.4 to 0.31.9 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25020", + "title": "Bump @rocket.chat/ui-kit from 0.31.4 to 0.31.9 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25019", + "title": "Bump @rocket.chat/message-parser from 0.31.4 to 0.31.9 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25018", + "title": "Bump @rocket.chat/string-helpers from 0.31.4 to 0.31.9 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25017", + "title": "Regression: Add createdOTR index", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24998", + "title": "Release 4.5.5", + "userLogin": "sampaiodiego", + "contributors": [ + "MartinSchoeler", + "sampaiodiego", + "filipemarins", + "tiagoevanp" + ] + }, + { + "pr": "24994", + "title": "[FIX] High CPU usage caused by CallProvider", + "userLogin": "tiagoevanp", + "description": "Remove infinity loop inside useVoipClient hook.\r\n\r\n#closes #24970", + "milestone": "4.5.5", + "contributors": [ + "ggazzo", + "tiagoevanp" + ] + }, + { + "pr": "24955", + "title": "[FIX] Multiple issues starting a new DM", + "userLogin": "filipemarins", + "description": "When the room object is searched for the first time, it does not exist on the front object yet (subscription), adding a fallback search for room list will guarantee to search the room details.\r\n\r\nbefore:\r\nhttps://user-images.githubusercontent.com/9275105/160223241-d2319f3e-82c5-47d6-867f-695ab2361a17.mp4\r\n\r\nafter:\r\nhttps://user-images.githubusercontent.com/9275105/160223244-84d0d2a1-3d95-464d-8b8a-e264b0d4d690.mp4", + "milestone": "4.5.5", + "contributors": [ + "filipemarins", + "pierre-lehnen-rc" + ] + }, + { + "pr": "24990", + "title": "Chore: Update Livechat", + "userLogin": "MartinSchoeler", + "milestone": "4.5.5", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24938", + "title": "Release 4.5.4", + "userLogin": "AllanPazRibeiro", + "contributors": [ + "geekgonecrazy", + "AllanPazRibeiro" + ] + }, + { + "pr": "24930", + "title": "[FIX] SAML Force name to string", + "userLogin": "geekgonecrazy", + "milestone": "4.5.4", + "contributors": [ + "geekgonecrazy", + "web-flow", + "pierre-lehnen-rc" + ] + }, + { + "pr": "25015", + "title": "Chore: Bump Fuselage packages", + "userLogin": "dougfabris", + "description": "It uses the last stable version of Fuselage packages.", + "milestone": "4.6.0", + "contributors": [ + "dougfabris", + "tassoevan" + ] + }, + { + "pr": "24999", + "title": "Regression: Custom roles displaying ID instead of name on some admin screens", + "userLogin": "pierre-lehnen-rc", + "description": "![image](https://user-images.githubusercontent.com/55164754/160981416-555bcaa1-c075-4260-937c-64523472da43.png)\r\n![image](https://user-images.githubusercontent.com/55164754/160981452-6eae4e74-8425-4073-8256-472aba72b9db.png)", + "milestone": "4.6.0", + "contributors": [ + "pierre-lehnen-rc", + "dougfabris", + "web-flow" + ] + }, + { + "pr": "24835", + "title": "[NEW] Upgrade Tab", + "userLogin": "gabriellsh", + "description": "![image](https://user-images.githubusercontent.com/27704687/160172260-c656282e-a487-4092-948d-d11c9bacb598.png)", + "milestone": "4.6.0", + "contributors": [ + "gabriellsh", + "dougfabris", + "web-flow", + "tassoevan", + "pierre-lehnen-rc" + ] + }, + { + "pr": "24980", + "title": "Regression: Error is raised when there's no Asterisk queue available yet", + "userLogin": "amolghode1981", + "milestone": "4.6.0", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "24994", + "title": "[FIX] High CPU usage caused by CallProvider", + "userLogin": "tiagoevanp", + "description": "Remove infinity loop inside useVoipClient hook.\r\n\r\n#closes #24970", + "milestone": "4.5.5", + "contributors": [ + "ggazzo", + "tiagoevanp" + ] + }, + { + "pr": "24955", + "title": "[FIX] Multiple issues starting a new DM", + "userLogin": "filipemarins", + "description": "When the room object is searched for the first time, it does not exist on the front object yet (subscription), adding a fallback search for room list will guarantee to search the room details.\r\n\r\nbefore:\r\nhttps://user-images.githubusercontent.com/9275105/160223241-d2319f3e-82c5-47d6-867f-695ab2361a17.mp4\r\n\r\nafter:\r\nhttps://user-images.githubusercontent.com/9275105/160223244-84d0d2a1-3d95-464d-8b8a-e264b0d4d690.mp4", + "milestone": "4.5.5", + "contributors": [ + "filipemarins", + "pierre-lehnen-rc" + ] + }, + { + "pr": "24969", + "title": "Chore: Storybook mocking and examples improved", + "userLogin": "tassoevan", + "description": "- Stories from `ee/` included;\r\n- Differentiate root story kinds;\r\n- Mocking of `ServerContext` via Storybook parameters.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "24990", + "title": "Chore: Update Livechat", + "userLogin": "MartinSchoeler", + "milestone": "4.5.5", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24989", + "title": "Revert: [NEW] Engagement Statistics", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24897", + "title": "[FIX] Room archived/unarchived system messages aren't sent when editing room settings", + "userLogin": "matheusbsilva137", + "description": "- Send the \"Room archived\" and \"Room unarchived\" system messages when editing room settings (and not only when rooms are archived/unarchived with the slash-command);\r\n- Fix the \"Hide System Messages\" option for the \"Room archived\" and \"Room unarchived\" system messages;", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "24925", + "title": "Chore: add some missing REST definitions", + "userLogin": "gerzonc", + "description": "On the [mobile client](https://github.com/RocketChat/Rocket.Chat.ReactNative), we made an effort to collect more `REST API` definitions that are missing on the server side during our migration to TypeScript. Since we're both migrating to TypeScript, we thought it would be a good idea to share those so you guys can benefit from our initiative.", + "contributors": [ + "gerzonc" + ] + }, + { + "pr": "24971", + "title": "i18n: Language update from LingoHub 🤖 on 2022-03-28Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null + ] + }, + { + "pr": "24921", + "title": "[FIX] Register with Secret URL", + "userLogin": "yash-rajpal", + "contributors": [ + "yash-rajpal" + ] + }, + { + "pr": "24948", + "title": "Regression: Fix unexpected errors breaking ddp-streamer", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24320", + "title": "[FIX] LDAP avatars being rotated according to metadata even if the setting to rotate uploads is off", + "userLogin": "matheusbsilva137", + "description": "- Use the `FileUpload_RotateImages` setting (**Administration > File Upload > Rotate images on upload**) to control whether avatars should be rotated automatically based on their data (XEIF);\r\n- Display the avatar image preview (orientation) according to the `FileUpload_RotateImages` setting.", + "milestone": "4.6.0", + "contributors": [ + "matheusbsilva137", + "web-flow", + "pierre-lehnen-rc" + ] + }, + { + "pr": "24930", + "title": "[FIX] SAML Force name to string", + "userLogin": "geekgonecrazy", + "milestone": "4.5.4", + "contributors": [ + "geekgonecrazy", + "web-flow", + "pierre-lehnen-rc" + ] + }, + { + "pr": "24908", + "title": "Regression: Call doesn't stop ringing after agent unregistration", + "userLogin": "MartinSchoeler", + "milestone": "4.6.0", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24777", + "title": "[NEW] Engagement Statistics", + "userLogin": "eduardofcabrera", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow" + ] + }, + { + "pr": "24920", + "title": "Regression: Fix account service login expiration", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24867", + "title": "[FIX] Duplicated \"jump to message\" button on starred messages", + "userLogin": "Himanshu664", + "contributors": [ + "Himanshu664" + ] + }, + { + "pr": "24860", + "title": "[FIX] External search providers not working", + "userLogin": "tkurz", + "contributors": [ + "tkurz" + ] + }, + { + "pr": "24052", + "title": "[FIX] Several issues related to custom roles", + "userLogin": "pierre-lehnen-rc", + "description": "- Throw an error when trying to delete a role (User or Subscription role) that are still being used;\r\n- Fix \"Invalid Role\" error for custom roles in Role Editing sidebar;\r\n- Fix \"Users in Role\" screen for custom roles.", + "milestone": "4.6.0", + "contributors": [ + "pierre-lehnen-rc", + "matheusbsilva137", + "web-flow" + ] + }, + { + "pr": "24781", + "title": "[NEW] Telemetry Events", + "userLogin": "eduardofcabrera", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow" + ] + }, + { + "pr": "24887", + "title": "[IMPROVE] Adding new statistics related to voip and omnichannel", + "userLogin": "cauefcr", + "description": "- Total of Canned response messages sent\r\n- Total of tags used\r\n- Last-Chatted Agent Preferred (enabled/disabled)\r\n- Assign new conversations to the contact manager (enabled/disabled)\r\n- How to handle Visitor Abandonment setting\r\n- Amount of chats placed on hold\r\n- VoIP Enabled\r\n- Amount of VoIP Calls\r\n- Amount of VoIP Extensions connected\r\n- Amount of Calls placed on hold (1x per call)\r\n- Fixed Session Aggregation type definitions", + "milestone": "4.6.0", + "contributors": [ + "cauefcr", + "KevLehman" + ] + }, + { + "pr": "24911", + "title": "Chore: Remove old scripts", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24898", + "title": "[FIX] DDP Rate Limiter Translation key", + "userLogin": "gabriellsh", + "description": "Before:\r\n\"image\"\r\n\r\n\r\nNow:\r\n\"image\"", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "24831", + "title": "[FIX][ENTERPRISE] Notifications not being sent by ddp-streamer", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24884", + "title": "Release 4.5.3", + "userLogin": "AllanPazRibeiro", + "contributors": [ + "tiagoevanp", + "sampaiodiego", + "KevLehman", + "amolghode1981", + "ggazzo" + ] + }, + { + "pr": "24901", + "title": "[FIX] Custom script not being fired", + "userLogin": "ggazzo", + "milestone": "4.5.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "24877", + "title": "Chore: Fix MongoDB versions on release notes", + "userLogin": "sampaiodiego", + "milestone": "4.5.3", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24864", + "title": "[FIX] Disable voip button when call is in progress", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24863", + "title": "[FIX] Broken build caused by PRs modifying same file differently", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman", + "tiagoevanp" + ] + }, + { + "pr": "24838", + "title": "[FIX] [VOIP] SidebarFooter component ", + "userLogin": "tiagoevanp", + "description": "- Improve the CallProvider code;\r\n- Adjust the text case of the VoIP component on the FooterSidebar;\r\n- Fix the bad behavior with the changes in queue's name.", + "milestone": "4.5.3", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "24837", + "title": "[IMPROVE] Standarize queue behavior for managers and agents when subscribing", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24829", + "title": "[FIX] Show only enabled departments on forward", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24799", + "title": "[FIX] Wrong param usage on queue summary call", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24789", + "title": "[FIX] VoIP button gets disabled whenever user status changes", + "userLogin": "amolghode1981", + "milestone": "4.5.3", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "24752", + "title": "[FIX] Show call icon only when user has extension associated", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24748", + "title": "[IMPROVE] UX - VoIP Call Component", + "userLogin": "tiagoevanp", + "milestone": "4.5.3", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "24814", + "title": "Release 4.5.2", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "MartinSchoeler", + "pierre-lehnen-rc", + "tassoevan", + "debdutdeb", + "KevLehman", + "murtaza98", + "sampaiodiego", + "juliajforesti" + ] + }, + { + "pr": "24812", + "title": "[FIX] Revert AutoComplete", + "userLogin": "juliajforesti", + "milestone": "4.5.2", + "contributors": [ + "juliajforesti", + "ggazzo" + ] + }, + { + "pr": "24809", + "title": "Regression: Fix ParentRoomWithEndpointData in loop", + "userLogin": "sampaiodiego", + "milestone": "4.5.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24732", + "title": "[FIX] `PaginatedSelectFiltered` not handling changes", + "userLogin": "tassoevan", + "milestone": "4.5.2", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "24805", + "title": "[FIX] Critical: Incorrect visitor getting assigned to a chat from apps", + "userLogin": "murtaza98", + "milestone": "4.5.2", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24804", + "title": "[FIX] \"livechat/webrtc.call\" endpoint not working", + "userLogin": "murtaza98", + "milestone": "4.5.2", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24792", + "title": "[FIX] VoipExtensionsPage component call", + "userLogin": "KevLehman", + "milestone": "4.5.2", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24705", + "title": "[FIX] Broken multiple OAuth integrations", + "userLogin": "debdutdeb", + "milestone": "4.5.2", + "contributors": [ + "debdutdeb" + ] + }, + { + "pr": "24623", + "title": "[FIX] Opening a new DM from user card", + "userLogin": "tassoevan", + "description": "A race condition on `useRoomIcon` -- delayed merge of rooms and subscriptions -- was causing a UI crash whenever someone tried to open a DM from the user card component.", + "milestone": "4.5.2", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "24750", + "title": "[IMPROVE] Voip Extensions disabled state", + "userLogin": "MartinSchoeler", + "milestone": "4.5.2", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24782", + "title": "Release 4.5.1", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "renatobecker", + "pierre-lehnen-rc", + "sampaiodiego", + "matheusbsilva137", + "amolghode1981", + "juliajforesti", + "tiagoevanp", + "KevLehman", + "MartinSchoeler", + "Aman-Maheshwari", + "cuonghuunguyen" + ] + }, + { + "pr": "24760", + "title": "[FIX] Apple login script being loaded even when Apple Login is disabled.", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.5.1", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "24754", + "title": "Chore: Update Livechat", + "userLogin": "MartinSchoeler", + "milestone": "4.5.1", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24683", + "title": "[FIX] no id of room closer in livechat-close message", + "userLogin": "cuonghuunguyen", + "milestone": "4.5.1", + "contributors": [ + null + ] + }, + { + "pr": "23795", + "title": "[FIX] Reload roomslist after successful deletion of a room from admin panel.", + "userLogin": "Aman-Maheshwari", + "description": "Removed the logic for calling the `rooms.adminRooms` endPoint from the `RoomsTable` Component and moved it to its parent component `RoomsPage`.\r\nThis allows to call the endPoint `rooms.adminRooms` from `EditRoomContextBar` Component which is also has `RoomPage` Component as its parent.\r\n\r\nAlso added a succes toast message after the successful deletion of room.", + "milestone": "4.5.1", + "contributors": [ + "Aman-Maheshwari", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "24743", + "title": "[FIX] System messages are sent when adding or removing a group from a team", + "userLogin": "matheusbsilva137", + "description": "- Do not send system messages when adding or removing a new or existing _group_ from a team.", + "milestone": "4.5.1", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "24737", + "title": "[FIX] Typo and placeholder on wrap up call modal", + "userLogin": "MartinSchoeler", + "milestone": "4.5.1", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24680", + "title": "[FIX] Show only available agents on extension association modal", + "userLogin": "KevLehman", + "milestone": "4.5.1", + "contributors": [ + "KevLehman", + "tiagoevanp" + ] + }, + { + "pr": "24607", + "title": "[FIX] VoIP Enable/Disable setting on CallContext/CallProvider Notifications", + "userLogin": "tiagoevanp", + "milestone": "4.5.1", + "contributors": [ + "tiagoevanp", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "24677", + "title": "[FIX] Components for user search", + "userLogin": "juliajforesti", + "milestone": "4.5.1", + "contributors": [ + "juliajforesti", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "24657", + "title": "[FIX] Voip Stream Reinitialization Error", + "userLogin": "amolghode1981", + "milestone": "4.5.1", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "24696", + "title": "[FIX] Room's message count not being incremented on import", + "userLogin": "matheusbsilva137", + "description": "- Fix rooms' message counter not being incremented on message import.", + "milestone": "4.5.1", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "24674", + "title": "[FIX] Missing username on messages imported from Slack", + "userLogin": "matheusbsilva137", + "description": "- Fix missing sender's username on messages imported from Slack.", + "milestone": "4.5.1", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "24590", + "title": "[FIX] Duplicated 'name' log key", + "userLogin": "sampaiodiego", + "milestone": "4.5.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24661", + "title": "[FIX] Typo in wrap-up term", + "userLogin": "renatobecker", + "milestone": "4.5.1", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "24606", + "title": "[FIX] Push privacy config to not show username not being respected", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24901", + "title": "[FIX] Custom script not being fired", + "userLogin": "ggazzo", + "milestone": "4.5.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "24896", + "title": "[FIX] Wrong business hour behavior", + "userLogin": "murtaza98", + "milestone": "4.6.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24845", + "title": "[FIX] Ignore customClass on messages", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24879", + "title": "[FIX] Apple OAuth", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24895", + "title": "i18n: Language update from LingoHub 🤖 on 2022-03-21Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null + ] + }, + { + "pr": "24749", + "title": "[IMPROVE] New omnichannel statistics and async statistics processing.", + "userLogin": "cauefcr", + "description": "https://app.clickup.com/t/1z4zg4e", + "contributors": [ + "cauefcr" + ] + }, + { + "pr": "24882", + "title": "[FIX] Missing dependency on useEffect at CallProvider", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24877", + "title": "Chore: Fix MongoDB versions on release notes", + "userLogin": "sampaiodiego", + "milestone": "4.5.3", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24779", + "title": "[FIX] auto-join team channels not honoring user preferences", + "userLogin": "ostjen", + "contributors": [ + "ostjen" + ] + }, + { + "pr": "24869", + "title": "Bump pino from 7.8.1 to 7.9.1 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24870", + "title": "Bump pino-pretty from 7.5.3 to 7.5.4 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24864", + "title": "[FIX] Disable voip button when call is in progress", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24863", + "title": "[FIX] Broken build caused by PRs modifying same file differently", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman", + "tiagoevanp" + ] + }, + { + "pr": "24850", + "title": "Regression: Role Sync not always working", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "24838", + "title": "[FIX] [VOIP] SidebarFooter component ", + "userLogin": "tiagoevanp", + "description": "- Improve the CallProvider code;\r\n- Adjust the text case of the VoIP component on the FooterSidebar;\r\n- Fix the bad behavior with the changes in queue's name.", + "milestone": "4.5.3", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "24837", + "title": "[IMPROVE] Standarize queue behavior for managers and agents when subscribing", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24789", + "title": "[FIX] VoIP button gets disabled whenever user status changes", + "userLogin": "amolghode1981", + "milestone": "4.5.3", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "24799", + "title": "[FIX] Wrong param usage on queue summary call", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24829", + "title": "[FIX] Show only enabled departments on forward", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24823", + "title": "i18n: Language update from LingoHub 🤖 on 2022-03-14Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24833", + "title": "Bump @types/mailparser from 3.0.2 to 3.4.0", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24832", + "title": "Bump @types/clipboard from 2.0.1 to 2.0.7", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24822", + "title": "Bump @types/nodemailer from 6.4.2 to 6.4.4", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24821", + "title": "Bump body-parser from 1.19.0 to 1.19.2", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24820", + "title": "Bump @types/ws from 8.5.2 to 8.5.3 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24764", + "title": "Chore: Add E2E tests for livechat/visitor", + "userLogin": "Muramatsu2602", + "description": "- Create a new test suite file under tests/end-to-end/api/livechat\r\n- Create tests for the following endpoints:\r\n + livechat/visitor (create visitor, update visitor, add custom fields to visitors)", + "contributors": [ + "Muramatsu2602", + "KevLehman" + ] + }, + { + "pr": "24729", + "title": "Chore: Add E2E tests for livechat/room.close", + "userLogin": "Muramatsu2602", + "description": "* Create a new test suite file under tests/end-to-end/api/livechat\r\n * Create tests for the following endpoint:\r\n\t + ivechat/room.close", + "contributors": [ + "Muramatsu2602", + "web-flow", + "KevLehman" + ] + }, + { + "pr": "24785", + "title": "[FIX] German translation for Monitore", + "userLogin": "JMoVS", + "contributors": [ + "JMoVS", + "web-flow" + ] + }, + { + "pr": "24812", + "title": "[FIX] Revert AutoComplete", + "userLogin": "juliajforesti", + "milestone": "4.5.2", + "contributors": [ + "juliajforesti", + "ggazzo" + ] + }, + { + "pr": "24747", + "title": "Chore: APIClass types", + "userLogin": "felipe-rod123", + "description": "This pull request creates a new `restivus` module (.d.ts) for the `api.js` file.", + "contributors": [ + "felipe-rod123", + "ggazzo" + ] + }, + { + "pr": "24801", + "title": "Bump is-svg from 4.3.1 to 4.3.2", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24803", + "title": "Bump prometheus-gc-stats from 0.6.2 to 0.6.3", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24810", + "title": "Chore: Skip local services changes when shutting down duplicated services", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24629", + "title": "[FIX] \"Match error\" when converting a team to a channel", + "userLogin": "matheusbsilva137", + "description": "- Fix \"Match error\" when trying to convert a channel to a team;", + "milestone": "4.6.0", + "contributors": [ + "matheusbsilva137", + "web-flow" + ] + }, + { + "pr": "24809", + "title": "Regression: Fix ParentRoomWithEndpointData in loop", + "userLogin": "sampaiodiego", + "milestone": "4.5.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24397", + "title": "Chore: Get Settings Statistics", + "userLogin": "albuquerquefabio", + "contributors": [ + "albuquerquefabio" + ] + }, + { + "pr": "24732", + "title": "[FIX] `PaginatedSelectFiltered` not handling changes", + "userLogin": "tassoevan", + "milestone": "4.5.2", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "24628", + "title": "Chore: converted more hooks to typescript", + "userLogin": "felipe-rod123", + "description": "Converted some functions on `client/hooks/` from JavaScript to Typescript.", + "contributors": [ + "felipe-rod123", + "ggazzo" + ] + }, + { + "pr": "24506", + "title": "Chore: added settings endpoint types", + "userLogin": "felipe-rod123", + "description": "Created typing for endpoint definitions on `settings.ts`.", + "contributors": [ + "felipe-rod123", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "24226", + "title": "[FIX] Handle Other Formats inside Upload Avatar", + "userLogin": "nishant23122000", + "description": "After resolving issue #24213 : \r\n\r\n\r\nhttps://user-images.githubusercontent.com/53515714/150325012-91413025-786e-4ce0-ae75-629f6b05b024.mp4", + "milestone": "4.6.0", + "contributors": [ + "nishant23122000", + "debdutdeb", + "web-flow", + "murtaza98" + ] + }, + { + "pr": "24424", + "title": "[FIX] Prune Message issue", + "userLogin": "nishant23122000", + "milestone": "4.6.0", + "contributors": [ + "nishant23122000", + "debdutdeb", + "web-flow" + ] + }, + { + "pr": "24805", + "title": "[FIX] Critical: Incorrect visitor getting assigned to a chat from apps", + "userLogin": "murtaza98", + "milestone": "4.5.2", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24804", + "title": "[FIX] \"livechat/webrtc.call\" endpoint not working", + "userLogin": "murtaza98", + "milestone": "4.5.2", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24507", + "title": "Chore: added Server Instances endpoint types", + "userLogin": "felipe-rod123", + "description": "Created typing for endpoint definitions on `instances.ts`.", + "contributors": [ + "felipe-rod123" + ] + }, + { + "pr": "24758", + "title": "[FIX] Prevent call button toggle when user is on call", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24800", + "title": "Regression: Register services right away", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24792", + "title": "[FIX] VoipExtensionsPage component call", + "userLogin": "KevLehman", + "milestone": "4.5.2", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24384", + "title": "Chore: Convert server functions from javascript to typescript", + "userLogin": "felipe-rod123", + "description": "This pull request will be used to rewrite some functions on the Chat Engine to Typescript, in order to increase security and specify variable types on the code.", + "contributors": [ + "felipe-rod123", + "ggazzo" + ] + }, + { + "pr": "24705", + "title": "[FIX] Broken multiple OAuth integrations", + "userLogin": "debdutdeb", + "milestone": "4.5.2", + "contributors": [ + "debdutdeb" + ] + }, + { + "pr": "24793", + "title": "[FIX][ENTERPRISE] Auto reload feature of ddp-streamer micro service", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24783", + "title": "Bump pino from 7.8.0 to 7.8.1 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23121", + "title": "Bump jschardet from 1.6.0 to 3.0.0", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24752", + "title": "[FIX] Show call icon only when user has extension associated", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24623", + "title": "[FIX] Opening a new DM from user card", + "userLogin": "tassoevan", + "description": "A race condition on `useRoomIcon` -- delayed merge of rooms and subscriptions -- was causing a UI crash whenever someone tried to open a DM from the user card component.", + "milestone": "4.5.2", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "24750", + "title": "[IMPROVE] Voip Extensions disabled state", + "userLogin": "MartinSchoeler", + "milestone": "4.5.2", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24748", + "title": "[IMPROVE] UX - VoIP Call Component", + "userLogin": "tiagoevanp", + "milestone": "4.5.3", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "24753", + "title": "Chore: Micro services fixes and cleanup", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24756", + "title": "Regression: Improve Sidenav open/close handling and fixed codeql configs and E2E tests", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "24760", + "title": "[FIX] Apple login script being loaded even when Apple Login is disabled.", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.5.1", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "24754", + "title": "Chore: Update Livechat", + "userLogin": "MartinSchoeler", + "milestone": "4.5.1", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24683", + "title": "[FIX] no id of room closer in livechat-close message", + "userLogin": "cuonghuunguyen", + "milestone": "4.5.1", + "contributors": [ + null + ] + }, + { + "pr": "24771", + "title": "Chore: fix grammatical errors in Features", + "userLogin": "aadishJ01", + "contributors": [ + "aadishJ01", + "web-flow" + ] + }, + { + "pr": "24759", + "title": "Chore: Fix grammatical errors in Code of Conduct", + "userLogin": "aadishJ01", + "contributors": [ + "aadishJ01", + "web-flow" + ] + }, + { + "pr": "23795", + "title": "[FIX] Reload roomslist after successful deletion of a room from admin panel.", + "userLogin": "Aman-Maheshwari", + "description": "Removed the logic for calling the `rooms.adminRooms` endPoint from the `RoomsTable` Component and moved it to its parent component `RoomsPage`.\r\nThis allows to call the endPoint `rooms.adminRooms` from `EditRoomContextBar` Component which is also has `RoomPage` Component as its parent.\r\n\r\nAlso added a succes toast message after the successful deletion of room.", + "milestone": "4.5.1", + "contributors": [ + "Aman-Maheshwari", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "24743", + "title": "[FIX] System messages are sent when adding or removing a group from a team", + "userLogin": "matheusbsilva137", + "description": "- Do not send system messages when adding or removing a new or existing _group_ from a team.", + "milestone": "4.5.1", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "24544", + "title": "Chore: Fix Cypress tests", + "userLogin": "rodrigok", + "contributors": [ + "rodrigok", + "tassoevan", + "dougfabris" + ] + }, + { + "pr": "24737", + "title": "[FIX] Typo and placeholder on wrap up call modal", + "userLogin": "MartinSchoeler", + "milestone": "4.5.1", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24739", + "title": "[IMPROVE][ENTERPRISE] Don't start presence monitor when running micro services", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24738", + "title": "[FIX][ENTERPRISE] DDP streamer not sending data to all clients", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24680", + "title": "[FIX] Show only available agents on extension association modal", + "userLogin": "KevLehman", + "milestone": "4.5.1", + "contributors": [ + "KevLehman", + "tiagoevanp" + ] + }, + { + "pr": "24710", + "title": "[FIX] DDP streamer errors", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24724", + "title": "[FIX][ENTERPRISE] Presence micro service logic", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24717", + "title": "i18n: Language update from LingoHub 🤖 on 2022-03-07Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24607", + "title": "[FIX] VoIP Enable/Disable setting on CallContext/CallProvider Notifications", + "userLogin": "tiagoevanp", + "milestone": "4.5.1", + "contributors": [ + "tiagoevanp", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "24726", + "title": "Chore: Improve logger to allow log of `unknown` values", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24677", + "title": "[FIX] Components for user search", + "userLogin": "juliajforesti", + "milestone": "4.5.1", + "contributors": [ + "juliajforesti", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "24542", + "title": "[FIX] Date Message Export Filter Fix", + "userLogin": "eduardofcabrera", + "description": "Fix message export filter to get all messages between \"from date\" and \"to date\", including \"to date\".", + "contributors": [ + "eduardofcabrera", + "web-flow" + ] + }, + { + "pr": "24709", + "title": "[FIX] API Error preventing adding an email to users without one (like bot/app users)", + "userLogin": "debdutdeb", + "contributors": [ + "debdutdeb" + ] + }, + { + "pr": "24716", + "title": "Bump ts-node from 10.6.0 to 10.7.0 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24476", + "title": "[FIX] Nextcloud OAuth for incomplete token URL", + "userLogin": "debdutdeb", + "milestone": "4.6.0", + "contributors": [ + "debdutdeb" + ] + }, + { + "pr": "24657", + "title": "[FIX] Voip Stream Reinitialization Error", + "userLogin": "amolghode1981", + "milestone": "4.5.1", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "24698", + "title": "Bump pino-pretty from 7.5.2 to 7.5.3 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24696", + "title": "[FIX] Room's message count not being incremented on import", + "userLogin": "matheusbsilva137", + "description": "- Fix rooms' message counter not being incremented on message import.", + "milestone": "4.5.1", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "23824", + "title": "Chore: Improvements on role syncing (ldap, oauth and saml)", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc", + "tassoevan" + ] + }, + { + "pr": "24689", + "title": "Bump pino-pretty from 7.5.1 to 7.5.2 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24674", + "title": "[FIX] Missing username on messages imported from Slack", + "userLogin": "matheusbsilva137", + "description": "- Fix missing sender's username on messages imported from Slack.", + "milestone": "4.5.1", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "24642", + "title": "Bump actions/setup-node from 2 to 3", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24644", + "title": "i18n: Language update from LingoHub 🤖 on 2022-02-28Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24590", + "title": "[FIX] Duplicated 'name' log key", + "userLogin": "sampaiodiego", + "milestone": "4.5.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24668", + "title": "Bump actions/checkout from 2 to 3", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24574", + "title": "Chore(deps-dev): Bump @types/mock-require from 2.0.0 to 2.0.1", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24667", + "title": "Bump ts-node from 10.5.0 to 10.6.0 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24666", + "title": "Bump @types/ws from 8.2.3 to 8.5.2 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24640", + "title": "Bump url-parse from 1.5.7 to 1.5.10", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24653", + "title": "Merge master into develop & Set version to 4.6.0-develop", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc", + "web-flow" + ] + }, + { + "pr": "24652", + "title": "Release 4.5.0", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "sampaiodiego", + "web-flow", + "aswinidev", + "debdutdeb", + "dependabot[bot]", + "lingohub[bot]", + "ostjen", + "KevLehman", + "dougfabris", + "LucasFASouza", + "felipe-rod123", + "guijun13", + "pierre-lehnen-rc", + "filipemarins", + "matheusbsilva137", + "gabriellsh" + ] + }, + { + "pr": "24661", + "title": "[FIX] Typo in wrap-up term", + "userLogin": "renatobecker", + "milestone": "4.5.1", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "24028", + "title": "[IMPROVE] Updated links in readme", + "userLogin": "aswinidev", + "contributors": [ + "aswinidev", + "web-flow", + "debdutdeb" + ] + }, + { + "pr": "24651", + "title": "Chore: Update Apps-Engine", + "userLogin": "d-gubert", + "milestone": "4.5.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "24649", + "title": "Regression: Refresh server connection when MI server settings change", + "userLogin": "KevLehman", + "milestone": "4.5.0", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24648", + "title": "Regression: Prevent button from losing state when rerendering", + "userLogin": "KevLehman", + "milestone": "4.5.0", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24585", + "title": "Regression: Error setting user avatars and mentioning rooms on Slack Import", + "userLogin": "matheusbsilva137", + "description": "- Fix `Mentioned room not found` error when importing rooms from Slack;\r\n- Fix `Forbidden` error when setting avatars for users imported from Slack (on user import/creation);\r\n- Fix incorrect message count on imported rooms;\r\n- Fix missing username on messages imported from Slack;", + "contributors": [ + "matheusbsilva137", + "pierre-lehnen-rc" + ] + }, + { + "pr": "24647", + "title": "Regression: Fix wrong tab name for VoIP settings", + "userLogin": "renatobecker", + "milestone": "4.5.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "24646", + "title": "Regression: Server crashing if Voip credentials are invalid", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24645", + "title": "Regression: Extension List panel UI not aligned with designs", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24635", + "title": "Regression: Queue counter aggregator for incoming/hanged calls", + "userLogin": "amolghode1981", + "milestone": "4.5.0", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "24630", + "title": "Regression: Fix double value on holdTime and empty msg on last message", + "userLogin": "MartinSchoeler", + "contributors": [ + "MartinSchoeler", + "web-flow" + ] + }, + { + "pr": "24624", + "title": "Regression: If Asterisk suddenly goes down, server has no way to know. Causes server to get stuck. Needs restart", + "userLogin": "amolghode1981", + "milestone": "4.5.0", + "contributors": [ + "amolghode1981", + "KevLehman" + ] + }, + { + "pr": "24601", + "title": "Regression: Prevent connect to asterisk when VoIP is disabled", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98", + "web-flow", + "KevLehman" + ] + }, + { + "pr": "24626", + "title": "Regression: Encode registration info as JWT when signing key is provided", + "userLogin": "KevLehman", + "milestone": "4.5.0", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24625", + "title": "Regression: Fix time fields and wrap up in Voip Room Contexual bar", + "userLogin": "MartinSchoeler", + "milestone": "4.5.0", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24592", + "title": "Regression: Fix in-correct room status shown to agents", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24619", + "title": "Regression: Do not show toast on incoming voip calls", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98", + "web-flow" + ] + }, + { + "pr": "24616", + "title": "Regression: Fix incoming voip call ringtone is not ringing", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98", + "web-flow" + ] + }, + { + "pr": "24610", + "title": "Regression: Mark all rooms as read modal closing instantly.", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "24615", + "title": "Regression: Fix translation for call started message", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98", + "web-flow" + ] + }, + { + "pr": "24594", + "title": "Regression: Bunch of settings fixes for VoIP", + "userLogin": "MartinSchoeler", + "milestone": "4.5.0", + "contributors": [ + "MartinSchoeler", + "web-flow" + ] + }, + { + "pr": "24609", + "title": "Regression: Admin Sidebar colors inverted.", + "userLogin": "gabriellsh", + "milestone": "4.5.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "24602", + "title": "Regression: No audio when call comes from Skype/IP phone", + "userLogin": "amolghode1981", + "description": "The audio was not rendered because of re-rendering of react element based on\r\nqueueCounter and roomInfo. queueCounter and roomInfo cause the dom to re-render when call gets accepted\r\nbecause after accepting call, queueCounter changes or a room gets created.\r\nThe audio element gets recreated. But VoIP user probably holds the old one.\r\nThe behaviour is not predictable when such case happens. If everything gets cleanly setup,\r\neven if the audio element goes headless, it still continues to play the remote audio.\r\nBut in other cases, it is unreferenced the one on dom has its srcObject as null.\r\nThis causes no audio.\r\n\r\nThis fix provides a way to re-initialise the rendering elements in VoIP user\r\nand calls this function on useEffect() if the re-render has happen.", + "milestone": "4.5.0", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "24596", + "title": "Regression: Fixes in Voice Contextual Bar and Directory", + "userLogin": "MartinSchoeler", + "milestone": "4.5.0", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24603", + "title": "Regression: Fix time format on Voip system messages", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24598", + "title": "Regression: VoIP service button displayed when VoIP is disabled", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24581", + "title": "Regression: Add support to namespace within micro services", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24583", + "title": "Regression: Error when trying to load name of dm rooms for avatars and notifications", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "24567", + "title": "[NEW] Marketplace sort filter", + "userLogin": "ujorgeleite", + "description": "Implemented a sort filter for the marketplace screen. This component sorts the marketplace apps list in 4 ways, alphabetical order(A-Z), inverse alphabetical order(Z-A), most recently updated(MRU), and least recent updated(LRU). Besides that, I've generalized some components and types to increase code reusability, renamed some helpers as well as deleted some useless ones, and inserted the necessary new translations on the English i18n dictionary.\r\nDemo gif:\r\n![Marketplace sort filter](https://user-images.githubusercontent.com/43561537/155033709-e07a6306-a85a-4f7f-9624-b53ba5dd7fa9.gif)", + "milestone": "4.5.0", + "contributors": [ + "rique223", + "ujorgeleite" + ] + }, + { + "pr": "23102", + "title": "[NEW] VoIP Support for Omnichannel", + "userLogin": "KevLehman", + "description": "- Created VoipService to manage VoIP connections and PBX connection\r\n- Created LivechatVoipService that will handle custom cases for livechat (creating rooms, assigning chats to queue, actions when call is finished, etc)\r\n- Created Basic interfaces to support new services and new model\r\n- Created Endpoints for management interfaces\r\n- Implemented asterisk connector on VoIP service\r\n- Created UI components to show calls incoming and to allow answering/rejecting calls\r\n- Added new settings to control call server/management server connection values\r\n- Added endpoints to associate Omnichannel Agents with PBX Extensions\r\n- Added support for event listening on server side, to get metadata about calls being received/ongoing\r\n- Created new pages to update settings & to see user-extension association\r\n- Created new page to see ongoing calls (and past calls)\r\n- Added support for remote hangup/hold on calls\r\n- Implemented call metrics calculation (hold time, waiting time, talk time)\r\n- Show a notificaiton when call is received", + "milestone": "4.5.0", + "contributors": [ + "KevLehman", + "amolghode1981", + "web-flow", + "tiagoevanp", + "murtaza98", + "MartinSchoeler" + ] + }, + { + "pr": "24562", + "title": "Regression: Fix room not getting created due to null visitor status", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24573", + "title": "Chore: Bump Fuselage packages", + "userLogin": "tassoevan", + "description": "It uses the last stable version of Fuselage packages.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "24558", + "title": "i18n: Language update from LingoHub 🤖 on 2022-02-21Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24572", + "title": "[FIX] 2FA via email when logging in using OAuth", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24568", + "title": "Chore: Update Apps-Engine", + "userLogin": "d-gubert", + "milestone": "4.5.0", + "contributors": [ + "d-gubert", + "web-flow" + ] + }, + { + "pr": "24536", + "title": "Chore: roomTypes: Stop mixing client and server code together", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.5.0", + "contributors": [ + "pierre-lehnen-rc", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "24529", + "title": "[IMPROVE] Replace AutoComplete in UserAutoComplete & UserAutoCompleteMultiple components", + "userLogin": "juliajforesti", + "description": "This PR replaces a deprecated fuselage's component `AutoComplete` in favor of `Select` and `MultiSelect` which fixes some of UX/UI issues in selecting users\r\n\r\n### before\r\n![Screen Shot 2022-02-19 at 13 33 28](https://user-images.githubusercontent.com/27704687/154809737-8181a06c-4f20-48ea-90f7-01e828b9a452.png)\r\n\r\n### after\r\n![Screen Shot 2022-02-19 at 13 30 58](https://user-images.githubusercontent.com/27704687/154809653-a8ec9a80-c0dd-4a25-9c00-0f96147d79e9.png)", + "contributors": [ + "juliajforesti", + "dougfabris", + "tassoevan" + ] + }, + { + "pr": "24513", + "title": "Chore: Run tests using microservices deployment on CI", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "24556", + "title": "Bump @types/ws from 8.2.2 to 8.2.3 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24501", + "title": "Chore: Update fuselage deps to match monolith versions", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24538", + "title": "Bump adm-zip from 0.4.14 to 0.5.9", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24454", + "title": "[IMPROVE] Purchase Type Filter for marketplace apps and Categories filter anchor refactoring", + "userLogin": "rique223", + "description": "Implemented a filter by purchase type(free or paid) component for the apps screen of the marketplace. Besides that, new entries on the dictionary, fixed some parts of the App type (purchaseType was typed as unknown and price as string), and created some helpers to work alongside the filter. Will be refactoring the categories filter anchor and then will open this PR for reviews.\r\n\r\nDemo gif:\r\n![purchaseTypeFIlter](https://user-images.githubusercontent.com/43561537/153101228-7b7ebdc3-2d34-420f-aa9d-f7cbc8d4b53f.gif)\r\n\r\nRefactored the categories filter anchor from a plain fuselage select to a select button with dynamic colors.\r\nDemo gif:\r\n![New categories filter anchor(PR)](https://user-images.githubusercontent.com/43561537/153422427-28012b7d-e0ec-45f4-861d-c9368c57ad04.gif)", + "contributors": [ + "rique223", + "dougfabris", + "web-flow" + ] + }, + { + "pr": "24475", + "title": "[IMPROVE] Skip encryption for slash commands in E2E rooms", + "userLogin": "yash-rajpal", + "description": "Currently Slash Commands don't work in an E2EE room, as we encrypt the message before slash command is detected by the server, So removed encryption for slash commands in e2e rooms.", + "contributors": [ + "yash-rajpal", + "albuquerquefabio", + "web-flow" + ] + }, + { + "pr": "24304", + "title": "Chore: Js to ts slash commands archive", + "userLogin": "eduardofcabrera", + "description": "Convert Slash Commands archive files to typescript", + "contributors": [ + "eduardofcabrera", + "web-flow" + ] + }, + { + "pr": "24114", + "title": "[NEW] E2E password generator", + "userLogin": "ostjen", + "contributors": [ + "ostjen", + "web-flow", + "eduardofcabrera", + "tassoevan" + ] + }, + { + "pr": "24553", + "title": "[FIX] Omnichannel managers can't join chats in progress", + "userLogin": "renatobecker", + "milestone": "4.5.0", + "contributors": [ + "renatobecker", + "murtaza98", + "web-flow" + ] + }, + { + "pr": "24559", + "title": "[FIX] Room context tabs not working in Omnichannel current chats page", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24173", + "title": "[FIX] respect `Accounts_Registration_Users_Default_Roles` setting", + "userLogin": "debdutdeb", + "description": "- Fix `user` role being added as default regardless of the `Accounts_Registration_Users_Default_Roles` setting.", + "milestone": "4.5.0", + "contributors": [ + "debdutdeb", + "web-flow", + "matheusbsilva137" + ] + }, + { + "pr": "24485", + "title": "[FIX] Skip admin info in setup wizard for servers with admin registered", + "userLogin": "dougfabris", + "contributors": [ + "dougfabris", + "tassoevan" + ] + }, + { + "pr": "24537", + "title": "Bump pm2 from 5.1.2 to 5.2.0 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24209", + "title": "[IMPROVE] Team system messages feedback", + "userLogin": "ostjen", + "description": "- Delete some keys that aren't being used (eg: User_left_female).\r\n- Add new Teams' system messages:\r\n - `added-user-to-team`: **added** @\\user to this Team;\r\n - `removed-user-from-team`: **removed** @\\user from this Team;\r\n - `user-converted-to-team`: **converted** #\\room to a Team;\r\n - `user-converted-to-channel`: **converted** #\\room to a Channel;\r\n - `user-removed-room-from-team`: **removed** @\\user from this Team;\r\n - `user-deleted-room-from-team`: **deleted** #\\room from this Team;\r\n - `user-added-room-to-team`: **deleted** #\\room to this Team;\r\n- Add the corresponding options to hide each new system message and the missing `ujt` and `ult` hide options.", + "milestone": "4.5.0", + "contributors": [ + "ostjen", + "tassoevan", + "web-flow", + "dougfabris", + "matheusbsilva137" + ] + }, + { + "pr": "24467", + "title": "Chore: Improve PR title validation regex", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow", + "debdutdeb" + ] + }, + { + "pr": "24058", + "title": "Bump date-fns from 2.24.0 to 2.28.0", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24508", + "title": "[FIX] Read receipts showing first messages of the room as read even if not read by everyone", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow", + "dougfabris" + ] + }, + { + "pr": "24530", + "title": "Chore: Remove storybook build job from CI", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24528", + "title": "Bump url-parse from 1.5.3 to 1.5.7", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24333", + "title": "Chore: Add description to global OTR setting", + "userLogin": "pedrogssouza", + "contributors": [ + "pedrogssouza", + "yash-rajpal", + "web-flow" + ] + }, + { + "pr": "24382", + "title": "[IMPROVE] OTR system messages", + "userLogin": "yash-rajpal", + "description": "OTR system messages to indicate key refresh and joining chat to users.", + "contributors": [ + "yash-rajpal", + "web-flow" + ] + }, + { + "pr": "24121", + "title": "[IMPROVE] Descriptive tooltip for Encrypted Key on Room Header", + "userLogin": "yash-rajpal", + "milestone": "4.5.0", + "contributors": [ + "yash-rajpal", + "web-flow" + ] + }, + { + "pr": "24522", + "title": "Bump express from 4.17.2 to 4.17.3 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24518", + "title": "Chore: `twoFactorRequired` signature", + "userLogin": "tassoevan", + "description": "Improved type checking for decorator `twoFactorRequired`.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "24517", + "title": "Bump body-parser from 1.19.1 to 1.19.2 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24441", + "title": "[FIX] GDPR action to forget visitor data on request", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman", + "murtaza98", + "web-flow" + ] + }, + { + "pr": "24306", + "title": "Chore: Convert to typescript the slash commands create files", + "userLogin": "eduardofcabrera", + "description": "Convert Slash Commands create files to typescript.", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow" + ] + }, + { + "pr": "24325", + "title": "Chore: Convert to typescript the mute and unmute slash commands files", + "userLogin": "eduardofcabrera", + "description": "Convert to typescript the mute and unmute slash commands files", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow" + ] + }, + { + "pr": "24321", + "title": "Chore: Convert to typescript the me slashCommands files", + "userLogin": "eduardofcabrera", + "description": "Convert to typescript the me slashCommands files", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow" + ] + }, + { + "pr": "23512", + "title": "Bump sodium-native from 3.2.1 to 3.3.0 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24311", + "title": "Chore: Convert to typescript the slash commands invite files", + "userLogin": "eduardofcabrera", + "description": "Convert to typescript the slash commands invite files", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow" + ] + }, + { + "pr": "24509", + "title": "Bump vm2 from 3.9.5 to 3.9.7 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24451", + "title": "[IMPROVE] ChatBox Text to File Description", + "userLogin": "eduardofcabrera", + "description": "The text content from chatbox goes to the file description when drag and drop a file.", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow", + "dougfabris" + ] + }, + { + "pr": "24461", + "title": "Chore: Update Meteor to 2.5.6", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "24477", + "title": "Chore: Update ws package", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24498", + "title": "Bump underscore.string from 3.3.5 to 3.3.6 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24491", + "title": "Bump follow-redirects from 1.14.7 to 1.14.8 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24493", + "title": "i18n: Language update from LingoHub 🤖 on 2022-02-14Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null + ] + }, + { + "pr": "24331", + "title": "Chore: Convert to typescript the unarchive slash commands files", + "userLogin": "eduardofcabrera", + "description": "Convert to typescript the unarchive slash commands files", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow" + ] + }, + { + "pr": "24483", + "title": "[IMPROVE] Add tooltips on action buttons of Canned Response message composer", + "userLogin": "LucasFASouza", + "description": "The tooltips were missing on the action buttons of CR message composer.\r\n\r\n![image](https://user-images.githubusercontent.com/32396925/153620327-91107245-4b47-4d39-a99a-6da6d1cf5734.png)\r\n\r\nUsers can now feel more encouraged to use these actions knowing what they are supposed to do.", + "contributors": [ + "LucasFASouza", + "tiagoevanp", + "web-flow" + ] + }, + { + "pr": "24196", + "title": "Chore: Delete unused file (NewAdminInfoPage.js)", + "userLogin": "gabriellsh", + "description": "Just removing a duplicated/unused file.", + "milestone": "4.5.0", + "contributors": [ + "gabriellsh", + "web-flow" + ] + }, + { + "pr": "24388", + "title": "[IMPROVE][ENTERPRISE] Improve how micro services are loaded", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "24458", + "title": "[IMPROVE] Add return button in chats opened from the list of current chats", + "userLogin": "LucasFASouza", + "description": "The new return button for Omnichannel chats came out with release 3.15 but the feature was only available for chats that were opened from Omnichannel Contact Center.\r\nNow, the same UI/UX is supported for chats opened from Current Chats list.\r\n\r\n![image](https://user-images.githubusercontent.com/32396925/153283190-bd5c9748-c36b-4874-a704-6043afc7e3a1.png)\r\n\r\nThe chat now opens in the Omnichannel settings and has the return button so the user can go back to the Current Chats list.\r\n\r\n![image](https://user-images.githubusercontent.com/32396925/153285591-fad8e4a0-d2ea-4a02-8b2a-15e383b3c876.png)", + "contributors": [ + "LucasFASouza", + "tiagoevanp", + "web-flow" + ] + }, + { + "pr": "24469", + "title": "Bump express from 4.17.1 to 4.17.2 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24472", + "title": "Bump cookie from 0.4.1 to 0.4.2 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24275", + "title": "[IMPROVE] Close modal on esc and outside click", + "userLogin": "gabriellsh", + "description": "This is a QUICK change in order to close modals pressing Esc button and clicking outside of it **intentionally**.", + "milestone": "4.5.0", + "contributors": [ + "gabriellsh", + "dougfabris", + "tassoevan" + ] + }, + { + "pr": "24435", + "title": "Chore(deps-dev): Bump ts-node from 10.0.0 to 10.5.0 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24041", + "title": "[IMPROVE] Add user to room on \"Click to Join!\" button press", + "userLogin": "matheusbsilva137", + "description": "- Add user to room on \"Click to Join!\" button press;\r\n- Display the \"Join\" button in discussions inside channels (keeping the behavior consistent with discussions inside groups).", + "contributors": [ + "matheusbsilva137", + "web-flow", + "tassoevan", + "pierre-lehnen-rc", + "ostjen" + ] + }, + { + "pr": "24310", + "title": "[FIX] Implement client errors on ddp-streamer", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "23963", + "title": "Bump body-parser from 1.19.0 to 1.19.1 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23961", + "title": "Bump jaeger-client from 3.18.1 to 3.19.0 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24466", + "title": "[FIX] typo on register server tooltip of setup wizard", + "userLogin": "filipemarins", + "milestone": "4.5.0", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "24037", + "title": "[FIX] Inconsistent validation of user's access to rooms", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc", + "ostjen", + "web-flow" + ] + }, + { + "pr": "24450", + "title": "[FIX] OAuth mismatch redirect_uri error", + "userLogin": "sampaiodiego", + "milestone": "4.4.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24305", + "title": "[FIX] Prevent Apps Bridge to remove visitor status from room", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman", + "d-gubert" + ] + }, + { + "pr": "24453", + "title": "Chore: bump fuselage version", + "userLogin": "dougfabris", + "milestone": "4.4.2", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "24253", + "title": "[FIX] Issues on selecting users when importing CSV", + "userLogin": "guijun13", + "description": "* Fix users selecting by fixing their _id\r\n* Add condition to disable 'Start importing' button if `usersCount`, `channelsCount` and `messageCount` equals 0, or if messageCount is alone\r\n* Remove `disabled={usersCount === 0}` on user Tab", + "contributors": [ + "guijun13", + "tassoevan", + "web-flow", + "pierre-lehnen-rc" + ] + }, + { + "pr": "24299", + "title": "Chore(deps): Bump node-fetch from 2.6.1 to 2.6.7 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24418", + "title": "[FIX] Oembed request not respecting payload limit", + "userLogin": "sampaiodiego", + "milestone": "4.4.1", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24429", + "title": "i18n: Language update from LingoHub 🤖 on 2022-02-07Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24407", + "title": "[FIX] Skip cloud steps for registered servers on setup wizard", + "userLogin": "dougfabris", + "milestone": "4.4.1", + "contributors": [ + "dougfabris", + "tassoevan", + "gabriellsh", + "web-flow" + ] + }, + { + "pr": "24410", + "title": "Chore: Convert JS files to Typescript", + "userLogin": "felipe-rod123", + "description": "This pull request converts 26 more files from Javascript to Typescript, to check variable types and increase validation on the code.", + "contributors": [ + "felipe-rod123", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "24369", + "title": "[IMPROVE] Convert tag edit with department data to tsx", + "userLogin": "LucasFASouza", + "contributors": [ + "LucasFASouza", + "tiagoevanp", + "web-flow" + ] + }, + { + "pr": "24401", + "title": "[FIX] Outgoing webhook without scripts not saving messages", + "userLogin": "sampaiodiego", + "milestone": "4.4.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24334", + "title": "[IMPROVE] CloudLoginModal visual consistency", + "userLogin": "dougfabris", + "description": "### before\r\n![image](https://user-images.githubusercontent.com/27704687/151585064-dc6a1e29-9903-4241-8fbd-dfbe6c55fbef.png)\r\n\r\n### after\r\n![Screen Shot 2022-01-28 at 13 32 02](https://user-images.githubusercontent.com/27704687/151585101-75b98502-9aae-4198-bc3e-4956750e5d8b.png)", + "milestone": "4.5.0", + "contributors": [ + "dougfabris", + "gabriellsh", + "web-flow" + ] + }, + { + "pr": "24409", + "title": "[FIX] Startup errors creating indexes", + "userLogin": "sampaiodiego", + "description": "Fix `bio` and `prid` startup index creation errors.", + "milestone": "4.4.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24406", + "title": "Chore: Unify ILivechatAgent with ILivechatAgentRecord", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24381", + "title": "[FIX] Add ?close to OAuth callback url", + "userLogin": "sampaiodiego", + "milestone": "4.4.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24387", + "title": "[FIX] Slash commands previews not working", + "userLogin": "ostjen", + "milestone": "4.4.1", + "contributors": [ + "ostjen" + ] + }, + { + "pr": "24357", + "title": "i18n: Language update from LingoHub 🤖 on 2022-01-31Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null + ] + }, + { + "pr": "24341", + "title": "Bump simple-get from 4.0.0 to 4.0.1", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24366", + "title": "Chore: Set Docker image tag to latest only when really latest", + "userLogin": "debdutdeb", + "contributors": [ + "debdutdeb", + "web-flow" + ] + }, + { + "pr": "24109", + "title": "[IMPROVE] Added a new \"All\" tab which shows all integrations in Integrations", + "userLogin": "aswinidev", + "milestone": "4.5.0", + "contributors": [ + "aswinidev", + "dougfabris", + "web-flow" + ] + }, + { + "pr": "24363", + "title": "Merge master into develop & Set version to 4.5.0-develop", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "25022", + "title": "[FIX] Proxy settings being ignored", + "userLogin": "pierre-lehnen-rc", + "description": "Modify Meteor's `HTTP.call` to add back proxy support", + "milestone": "4.6.1", + "contributors": [ + "pierre-lehnen-rc", + "sampaiodiego" + ] + }, + { + "pr": "25067", + "title": "[FIX] NPS never finishing sending results", + "userLogin": "sampaiodiego", + "milestone": "4.6.1", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "4.4.5": { + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "25022", + "title": "[FIX] Proxy settings being ignored", + "userLogin": "pierre-lehnen-rc", + "description": "Modify Meteor's `HTTP.call` to add back proxy support", + "milestone": "4.6.1", + "contributors": [ + "pierre-lehnen-rc", + "sampaiodiego" + ] + }, + { + "pr": "25067", + "title": "[FIX] NPS never finishing sending results", + "userLogin": "sampaiodiego", + "milestone": "4.6.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25580", + "title": "Release 4.7.2", + "userLogin": "d-gubert", + "contributors": [ + "tiagoevanp", + "d-gubert", + "MartinSchoeler", + "ggazzo", + "cauefcr", + "geekgonecrazy" + ] + }, + { + "pr": "25544", + "title": "[FIX] Initial User not added to default channel", + "userLogin": "geekgonecrazy", + "description": "If injecting initial user. The user wasn’t added to the default General channel", + "milestone": "4.7.2", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "25520", + "title": "[FIX] User abandonment setting was not working doe to failing event hook", + "userLogin": "cauefcr", + "description": "A setting watcher and the query for grabbing abandoned chats were broken, now they're not.", + "milestone": "4.7.2", + "contributors": [ + "cauefcr", + "tiagoevanp" + ] + }, + { + "pr": "25495", + "title": "[FIX] Dynamic load matrix is enabled and handle failure ", + "userLogin": "ggazzo", + "milestone": "4.7.2", + "contributors": [ + "ggazzo", + "geekgonecrazy" + ] + }, + { + "pr": "25409", + "title": "[FIX] One of the triggers was not working correctly", + "userLogin": "MartinSchoeler", + "milestone": "4.7.2", + "contributors": [ + "MartinSchoeler", + "tiagoevanp" + ] + }, + { + "pr": "25407", + "title": "[FIX] UI/UX issues on Live Chat widget", + "userLogin": "MartinSchoeler", + "milestone": "4.7.2", + "contributors": [ + "MartinSchoeler", + "dougfabris" + ] + }, + { + "pr": "25312", + "title": "Chore: Add Livechat repo into Monorepo packages", + "userLogin": "tiagoevanp", + "milestone": "4.7.2", + "contributors": [ + "tiagoevanp", + "ggazzo", + "web-flow", + "MartinSchoeler" + ] + }, + { + "pr": "25510", + "title": "Release 4.7.1", + "userLogin": "d-gubert", + "contributors": [ + "felipe-menelau", + "d-gubert", + "pierre-lehnen-rc" + ] + }, + { + "pr": "25471", + "title": "[FIX] Spotlight results showing usernames instead of real names", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.7.1", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "25434", + "title": "[FIX] LDAP sync removing users from channels when multiple groups are mapped to it", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.7.1", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "25441", + "title": "[NEW] Use setting to determine if initial general channel is needed", + "userLogin": "felipe-menelau", + "description": "- Adds flag responsible for overwriting #general channel creation", + "milestone": "4.7.1", + "contributors": [ + "felipe-menelau", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "25390", + "title": "Release 4.7.0", + "userLogin": "d-gubert", + "contributors": [ + "sampaiodiego", + "web-flow", + "lingohub[bot]", + "dependabot[bot]", + "ggazzo", + "dougfabris", + "gabriellsh", + "tmontini", + "debdutdeb", + "Himanshu664", + "yash-rajpal", + "MartinSchoeler" + ] + }, + { + "pr": "25380", + "title": "Regression: Fix clicking on visitor's chat in the sidebar does not display the chat window", + "userLogin": "filipemarins", + "description": "Fix: livechat room not opening.", + "milestone": "4.7.0", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "25314", + "title": "Regression: Fix size of custom emoji and render emoji on thread message preview", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins", + "gabriellsh" + ] + }, + { + "pr": "25371", + "title": "Chore: Bump fuselage", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25336", + "title": "Chore: Add options to debug stdout and rate limiter", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25368", + "title": "Regression: Fix English i18n react text", + "userLogin": "d-gubert", + "description": "Incorrect text in reaction tooltip has been fixed", + "milestone": "4.7.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "25349", + "title": "Regression: Rocket.Chat Webapp not loading.", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc", + "gabriellsh" + ] + }, + { + "pr": "25317", + "title": "Regression: Fix multi line is not showing an empty line between lines", + "userLogin": "filipemarins", + "milestone": "4.7.0", + "contributors": [ + "filipemarins", + "gabriellsh" + ] + }, + { + "pr": "25320", + "title": "Regression: bump onboarding-ui version", + "userLogin": "guijun13", + "description": "- Bump to 'next' the onboarding-ui package from fuselage.\r\n- Update from 'companyEmail' to 'email' adminData usage types", + "contributors": [ + "guijun13" + ] + }, + { + "pr": "25335", + "title": "Chore: Create README.md for Rest Typings", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25327", + "title": "Regression: Messages in new message template Crashing.", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25323", + "title": "Regression: Better MongoDB connection management for micro services", + "userLogin": "sampaiodiego", + "milestone": "4.7.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25250", + "title": "Regression: Validate empty fields for Message template", + "userLogin": "gabriellsh", + "milestone": "4.8.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25319", + "title": "Regression: Fix the alpine image and dev UX installing matrix-rust-sdk-bindings", + "userLogin": "geekgonecrazy", + "description": "The package only included a few pre-built which caused all macs to have to compile every time they installed and also caused our alpine not to work.\r\n\r\nThis temporarily switches to a fork of the matrix-appservice-bridge package.\r\n\r\nMade changes to one of its child dependencies `matrix-rust-sdk-bindings` that adds pre-built binaries for mac and musl (for alpine).", + "milestone": "4.7.0", + "contributors": [ + "geekgonecrazy", + "web-flow", + "d-gubert" + ] + }, + { + "pr": "25255", + "title": "Regression: Change preference to be default legacy messages", + "userLogin": "gabriellsh", + "milestone": "4.8.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25306", + "title": "Regression: Fix reply button not working when hideFlexTab is enabled", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins", + "gabriellsh" + ] + }, + { + "pr": "25311", + "title": "Regression: Add eslint package to micro services Dockerfile", + "userLogin": "sampaiodiego", + "milestone": "4.7.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25218", + "title": "Chore: ensure scripts use cross-env and ignore some dirs (ROC-54)", + "userLogin": "souzaramon", + "description": "- data and test-failure should be ignored\r\n- ensure scripts use cross-env", + "contributors": [ + "souzaramon" + ] + }, + { + "pr": "25313", + "title": "Regression: Revert Bugsnag version", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25305", + "title": "Regression: eslint not running on packages", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc", + "ggazzo" + ] + }, + { + "pr": "25299", + "title": "Regression: Add `isPending` status to message", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "25301", + "title": "Regression: Shows error if micro service cannot connect to Mongo", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25287", + "title": "Regression: Use exact Node version on micro services Docker images", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25286", + "title": "Chore: Add root package.json to houston files", + "userLogin": "d-gubert", + "description": "See title", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "25284", + "title": "Chore: Sync with master", + "userLogin": "d-gubert", + "contributors": [ + "sampaiodiego", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "25269", + "title": "Chore: Minor dependency updates", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "25224", + "title": "Chore: Add yarn plugin to check node and yarn version", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25235", + "title": "Release 4.6.3", + "userLogin": "d-gubert", + "contributors": [ + "sampaiodiego", + "d-gubert" + ] + }, + { + "pr": "25220", + "title": "[FIX] Desktop notification on multi-instance environments", + "userLogin": "sampaiodiego", + "milestone": "4.6.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25280", + "title": "Chore: Remove package-lock.json from houston files", + "userLogin": "d-gubert", + "description": "Houston config in the `package.json` file still mentioned `package-lock.json`, but it doesn't exist anymore", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "25260", + "title": "[FIX] Adjust email label in Setup Wizard i18n files", + "userLogin": "guijun13", + "description": "- remove 'Company' label on onboarding email keys in certain languages", + "contributors": [ + "guijun13" + ] + }, + { + "pr": "25275", + "title": "Chore: Fix return type warnings", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23870", + "title": "[NEW] Expand Apps Engine's environment variable allowed list", + "userLogin": "cuonghuunguyen", + "milestone": "4.7.0", + "contributors": [ + null, + "debdutdeb", + "web-flow", + "cuonghuunguyen", + "dougfabris" + ] + }, + { + "pr": "25273", + "title": "Regression: Fix federation Matrix bridge startup", + "userLogin": "sampaiodiego", + "milestone": "4.7.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25092", + "title": "[FIX] Message preview not available for queued chats", + "userLogin": "murtaza98", + "milestone": "4.7.0", + "contributors": [ + "murtaza98", + "KevLehman" + ] + }, + { + "pr": "23688", + "title": "[NEW] Alpha Matrix Federation", + "userLogin": "alansikora", + "description": "Experimental support for Matrix Federation with a Bridge\r\n\r\nhttps://user-images.githubusercontent.com/51996/164530391-e8b17ecd-a4d0-4ef8-a8b7-81230c1773d3.mp4", + "milestone": "4.7.0", + "contributors": [ + "alansikora", + "geekgonecrazy", + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "25259", + "title": "Chore: Bump Fuselage packages", + "userLogin": "dougfabris", + "milestone": "4.7.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25261", + "title": "[FIX] Incorrect websocket url in livechat widget", + "userLogin": "debdutdeb", + "milestone": "4.7.0", + "contributors": [ + "debdutdeb" + ] + }, + { + "pr": "25007", + "title": "[FIX] Showing Blank Message Inside Report", + "userLogin": "nishant23122000", + "description": "https://user-images.githubusercontent.com/53515714/161038085-4a86c7ae-6751-4996-9767-b1c9e0331a6c.mp4", + "contributors": [ + "nishant23122000" + ] + }, + { + "pr": "25251", + "title": "Regression: Add select message to system message and thread preview and allow select on legacy template", + "userLogin": "filipemarins", + "milestone": "4.7.0", + "contributors": [ + "filipemarins", + "ggazzo", + "web-flow", + "gabriellsh", + "dougfabris" + ] + }, + { + "pr": "25239", + "title": "[FIX] Add katex render to new message react template", + "userLogin": "filipemarins", + "milestone": "4.7.0", + "contributors": [ + "filipemarins", + "ggazzo", + "dougfabris" + ] + }, + { + "pr": "25257", + "title": "Chore: Update Livechat to the last version", + "userLogin": "tiagoevanp", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "24515", + "title": "[FIX] Custom sound error toast messages", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664", + "dougfabris" + ] + }, + { + "pr": "25211", + "title": "Regression: Avatar not loading on first direct message", + "userLogin": "filipemarins", + "description": "fix avatar not loading on a first direct message", + "milestone": "4.7.0", + "contributors": [ + "filipemarins", + "ggazzo" + ] + }, + { + "pr": "25254", + "title": "Regression: Show username and real name on the message system", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "25217", + "title": "[IMPROVE] Performance for some Omnichannel features", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25200", + "title": "[FIX] room creation fails if app framework is disabled", + "userLogin": "debdutdeb", + "milestone": "4.7.0", + "contributors": [ + "debdutdeb" + ] + }, + { + "pr": "24565", + "title": "[IMPROVE] Add OTR Room States", + "userLogin": "yash-rajpal", + "description": "Earlier OTR room uses only 2 states, we need more states to support future features. \r\nThis adds more states for the OTR contextualBar.\r\n\r\n- Expired\r\n\"Screen\r\n\r\n- Declined\r\nScreen Shot 2022-04-20 at 13 49 28\r\n\r\n- Error\r\n\"Screen", + "milestone": "4.7.0", + "contributors": [ + "yash-rajpal", + "dougfabris" + ] + }, + { + "pr": "25170", + "title": "[FIX] Client disconnection on network loss", + "userLogin": "amolghode1981", + "description": "Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online\r\nunless agent explicitly logs off.\r\nAgent can stop receiving calls in multiple ways due to network loss. Network loss can happen in following ways.\r\n1. User tries to switch the network. User experiences a glitch of disconnectivity. This can be simulated by turning the network off\r\nin the network tab of chrome's dev tool. This can disconnect the UA if the disconnection happens just before the registration refresh.\r\n2. Second reason is when computer goes in sleep mode.\r\n3. Third reason is that when asterisk is crashed/in maintenance mode/explicitly stopped.\r\n\r\nSolution:\r\nThe idea is to detect the network disconnection and start the start the attempts to reconnect.\r\nThe detection of the disconnection does not happen in case#1. The SIPUA's UserAgent transport does not\r\ncall onDisconnected when network loss of such kind happens. To tackle this problem, window's online and offline event handlers are\r\nused.\r\n\r\nThe number of retries is configurable but ideally it is to be kept at -1. Whenever disconnection happens, it should keep on trying to\r\nreconnect with increasing backoff time. This behaviour is useful when the asterisk is stopped.\r\n\r\nWhen the server is disconnected, it should be indicated on the phone button.", + "contributors": [ + "amolghode1981", + "KevLehman" + ] + }, + { + "pr": "25244", + "title": "[FIX] Read receipts show with color gray when not read yet", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins", + "gabriellsh" + ] + }, + { + "pr": "25230", + "title": "[FIX] VoIP disabled/enabled sequence puts voip agent in error state", + "userLogin": "amolghode1981", + "description": "Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side)\r\n\r\nIt was realised that the race does not happen because of the database or server noticing the changes late. But because of the time taken to establish the AMI connection with Asterisk.\r\n\r\nSolution:\r\n\r\n1. Change apps/meteor/app/voip/server/startup.ts. When VoIP_Enabled is changed, await for Voip.init() to complete and then broadcast connector.statuschanged with changed value.\r\n2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance.\r\n3. in apps/meteor/client/providers/CallProvider/hooks/useVoipClient.ts add the event handler that receives this event. Change voipEnabled from constant to state. Change this state based on the 'value' that is received by the handler.", + "contributors": [ + "amolghode1981", + "KevLehman" + ] + }, + { + "pr": "25087", + "title": "[NEW] Add expire index to integration history", + "userLogin": "geekgonecrazy", + "milestone": "4.7.0", + "contributors": [ + "geekgonecrazy" + ] + }, + { + "pr": "24521", + "title": "Chore: update OTR icon", + "userLogin": "kibonusp", + "description": "I changed the shredder icon in OTR contextual bar to the stopwatch icon, recently added to the fuselage.", + "milestone": "4.7.0", + "contributors": [ + "kibonusp", + "yash-rajpal", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "25237", + "title": "[FIX] Toolbox hiding under contextual bar", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25231", + "title": "[IMPROVE] Added MaxNickNameLength and MaxBioLength constants", + "userLogin": "aakash-gitdev", + "contributors": [ + "aakash-gitdev", + "web-flow", + "gabriellsh" + ] + }, + { + "pr": "25220", + "title": "[FIX] Desktop notification on multi-instance environments", + "userLogin": "sampaiodiego", + "milestone": "4.6.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25175", + "title": "[FIX] Reply button behavior on broadcast channel", + "userLogin": "filipemarins", + "description": "Hide reply button for the user that sent the message", + "contributors": [ + "filipemarins", + "web-flow" + ] + }, + { + "pr": "25216", + "title": "[FIX] Read receipts showing before message read", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "25222", + "title": "[FIX] Add reaction not working in legacy messages", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25223", + "title": "Chore: Add error boundary to message component", + "userLogin": "gabriellsh", + "description": "Not crash the whole application if something goes wrong in the MessageList component.\r\n\r\n![image](https://user-images.githubusercontent.com/40830821/162269915-931c5c3c-c979-4234-b74c-371f67467ce0.png)", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25130", + "title": "Chore: Update Livechat version", + "userLogin": "tiagoevanp", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "25073", + "title": "[FIX] AgentOverview analytics wrong departmentId parameter", + "userLogin": "paulobernardoaf", + "description": "When filtering the analytics charts by department, data would not appear because the object:\r\n```js\r\n{\r\n value: \"department-id\",\r\n label: \"department-name\"\r\n}\r\n```\r\nwas being used in the `departmentId` parameter.\r\n\r\n- Before:\r\n![image](https://user-images.githubusercontent.com/30026625/161832057-d96ffd21-a7dd-421e-bfaa-3b9f4a9127b2.png)\r\n\r\n- After:\r\n![image](https://user-images.githubusercontent.com/30026625/161831092-9ee77b51-b083-4f45-9c48-ab2e0511c4d6.png)", + "milestone": "4.7.0", + "contributors": [ + "paulobernardoaf" + ] + }, + { + "pr": "25056", + "title": "[FIX] Close room when dismiss wrap up call modal", + "userLogin": "tiagoevanp", + "milestone": "4.7.0", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "25208", + "title": "Regression: yarn dev triggers build dependencies", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "24714", + "title": "[FIX] Added invalid password error message", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664", + "dougfabris" + ] + }, + { + "pr": "25196", + "title": "Chore: Tests with Playwright (task: ROC-28, 09-channels)", + "userLogin": "tmontini", + "contributors": [ + "tmontini" + ] + }, + { + "pr": "25174", + "title": "Chore: Template to generate packages", + "userLogin": "ggazzo", + "description": "```\r\nnpx hygen package new test\r\n```", + "contributors": [ + "ggazzo", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "25193", + "title": "Regression: Fix micro services Docker build", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25191", + "title": "Release 4.6.2", + "userLogin": "sampaiodiego", + "contributors": [ + "sidmohanty11", + "sampaiodiego" + ] + }, + { + "pr": "25101", + "title": "[FIX] Database indexes not being created", + "userLogin": "sampaiodiego", + "milestone": "4.6.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24933", + "title": "[FIX] Deactivating user breaks if user is the only room owner", + "userLogin": "sidmohanty11", + "description": "## Before\r\n\r\nhttps://user-images.githubusercontent.com/73601258/160000871-cfc2f2a5-2a59-4d27-8049-7754d003dd48.mp4\r\n\r\n\r\n\r\n## After\r\nhttps://user-images.githubusercontent.com/73601258/159998287-681ab475-ff33-4282-82ff-db751c59a392.mp4", + "milestone": "4.6.2", + "contributors": [ + "sidmohanty11", + "sampaiodiego" + ] + }, + { + "pr": "25095", + "title": "Release 4.6.1", + "userLogin": "sampaiodiego", + "contributors": [ + "dougfabris", + "sampaiodiego", + "gabriellsh", + "yash-rajpal", + "pierre-lehnen-rc" + ] + }, + { + "pr": "25022", + "title": "[FIX] Proxy settings being ignored", + "userLogin": "pierre-lehnen-rc", + "description": "Modify Meteor's `HTTP.call` to add back proxy support", + "milestone": "4.6.1", + "contributors": [ + "pierre-lehnen-rc", + "sampaiodiego" + ] + }, + { + "pr": "25082", + "title": "[FIX] Invitation links don't redirect to the registration form", + "userLogin": "yash-rajpal", + "milestone": "4.6.1", + "contributors": [ + "yash-rajpal" + ] + }, + { + "pr": "25069", + "title": "[FIX] FormData uploads not working", + "userLogin": "gabriellsh", + "milestone": "4.6.1", + "contributors": [ + "gabriellsh", + "dougfabris" + ] + }, + { + "pr": "25067", + "title": "[FIX] NPS never finishing sending results", + "userLogin": "sampaiodiego", + "milestone": "4.6.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25050", + "title": "[FIX] Upgrade Tab showing for a split second", + "userLogin": "gabriellsh", + "milestone": "4.6.1", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25055", + "title": "[FIX] UserAutoComplete not rendering UserAvatar correctly", + "userLogin": "dougfabris", + "description": "### before\r\n![Screen Shot 2022-04-04 at 16 50 21](https://user-images.githubusercontent.com/27704687/161620921-800bf66a-806d-4f83-b2e1-073c34215001.png)\r\n\r\n### after\r\n![Screen Shot 2022-04-04 at 16 49 00](https://user-images.githubusercontent.com/27704687/161620720-3e27774d-c241-46ca-b764-932a9295d709.png)", + "milestone": "4.6.1", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25180", + "title": "Chore: Remove duplicated useUserRoom", + "userLogin": "dougfabris", + "milestone": "4.7.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25167", + "title": "Chore: TS migration SortList", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "24933", + "title": "[FIX] Deactivating user breaks if user is the only room owner", + "userLogin": "sidmohanty11", + "description": "## Before\r\n\r\nhttps://user-images.githubusercontent.com/73601258/160000871-cfc2f2a5-2a59-4d27-8049-7754d003dd48.mp4\r\n\r\n\r\n\r\n## After\r\nhttps://user-images.githubusercontent.com/73601258/159998287-681ab475-ff33-4282-82ff-db751c59a392.mp4", + "milestone": "4.6.2", + "contributors": [ + "sidmohanty11", + "sampaiodiego" + ] + }, + { + "pr": "25181", + "title": "Regression: Fix services Docker build on CI", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25089", + "title": "[FIX] UserCard sanitization", + "userLogin": "dougfabris", + "description": "- Rewrites the component to TS\r\n- Fixes some visual issues\r\n\r\n### before\r\n![Screen Shot 2022-04-07 at 00 23 11](https://user-images.githubusercontent.com/27704687/162113925-5c9484d1-23e9-4623-8b86-3fbc71b461a1.png)\r\n\r\n### after\r\n![Screen Shot 2022-04-07 at 00 07 13](https://user-images.githubusercontent.com/27704687/162112353-afd6aac6-b27c-4470-a642-631b8080d59e.png)", + "milestone": "4.7.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25085", + "title": "Chore: move definitions to packages", + "userLogin": "ggazzo", + "milestone": "3.7.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25168", + "title": "Regression: CI playwright", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25125", + "title": "Chore: Convert NotificationStatus to TS", + "userLogin": "jeanfbrito", + "contributors": [ + "jeanfbrito", + "ggazzo" + ] + }, + { + "pr": "25148", + "title": "[FIX] Message menu action not working on legacy messages.", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25122", + "title": "Chore: Tests with Playwright (task: All works)", + "userLogin": "weslley543", + "contributors": [ + "weslley543" + ] + }, + { + "pr": "25129", + "title": "Chore: Remove old files from removed Omnichannel feature", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25128", + "title": "Chore: Convert admin custom sound to tsx", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25126", + "title": "Chore: Migrate oauth2server to typescript", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25123", + "title": "Chore: Convert LivechatAgentActivity to raw model and TS", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25124", + "title": "Chore: Remove unused Drone CI files", + "userLogin": "geekgonecrazy", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "25121", + "title": "Chore: Convert Mailer to TS", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti", + "sampaiodiego" + ] + }, + { + "pr": "25107", + "title": "Regression: Fix CI monorepo build", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25074", + "title": "Chore: Monorepo ", + "userLogin": "ggazzo", + "milestone": "3.7.0", + "contributors": [ + "sampaiodiego", + "ggazzo" + ] + }, + { + "pr": "25097", + "title": "[IMPROVE] Rename upgrade tab routes", + "userLogin": "guijun13", + "description": "Change 'upgrade tab' routes names from camelCase ('goFullyFeatured') to kebab-case ('go-fully-featured') due to URL naming consistency. Changed types, main function and test.", + "contributors": [ + "guijun13" + ] + }, + { + "pr": "25076", + "title": "Bump eslint-plugin-anti-trojan-source from 1.0.6 to 1.1.0", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24936", + "title": "[FIX] End call button disappearing when on-hold", + "userLogin": "tiagoevanp", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "24932", + "title": "[FIX] Use correct room property for call ended at", + "userLogin": "MartinSchoeler", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "25022", + "title": "[FIX] Proxy settings being ignored", + "userLogin": "pierre-lehnen-rc", + "description": "Modify Meteor's `HTTP.call` to add back proxy support", + "milestone": "4.6.1", + "contributors": [ + "pierre-lehnen-rc", + "sampaiodiego" + ] + }, + { + "pr": "25082", + "title": "[FIX] Invitation links don't redirect to the registration form", + "userLogin": "yash-rajpal", + "milestone": "4.6.1", + "contributors": [ + "yash-rajpal" + ] + }, + { + "pr": "23971", + "title": "[NEW] Message Template React Component", + "userLogin": "ggazzo", + "description": "Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template).\r\n\r\n\r\n\r\n![Screen Shot 2022-04-05 at 11 14 18](https://user-images.githubusercontent.com/27704687/161774027-38dd9c7b-eeeb-45e2-b9d8-ea2a9be8486d.png)\r\nIn case you encounter any problems, or want to compare, temporarily it is possible to use the old version\r\n\r\n\"image\"", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "25069", + "title": "[FIX] FormData uploads not working", + "userLogin": "gabriellsh", + "milestone": "4.6.1", + "contributors": [ + "gabriellsh", + "dougfabris" + ] + }, + { + "pr": "19866", + "title": "[FIX] Video and Audio not skipping forward", + "userLogin": "MartinSchoeler", + "contributors": [ + "MartinSchoeler", + "tassoevan", + "web-flow", + "dougfabris" + ] + }, + { + "pr": "25067", + "title": "[FIX] NPS never finishing sending results", + "userLogin": "sampaiodiego", + "milestone": "4.6.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24405", + "title": "[IMPROVE] Add tooltip to sidebar room menu", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664", + "web-flow", + "dougfabris" + ] + }, + { + "pr": "24431", + "title": "[IMPROVE] Added tooltip options for message menu", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664", + "dougfabris" + ] + }, + { + "pr": "24166", + "title": "[FIX] Replace encrypted text to Encrypted Message Placeholder", + "userLogin": "yash-rajpal", + "description": "### before \r\n![image](https://user-images.githubusercontent.com/27704687/150807900-154a9cdb-ee13-4333-8628-f287ab914b40.png)\r\n\r\n### after\r\n\"Screenshot", + "milestone": "4.7.0", + "contributors": [ + "yash-rajpal", + "albuquerquefabio", + "web-flow" + ] + }, + { + "pr": "24984", + "title": "[FIX] Prevent sequential messages edited icon to hide on hover", + "userLogin": "dougfabris", + "description": "### before\r\n\"Screen\r\n\r\n### after\r\n\"Screen", + "milestone": "4.7.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25024", + "title": "[IMPROVE] Improve active/hover colors in account sidebar", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664" + ] + }, + { + "pr": "24856", + "title": "[FIX] Full error message is visible", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664", + "tassoevan" + ] + }, + { + "pr": "24708", + "title": "Chore: Cancel running jobs if PR is updated", + "userLogin": "debdutdeb", + "contributors": [ + "debdutdeb", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24900", + "title": "Chore: organize test files and fix code coverage", + "userLogin": "tmontini", + "contributors": [ + null, + "tmontini", + "rodrigok" + ] + }, + { + "pr": "24464", + "title": "Chore: Missing keys in APIsDisplay", + "userLogin": "dougfabris", + "milestone": "4.7.0", + "contributors": [ + "dougfabris", + "tassoevan" + ] + }, + { + "pr": "25057", + "title": "Bump ejson from 2.2.1 to 2.2.2", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25053", + "title": "Chore: Remove Alpine image deps after using them", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25052", + "title": "Bump pino and pino-pretty", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25050", + "title": "[FIX] Upgrade Tab showing for a split second", + "userLogin": "gabriellsh", + "milestone": "4.6.1", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25055", + "title": "[FIX] UserAutoComplete not rendering UserAvatar correctly", + "userLogin": "dougfabris", + "description": "### before\r\n![Screen Shot 2022-04-04 at 16 50 21](https://user-images.githubusercontent.com/27704687/161620921-800bf66a-806d-4f83-b2e1-073c34215001.png)\r\n\r\n### after\r\n![Screen Shot 2022-04-04 at 16 49 00](https://user-images.githubusercontent.com/27704687/161620720-3e27774d-c241-46ca-b764-932a9295d709.png)", + "milestone": "4.6.1", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25031", + "title": "Chore: TS conversion folder client", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "24991", + "title": "Bump minimist from 1.2.5 to 1.2.6 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25002", + "title": "Bump template-file from 6.0.0 to 6.0.1", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25042", + "title": "Bump body-parser from 1.19.2 to 1.20.0 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25043", + "title": "i18n: Language update from LingoHub 🤖 on 2022-04-04Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null + ] + }, + { + "pr": "25028", + "title": "Merge master into develop & Set version to 4.7.0-develop", + "userLogin": "sampaiodiego", + "contributors": [ + "AllanPazRibeiro", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "25027", + "title": "Release 4.6.0", + "userLogin": "sampaiodiego", + "contributors": [ + "pierre-lehnen-rc", + "aswinidev", + "web-flow", + "renatobecker", + "sampaiodiego", + "dependabot[bot]", + "lingohub[bot]", + "matheusbsilva137", + "amolghode1981", + "debdutdeb", + "eduardofcabrera", + "juliajforesti", + "tiagoevanp", + "KevLehman" + ] + }, + { + "pr": "25021", + "title": "Bump @rocket.chat/emitter from 0.31.4 to 0.31.9 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25020", + "title": "Bump @rocket.chat/ui-kit from 0.31.4 to 0.31.9 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25019", + "title": "Bump @rocket.chat/message-parser from 0.31.4 to 0.31.9 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25018", + "title": "Bump @rocket.chat/string-helpers from 0.31.4 to 0.31.9 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25017", + "title": "Regression: Add createdOTR index", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24998", + "title": "Release 4.5.5", + "userLogin": "sampaiodiego", + "contributors": [ + "MartinSchoeler", + "sampaiodiego", + "filipemarins", + "tiagoevanp" + ] + }, + { + "pr": "24994", + "title": "[FIX] High CPU usage caused by CallProvider", + "userLogin": "tiagoevanp", + "description": "Remove infinity loop inside useVoipClient hook.\r\n\r\n#closes #24970", + "milestone": "4.5.5", + "contributors": [ + "ggazzo", + "tiagoevanp" + ] + }, + { + "pr": "24955", + "title": "[FIX] Multiple issues starting a new DM", + "userLogin": "filipemarins", + "description": "When the room object is searched for the first time, it does not exist on the front object yet (subscription), adding a fallback search for room list will guarantee to search the room details.\r\n\r\nbefore:\r\nhttps://user-images.githubusercontent.com/9275105/160223241-d2319f3e-82c5-47d6-867f-695ab2361a17.mp4\r\n\r\nafter:\r\nhttps://user-images.githubusercontent.com/9275105/160223244-84d0d2a1-3d95-464d-8b8a-e264b0d4d690.mp4", + "milestone": "4.5.5", + "contributors": [ + "filipemarins", + "pierre-lehnen-rc" + ] + }, + { + "pr": "24990", + "title": "Chore: Update Livechat", + "userLogin": "MartinSchoeler", + "milestone": "4.5.5", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24938", + "title": "Release 4.5.4", + "userLogin": "AllanPazRibeiro", + "contributors": [ + "geekgonecrazy", + "AllanPazRibeiro" + ] + }, + { + "pr": "24930", + "title": "[FIX] SAML Force name to string", + "userLogin": "geekgonecrazy", + "milestone": "4.5.4", + "contributors": [ + "geekgonecrazy", + "web-flow", + "pierre-lehnen-rc" + ] + }, + { + "pr": "25015", + "title": "Chore: Bump Fuselage packages", + "userLogin": "dougfabris", + "description": "It uses the last stable version of Fuselage packages.", + "milestone": "4.6.0", + "contributors": [ + "dougfabris", + "tassoevan" + ] + }, + { + "pr": "24999", + "title": "Regression: Custom roles displaying ID instead of name on some admin screens", + "userLogin": "pierre-lehnen-rc", + "description": "![image](https://user-images.githubusercontent.com/55164754/160981416-555bcaa1-c075-4260-937c-64523472da43.png)\r\n![image](https://user-images.githubusercontent.com/55164754/160981452-6eae4e74-8425-4073-8256-472aba72b9db.png)", + "milestone": "4.6.0", + "contributors": [ + "pierre-lehnen-rc", + "dougfabris", + "web-flow" + ] + }, + { + "pr": "24835", + "title": "[NEW] Upgrade Tab", + "userLogin": "gabriellsh", + "description": "![image](https://user-images.githubusercontent.com/27704687/160172260-c656282e-a487-4092-948d-d11c9bacb598.png)", + "milestone": "4.6.0", + "contributors": [ + "gabriellsh", + "dougfabris", + "web-flow", + "tassoevan", + "pierre-lehnen-rc" + ] + }, + { + "pr": "24980", + "title": "Regression: Error is raised when there's no Asterisk queue available yet", + "userLogin": "amolghode1981", + "milestone": "4.6.0", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "24994", + "title": "[FIX] High CPU usage caused by CallProvider", + "userLogin": "tiagoevanp", + "description": "Remove infinity loop inside useVoipClient hook.\r\n\r\n#closes #24970", + "milestone": "4.5.5", + "contributors": [ + "ggazzo", + "tiagoevanp" + ] + }, + { + "pr": "24955", + "title": "[FIX] Multiple issues starting a new DM", + "userLogin": "filipemarins", + "description": "When the room object is searched for the first time, it does not exist on the front object yet (subscription), adding a fallback search for room list will guarantee to search the room details.\r\n\r\nbefore:\r\nhttps://user-images.githubusercontent.com/9275105/160223241-d2319f3e-82c5-47d6-867f-695ab2361a17.mp4\r\n\r\nafter:\r\nhttps://user-images.githubusercontent.com/9275105/160223244-84d0d2a1-3d95-464d-8b8a-e264b0d4d690.mp4", + "milestone": "4.5.5", + "contributors": [ + "filipemarins", + "pierre-lehnen-rc" + ] + }, + { + "pr": "24969", + "title": "Chore: Storybook mocking and examples improved", + "userLogin": "tassoevan", + "description": "- Stories from `ee/` included;\r\n- Differentiate root story kinds;\r\n- Mocking of `ServerContext` via Storybook parameters.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "24990", + "title": "Chore: Update Livechat", + "userLogin": "MartinSchoeler", + "milestone": "4.5.5", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24989", + "title": "Revert: [NEW] Engagement Statistics", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24897", + "title": "[FIX] Room archived/unarchived system messages aren't sent when editing room settings", + "userLogin": "matheusbsilva137", + "description": "- Send the \"Room archived\" and \"Room unarchived\" system messages when editing room settings (and not only when rooms are archived/unarchived with the slash-command);\r\n- Fix the \"Hide System Messages\" option for the \"Room archived\" and \"Room unarchived\" system messages;", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "24925", + "title": "Chore: add some missing REST definitions", + "userLogin": "gerzonc", + "description": "On the [mobile client](https://github.com/RocketChat/Rocket.Chat.ReactNative), we made an effort to collect more `REST API` definitions that are missing on the server side during our migration to TypeScript. Since we're both migrating to TypeScript, we thought it would be a good idea to share those so you guys can benefit from our initiative.", + "contributors": [ + "gerzonc" + ] + }, + { + "pr": "24971", + "title": "i18n: Language update from LingoHub 🤖 on 2022-03-28Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null + ] + }, + { + "pr": "24921", + "title": "[FIX] Register with Secret URL", + "userLogin": "yash-rajpal", + "contributors": [ + "yash-rajpal" + ] + }, + { + "pr": "24948", + "title": "Regression: Fix unexpected errors breaking ddp-streamer", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24320", + "title": "[FIX] LDAP avatars being rotated according to metadata even if the setting to rotate uploads is off", + "userLogin": "matheusbsilva137", + "description": "- Use the `FileUpload_RotateImages` setting (**Administration > File Upload > Rotate images on upload**) to control whether avatars should be rotated automatically based on their data (XEIF);\r\n- Display the avatar image preview (orientation) according to the `FileUpload_RotateImages` setting.", + "milestone": "4.6.0", + "contributors": [ + "matheusbsilva137", + "web-flow", + "pierre-lehnen-rc" + ] + }, + { + "pr": "24930", + "title": "[FIX] SAML Force name to string", + "userLogin": "geekgonecrazy", + "milestone": "4.5.4", + "contributors": [ + "geekgonecrazy", + "web-flow", + "pierre-lehnen-rc" + ] + }, + { + "pr": "24908", + "title": "Regression: Call doesn't stop ringing after agent unregistration", + "userLogin": "MartinSchoeler", + "milestone": "4.6.0", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24777", + "title": "[NEW] Engagement Statistics", + "userLogin": "eduardofcabrera", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow" + ] + }, + { + "pr": "24920", + "title": "Regression: Fix account service login expiration", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24867", + "title": "[FIX] Duplicated \"jump to message\" button on starred messages", + "userLogin": "Himanshu664", + "contributors": [ + "Himanshu664" + ] + }, + { + "pr": "24860", + "title": "[FIX] External search providers not working", + "userLogin": "tkurz", + "contributors": [ + "tkurz" + ] + }, + { + "pr": "24052", + "title": "[FIX] Several issues related to custom roles", + "userLogin": "pierre-lehnen-rc", + "description": "- Throw an error when trying to delete a role (User or Subscription role) that are still being used;\r\n- Fix \"Invalid Role\" error for custom roles in Role Editing sidebar;\r\n- Fix \"Users in Role\" screen for custom roles.", + "milestone": "4.6.0", + "contributors": [ + "pierre-lehnen-rc", + "matheusbsilva137", + "web-flow" + ] + }, + { + "pr": "24781", + "title": "[NEW] Telemetry Events", + "userLogin": "eduardofcabrera", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow" + ] + }, + { + "pr": "24887", + "title": "[IMPROVE] Adding new statistics related to voip and omnichannel", + "userLogin": "cauefcr", + "description": "- Total of Canned response messages sent\r\n- Total of tags used\r\n- Last-Chatted Agent Preferred (enabled/disabled)\r\n- Assign new conversations to the contact manager (enabled/disabled)\r\n- How to handle Visitor Abandonment setting\r\n- Amount of chats placed on hold\r\n- VoIP Enabled\r\n- Amount of VoIP Calls\r\n- Amount of VoIP Extensions connected\r\n- Amount of Calls placed on hold (1x per call)\r\n- Fixed Session Aggregation type definitions", + "milestone": "4.6.0", + "contributors": [ + "cauefcr", + "KevLehman" + ] + }, + { + "pr": "24911", + "title": "Chore: Remove old scripts", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24898", + "title": "[FIX] DDP Rate Limiter Translation key", + "userLogin": "gabriellsh", + "description": "Before:\r\n\"image\"\r\n\r\n\r\nNow:\r\n\"image\"", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "24831", + "title": "[FIX][ENTERPRISE] Notifications not being sent by ddp-streamer", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24884", + "title": "Release 4.5.3", + "userLogin": "AllanPazRibeiro", + "contributors": [ + "tiagoevanp", + "sampaiodiego", + "KevLehman", + "amolghode1981", + "ggazzo" + ] + }, + { + "pr": "24901", + "title": "[FIX] Custom script not being fired", + "userLogin": "ggazzo", + "milestone": "4.5.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "24877", + "title": "Chore: Fix MongoDB versions on release notes", + "userLogin": "sampaiodiego", + "milestone": "4.5.3", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24864", + "title": "[FIX] Disable voip button when call is in progress", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24863", + "title": "[FIX] Broken build caused by PRs modifying same file differently", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman", + "tiagoevanp" + ] + }, + { + "pr": "24838", + "title": "[FIX] [VOIP] SidebarFooter component ", + "userLogin": "tiagoevanp", + "description": "- Improve the CallProvider code;\r\n- Adjust the text case of the VoIP component on the FooterSidebar;\r\n- Fix the bad behavior with the changes in queue's name.", + "milestone": "4.5.3", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "24837", + "title": "[IMPROVE] Standarize queue behavior for managers and agents when subscribing", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24829", + "title": "[FIX] Show only enabled departments on forward", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24799", + "title": "[FIX] Wrong param usage on queue summary call", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24789", + "title": "[FIX] VoIP button gets disabled whenever user status changes", + "userLogin": "amolghode1981", + "milestone": "4.5.3", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "24752", + "title": "[FIX] Show call icon only when user has extension associated", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24748", + "title": "[IMPROVE] UX - VoIP Call Component", + "userLogin": "tiagoevanp", + "milestone": "4.5.3", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "24814", + "title": "Release 4.5.2", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "MartinSchoeler", + "pierre-lehnen-rc", + "tassoevan", + "debdutdeb", + "KevLehman", + "murtaza98", + "sampaiodiego", + "juliajforesti" + ] + }, + { + "pr": "24812", + "title": "[FIX] Revert AutoComplete", + "userLogin": "juliajforesti", + "milestone": "4.5.2", + "contributors": [ + "juliajforesti", + "ggazzo" + ] + }, + { + "pr": "24809", + "title": "Regression: Fix ParentRoomWithEndpointData in loop", + "userLogin": "sampaiodiego", + "milestone": "4.5.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24732", + "title": "[FIX] `PaginatedSelectFiltered` not handling changes", + "userLogin": "tassoevan", + "milestone": "4.5.2", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "24805", + "title": "[FIX] Critical: Incorrect visitor getting assigned to a chat from apps", + "userLogin": "murtaza98", + "milestone": "4.5.2", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24804", + "title": "[FIX] \"livechat/webrtc.call\" endpoint not working", + "userLogin": "murtaza98", + "milestone": "4.5.2", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24792", + "title": "[FIX] VoipExtensionsPage component call", + "userLogin": "KevLehman", + "milestone": "4.5.2", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24705", + "title": "[FIX] Broken multiple OAuth integrations", + "userLogin": "debdutdeb", + "milestone": "4.5.2", + "contributors": [ + "debdutdeb" + ] + }, + { + "pr": "24623", + "title": "[FIX] Opening a new DM from user card", + "userLogin": "tassoevan", + "description": "A race condition on `useRoomIcon` -- delayed merge of rooms and subscriptions -- was causing a UI crash whenever someone tried to open a DM from the user card component.", + "milestone": "4.5.2", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "24750", + "title": "[IMPROVE] Voip Extensions disabled state", + "userLogin": "MartinSchoeler", + "milestone": "4.5.2", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24782", + "title": "Release 4.5.1", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "renatobecker", + "pierre-lehnen-rc", + "sampaiodiego", + "matheusbsilva137", + "amolghode1981", + "juliajforesti", + "tiagoevanp", + "KevLehman", + "MartinSchoeler", + "Aman-Maheshwari", + "cuonghuunguyen" + ] + }, + { + "pr": "24760", + "title": "[FIX] Apple login script being loaded even when Apple Login is disabled.", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.5.1", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "24754", + "title": "Chore: Update Livechat", + "userLogin": "MartinSchoeler", + "milestone": "4.5.1", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24683", + "title": "[FIX] no id of room closer in livechat-close message", + "userLogin": "cuonghuunguyen", + "milestone": "4.5.1", + "contributors": [ + null + ] + }, + { + "pr": "23795", + "title": "[FIX] Reload roomslist after successful deletion of a room from admin panel.", + "userLogin": "Aman-Maheshwari", + "description": "Removed the logic for calling the `rooms.adminRooms` endPoint from the `RoomsTable` Component and moved it to its parent component `RoomsPage`.\r\nThis allows to call the endPoint `rooms.adminRooms` from `EditRoomContextBar` Component which is also has `RoomPage` Component as its parent.\r\n\r\nAlso added a succes toast message after the successful deletion of room.", + "milestone": "4.5.1", + "contributors": [ + "Aman-Maheshwari", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "24743", + "title": "[FIX] System messages are sent when adding or removing a group from a team", + "userLogin": "matheusbsilva137", + "description": "- Do not send system messages when adding or removing a new or existing _group_ from a team.", + "milestone": "4.5.1", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "24737", + "title": "[FIX] Typo and placeholder on wrap up call modal", + "userLogin": "MartinSchoeler", + "milestone": "4.5.1", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24680", + "title": "[FIX] Show only available agents on extension association modal", + "userLogin": "KevLehman", + "milestone": "4.5.1", + "contributors": [ + "KevLehman", + "tiagoevanp" + ] + }, + { + "pr": "24607", + "title": "[FIX] VoIP Enable/Disable setting on CallContext/CallProvider Notifications", + "userLogin": "tiagoevanp", + "milestone": "4.5.1", + "contributors": [ + "tiagoevanp", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "24677", + "title": "[FIX] Components for user search", + "userLogin": "juliajforesti", + "milestone": "4.5.1", + "contributors": [ + "juliajforesti", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "24657", + "title": "[FIX] Voip Stream Reinitialization Error", + "userLogin": "amolghode1981", + "milestone": "4.5.1", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "24696", + "title": "[FIX] Room's message count not being incremented on import", + "userLogin": "matheusbsilva137", + "description": "- Fix rooms' message counter not being incremented on message import.", + "milestone": "4.5.1", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "24674", + "title": "[FIX] Missing username on messages imported from Slack", + "userLogin": "matheusbsilva137", + "description": "- Fix missing sender's username on messages imported from Slack.", + "milestone": "4.5.1", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "24590", + "title": "[FIX] Duplicated 'name' log key", + "userLogin": "sampaiodiego", + "milestone": "4.5.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24661", + "title": "[FIX] Typo in wrap-up term", + "userLogin": "renatobecker", + "milestone": "4.5.1", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "24606", + "title": "[FIX] Push privacy config to not show username not being respected", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24901", + "title": "[FIX] Custom script not being fired", + "userLogin": "ggazzo", + "milestone": "4.5.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "24896", + "title": "[FIX] Wrong business hour behavior", + "userLogin": "murtaza98", + "milestone": "4.6.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24845", + "title": "[FIX] Ignore customClass on messages", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24879", + "title": "[FIX] Apple OAuth", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24895", + "title": "i18n: Language update from LingoHub 🤖 on 2022-03-21Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null + ] + }, + { + "pr": "24749", + "title": "[IMPROVE] New omnichannel statistics and async statistics processing.", + "userLogin": "cauefcr", + "description": "https://app.clickup.com/t/1z4zg4e", + "contributors": [ + "cauefcr" + ] + }, + { + "pr": "24882", + "title": "[FIX] Missing dependency on useEffect at CallProvider", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24877", + "title": "Chore: Fix MongoDB versions on release notes", + "userLogin": "sampaiodiego", + "milestone": "4.5.3", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24779", + "title": "[FIX] auto-join team channels not honoring user preferences", + "userLogin": "ostjen", + "contributors": [ + "ostjen" + ] + }, + { + "pr": "24869", + "title": "Bump pino from 7.8.1 to 7.9.1 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24870", + "title": "Bump pino-pretty from 7.5.3 to 7.5.4 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24864", + "title": "[FIX] Disable voip button when call is in progress", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24863", + "title": "[FIX] Broken build caused by PRs modifying same file differently", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman", + "tiagoevanp" + ] + }, + { + "pr": "24850", + "title": "Regression: Role Sync not always working", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "24838", + "title": "[FIX] [VOIP] SidebarFooter component ", + "userLogin": "tiagoevanp", + "description": "- Improve the CallProvider code;\r\n- Adjust the text case of the VoIP component on the FooterSidebar;\r\n- Fix the bad behavior with the changes in queue's name.", + "milestone": "4.5.3", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "24837", + "title": "[IMPROVE] Standarize queue behavior for managers and agents when subscribing", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24789", + "title": "[FIX] VoIP button gets disabled whenever user status changes", + "userLogin": "amolghode1981", + "milestone": "4.5.3", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "24799", + "title": "[FIX] Wrong param usage on queue summary call", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24829", + "title": "[FIX] Show only enabled departments on forward", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24823", + "title": "i18n: Language update from LingoHub 🤖 on 2022-03-14Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24833", + "title": "Bump @types/mailparser from 3.0.2 to 3.4.0", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24832", + "title": "Bump @types/clipboard from 2.0.1 to 2.0.7", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24822", + "title": "Bump @types/nodemailer from 6.4.2 to 6.4.4", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24821", + "title": "Bump body-parser from 1.19.0 to 1.19.2", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24820", + "title": "Bump @types/ws from 8.5.2 to 8.5.3 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24764", + "title": "Chore: Add E2E tests for livechat/visitor", + "userLogin": "Muramatsu2602", + "description": "- Create a new test suite file under tests/end-to-end/api/livechat\r\n- Create tests for the following endpoints:\r\n + livechat/visitor (create visitor, update visitor, add custom fields to visitors)", + "contributors": [ + "Muramatsu2602", + "KevLehman" + ] + }, + { + "pr": "24729", + "title": "Chore: Add E2E tests for livechat/room.close", + "userLogin": "Muramatsu2602", + "description": "* Create a new test suite file under tests/end-to-end/api/livechat\r\n * Create tests for the following endpoint:\r\n\t + ivechat/room.close", + "contributors": [ + "Muramatsu2602", + "web-flow", + "KevLehman" + ] + }, + { + "pr": "24785", + "title": "[FIX] German translation for Monitore", + "userLogin": "JMoVS", + "contributors": [ + "JMoVS", + "web-flow" + ] + }, + { + "pr": "24812", + "title": "[FIX] Revert AutoComplete", + "userLogin": "juliajforesti", + "milestone": "4.5.2", + "contributors": [ + "juliajforesti", + "ggazzo" + ] + }, + { + "pr": "24747", + "title": "Chore: APIClass types", + "userLogin": "felipe-rod123", + "description": "This pull request creates a new `restivus` module (.d.ts) for the `api.js` file.", + "contributors": [ + "felipe-rod123", + "ggazzo" + ] + }, + { + "pr": "24801", + "title": "Bump is-svg from 4.3.1 to 4.3.2", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24803", + "title": "Bump prometheus-gc-stats from 0.6.2 to 0.6.3", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24810", + "title": "Chore: Skip local services changes when shutting down duplicated services", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24629", + "title": "[FIX] \"Match error\" when converting a team to a channel", + "userLogin": "matheusbsilva137", + "description": "- Fix \"Match error\" when trying to convert a channel to a team;", + "milestone": "4.6.0", + "contributors": [ + "matheusbsilva137", + "web-flow" + ] + }, + { + "pr": "24809", + "title": "Regression: Fix ParentRoomWithEndpointData in loop", + "userLogin": "sampaiodiego", + "milestone": "4.5.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24397", + "title": "Chore: Get Settings Statistics", + "userLogin": "albuquerquefabio", + "contributors": [ + "albuquerquefabio" + ] + }, + { + "pr": "24732", + "title": "[FIX] `PaginatedSelectFiltered` not handling changes", + "userLogin": "tassoevan", + "milestone": "4.5.2", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "24628", + "title": "Chore: converted more hooks to typescript", + "userLogin": "felipe-rod123", + "description": "Converted some functions on `client/hooks/` from JavaScript to Typescript.", + "contributors": [ + "felipe-rod123", + "ggazzo" + ] + }, + { + "pr": "24506", + "title": "Chore: added settings endpoint types", + "userLogin": "felipe-rod123", + "description": "Created typing for endpoint definitions on `settings.ts`.", + "contributors": [ + "felipe-rod123", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "24226", + "title": "[FIX] Handle Other Formats inside Upload Avatar", + "userLogin": "nishant23122000", + "description": "After resolving issue #24213 : \r\n\r\n\r\nhttps://user-images.githubusercontent.com/53515714/150325012-91413025-786e-4ce0-ae75-629f6b05b024.mp4", + "milestone": "4.6.0", + "contributors": [ + "nishant23122000", + "debdutdeb", + "web-flow", + "murtaza98" + ] + }, + { + "pr": "24424", + "title": "[FIX] Prune Message issue", + "userLogin": "nishant23122000", + "milestone": "4.6.0", + "contributors": [ + "nishant23122000", + "debdutdeb", + "web-flow" + ] + }, + { + "pr": "24805", + "title": "[FIX] Critical: Incorrect visitor getting assigned to a chat from apps", + "userLogin": "murtaza98", + "milestone": "4.5.2", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24804", + "title": "[FIX] \"livechat/webrtc.call\" endpoint not working", + "userLogin": "murtaza98", + "milestone": "4.5.2", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24507", + "title": "Chore: added Server Instances endpoint types", + "userLogin": "felipe-rod123", + "description": "Created typing for endpoint definitions on `instances.ts`.", + "contributors": [ + "felipe-rod123" + ] + }, + { + "pr": "24758", + "title": "[FIX] Prevent call button toggle when user is on call", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24800", + "title": "Regression: Register services right away", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24792", + "title": "[FIX] VoipExtensionsPage component call", + "userLogin": "KevLehman", + "milestone": "4.5.2", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24384", + "title": "Chore: Convert server functions from javascript to typescript", + "userLogin": "felipe-rod123", + "description": "This pull request will be used to rewrite some functions on the Chat Engine to Typescript, in order to increase security and specify variable types on the code.", + "contributors": [ + "felipe-rod123", + "ggazzo" + ] + }, + { + "pr": "24705", + "title": "[FIX] Broken multiple OAuth integrations", + "userLogin": "debdutdeb", + "milestone": "4.5.2", + "contributors": [ + "debdutdeb" + ] + }, + { + "pr": "24793", + "title": "[FIX][ENTERPRISE] Auto reload feature of ddp-streamer micro service", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24783", + "title": "Bump pino from 7.8.0 to 7.8.1 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23121", + "title": "Bump jschardet from 1.6.0 to 3.0.0", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24752", + "title": "[FIX] Show call icon only when user has extension associated", + "userLogin": "KevLehman", + "milestone": "4.5.3", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24623", + "title": "[FIX] Opening a new DM from user card", + "userLogin": "tassoevan", + "description": "A race condition on `useRoomIcon` -- delayed merge of rooms and subscriptions -- was causing a UI crash whenever someone tried to open a DM from the user card component.", + "milestone": "4.5.2", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "24750", + "title": "[IMPROVE] Voip Extensions disabled state", + "userLogin": "MartinSchoeler", + "milestone": "4.5.2", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24748", + "title": "[IMPROVE] UX - VoIP Call Component", + "userLogin": "tiagoevanp", + "milestone": "4.5.3", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "24753", + "title": "Chore: Micro services fixes and cleanup", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24756", + "title": "Regression: Improve Sidenav open/close handling and fixed codeql configs and E2E tests", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "24760", + "title": "[FIX] Apple login script being loaded even when Apple Login is disabled.", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.5.1", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "24754", + "title": "Chore: Update Livechat", + "userLogin": "MartinSchoeler", + "milestone": "4.5.1", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24683", + "title": "[FIX] no id of room closer in livechat-close message", + "userLogin": "cuonghuunguyen", + "milestone": "4.5.1", + "contributors": [ + null + ] + }, + { + "pr": "24771", + "title": "Chore: fix grammatical errors in Features", + "userLogin": "aadishJ01", + "contributors": [ + "aadishJ01", + "web-flow" + ] + }, + { + "pr": "24759", + "title": "Chore: Fix grammatical errors in Code of Conduct", + "userLogin": "aadishJ01", + "contributors": [ + "aadishJ01", + "web-flow" + ] + }, + { + "pr": "23795", + "title": "[FIX] Reload roomslist after successful deletion of a room from admin panel.", + "userLogin": "Aman-Maheshwari", + "description": "Removed the logic for calling the `rooms.adminRooms` endPoint from the `RoomsTable` Component and moved it to its parent component `RoomsPage`.\r\nThis allows to call the endPoint `rooms.adminRooms` from `EditRoomContextBar` Component which is also has `RoomPage` Component as its parent.\r\n\r\nAlso added a succes toast message after the successful deletion of room.", + "milestone": "4.5.1", + "contributors": [ + "Aman-Maheshwari", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "24743", + "title": "[FIX] System messages are sent when adding or removing a group from a team", + "userLogin": "matheusbsilva137", + "description": "- Do not send system messages when adding or removing a new or existing _group_ from a team.", + "milestone": "4.5.1", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "24544", + "title": "Chore: Fix Cypress tests", + "userLogin": "rodrigok", + "contributors": [ + "rodrigok", + "tassoevan", + "dougfabris" + ] + }, + { + "pr": "24737", + "title": "[FIX] Typo and placeholder on wrap up call modal", + "userLogin": "MartinSchoeler", + "milestone": "4.5.1", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24739", + "title": "[IMPROVE][ENTERPRISE] Don't start presence monitor when running micro services", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24738", + "title": "[FIX][ENTERPRISE] DDP streamer not sending data to all clients", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24680", + "title": "[FIX] Show only available agents on extension association modal", + "userLogin": "KevLehman", + "milestone": "4.5.1", + "contributors": [ + "KevLehman", + "tiagoevanp" + ] + }, + { + "pr": "24710", + "title": "[FIX] DDP streamer errors", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24724", + "title": "[FIX][ENTERPRISE] Presence micro service logic", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24717", + "title": "i18n: Language update from LingoHub 🤖 on 2022-03-07Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24607", + "title": "[FIX] VoIP Enable/Disable setting on CallContext/CallProvider Notifications", + "userLogin": "tiagoevanp", + "milestone": "4.5.1", + "contributors": [ + "tiagoevanp", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "24726", + "title": "Chore: Improve logger to allow log of `unknown` values", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24677", + "title": "[FIX] Components for user search", + "userLogin": "juliajforesti", + "milestone": "4.5.1", + "contributors": [ + "juliajforesti", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "24542", + "title": "[FIX] Date Message Export Filter Fix", + "userLogin": "eduardofcabrera", + "description": "Fix message export filter to get all messages between \"from date\" and \"to date\", including \"to date\".", + "contributors": [ + "eduardofcabrera", + "web-flow" + ] + }, + { + "pr": "24709", + "title": "[FIX] API Error preventing adding an email to users without one (like bot/app users)", + "userLogin": "debdutdeb", + "contributors": [ + "debdutdeb" + ] + }, + { + "pr": "24716", + "title": "Bump ts-node from 10.6.0 to 10.7.0 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24476", + "title": "[FIX] Nextcloud OAuth for incomplete token URL", + "userLogin": "debdutdeb", + "milestone": "4.6.0", + "contributors": [ + "debdutdeb" + ] + }, + { + "pr": "24657", + "title": "[FIX] Voip Stream Reinitialization Error", + "userLogin": "amolghode1981", + "milestone": "4.5.1", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "24698", + "title": "Bump pino-pretty from 7.5.2 to 7.5.3 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24696", + "title": "[FIX] Room's message count not being incremented on import", + "userLogin": "matheusbsilva137", + "description": "- Fix rooms' message counter not being incremented on message import.", + "milestone": "4.5.1", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "23824", + "title": "Chore: Improvements on role syncing (ldap, oauth and saml)", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc", + "tassoevan" + ] + }, + { + "pr": "24689", + "title": "Bump pino-pretty from 7.5.1 to 7.5.2 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24674", + "title": "[FIX] Missing username on messages imported from Slack", + "userLogin": "matheusbsilva137", + "description": "- Fix missing sender's username on messages imported from Slack.", + "milestone": "4.5.1", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "24642", + "title": "Bump actions/setup-node from 2 to 3", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24644", + "title": "i18n: Language update from LingoHub 🤖 on 2022-02-28Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24590", + "title": "[FIX] Duplicated 'name' log key", + "userLogin": "sampaiodiego", + "milestone": "4.5.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24668", + "title": "Bump actions/checkout from 2 to 3", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24574", + "title": "Chore(deps-dev): Bump @types/mock-require from 2.0.0 to 2.0.1", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24667", + "title": "Bump ts-node from 10.5.0 to 10.6.0 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24666", + "title": "Bump @types/ws from 8.2.3 to 8.5.2 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24640", + "title": "Bump url-parse from 1.5.7 to 1.5.10", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24653", + "title": "Merge master into develop & Set version to 4.6.0-develop", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc", + "web-flow" + ] + }, + { + "pr": "24652", + "title": "Release 4.5.0", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "sampaiodiego", + "web-flow", + "aswinidev", + "debdutdeb", + "dependabot[bot]", + "lingohub[bot]", + "ostjen", + "KevLehman", + "dougfabris", + "LucasFASouza", + "felipe-rod123", + "guijun13", + "pierre-lehnen-rc", + "filipemarins", + "matheusbsilva137", + "gabriellsh" + ] + }, + { + "pr": "24661", + "title": "[FIX] Typo in wrap-up term", + "userLogin": "renatobecker", + "milestone": "4.5.1", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "24028", + "title": "[IMPROVE] Updated links in readme", + "userLogin": "aswinidev", + "contributors": [ + "aswinidev", + "web-flow", + "debdutdeb" + ] + }, + { + "pr": "24651", + "title": "Chore: Update Apps-Engine", + "userLogin": "d-gubert", + "milestone": "4.5.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "24649", + "title": "Regression: Refresh server connection when MI server settings change", + "userLogin": "KevLehman", + "milestone": "4.5.0", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24648", + "title": "Regression: Prevent button from losing state when rerendering", + "userLogin": "KevLehman", + "milestone": "4.5.0", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24585", + "title": "Regression: Error setting user avatars and mentioning rooms on Slack Import", + "userLogin": "matheusbsilva137", + "description": "- Fix `Mentioned room not found` error when importing rooms from Slack;\r\n- Fix `Forbidden` error when setting avatars for users imported from Slack (on user import/creation);\r\n- Fix incorrect message count on imported rooms;\r\n- Fix missing username on messages imported from Slack;", + "contributors": [ + "matheusbsilva137", + "pierre-lehnen-rc" + ] + }, + { + "pr": "24647", + "title": "Regression: Fix wrong tab name for VoIP settings", + "userLogin": "renatobecker", + "milestone": "4.5.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "24646", + "title": "Regression: Server crashing if Voip credentials are invalid", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24645", + "title": "Regression: Extension List panel UI not aligned with designs", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24635", + "title": "Regression: Queue counter aggregator for incoming/hanged calls", + "userLogin": "amolghode1981", + "milestone": "4.5.0", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "24630", + "title": "Regression: Fix double value on holdTime and empty msg on last message", + "userLogin": "MartinSchoeler", + "contributors": [ + "MartinSchoeler", + "web-flow" + ] + }, + { + "pr": "24624", + "title": "Regression: If Asterisk suddenly goes down, server has no way to know. Causes server to get stuck. Needs restart", + "userLogin": "amolghode1981", + "milestone": "4.5.0", + "contributors": [ + "amolghode1981", + "KevLehman" + ] + }, + { + "pr": "24601", + "title": "Regression: Prevent connect to asterisk when VoIP is disabled", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98", + "web-flow", + "KevLehman" + ] + }, + { + "pr": "24626", + "title": "Regression: Encode registration info as JWT when signing key is provided", + "userLogin": "KevLehman", + "milestone": "4.5.0", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24625", + "title": "Regression: Fix time fields and wrap up in Voip Room Contexual bar", + "userLogin": "MartinSchoeler", + "milestone": "4.5.0", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24592", + "title": "Regression: Fix in-correct room status shown to agents", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24619", + "title": "Regression: Do not show toast on incoming voip calls", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98", + "web-flow" + ] + }, + { + "pr": "24616", + "title": "Regression: Fix incoming voip call ringtone is not ringing", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98", + "web-flow" + ] + }, + { + "pr": "24610", + "title": "Regression: Mark all rooms as read modal closing instantly.", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "24615", + "title": "Regression: Fix translation for call started message", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98", + "web-flow" + ] + }, + { + "pr": "24594", + "title": "Regression: Bunch of settings fixes for VoIP", + "userLogin": "MartinSchoeler", + "milestone": "4.5.0", + "contributors": [ + "MartinSchoeler", + "web-flow" + ] + }, + { + "pr": "24609", + "title": "Regression: Admin Sidebar colors inverted.", + "userLogin": "gabriellsh", + "milestone": "4.5.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "24602", + "title": "Regression: No audio when call comes from Skype/IP phone", + "userLogin": "amolghode1981", + "description": "The audio was not rendered because of re-rendering of react element based on\r\nqueueCounter and roomInfo. queueCounter and roomInfo cause the dom to re-render when call gets accepted\r\nbecause after accepting call, queueCounter changes or a room gets created.\r\nThe audio element gets recreated. But VoIP user probably holds the old one.\r\nThe behaviour is not predictable when such case happens. If everything gets cleanly setup,\r\neven if the audio element goes headless, it still continues to play the remote audio.\r\nBut in other cases, it is unreferenced the one on dom has its srcObject as null.\r\nThis causes no audio.\r\n\r\nThis fix provides a way to re-initialise the rendering elements in VoIP user\r\nand calls this function on useEffect() if the re-render has happen.", + "milestone": "4.5.0", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "24596", + "title": "Regression: Fixes in Voice Contextual Bar and Directory", + "userLogin": "MartinSchoeler", + "milestone": "4.5.0", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "24603", + "title": "Regression: Fix time format on Voip system messages", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24598", + "title": "Regression: VoIP service button displayed when VoIP is disabled", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24581", + "title": "Regression: Add support to namespace within micro services", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24583", + "title": "Regression: Error when trying to load name of dm rooms for avatars and notifications", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "24567", + "title": "[NEW] Marketplace sort filter", + "userLogin": "ujorgeleite", + "description": "Implemented a sort filter for the marketplace screen. This component sorts the marketplace apps list in 4 ways, alphabetical order(A-Z), inverse alphabetical order(Z-A), most recently updated(MRU), and least recent updated(LRU). Besides that, I've generalized some components and types to increase code reusability, renamed some helpers as well as deleted some useless ones, and inserted the necessary new translations on the English i18n dictionary.\r\nDemo gif:\r\n![Marketplace sort filter](https://user-images.githubusercontent.com/43561537/155033709-e07a6306-a85a-4f7f-9624-b53ba5dd7fa9.gif)", + "milestone": "4.5.0", + "contributors": [ + "rique223", + "ujorgeleite" + ] + }, + { + "pr": "23102", + "title": "[NEW] VoIP Support for Omnichannel", + "userLogin": "KevLehman", + "description": "- Created VoipService to manage VoIP connections and PBX connection\r\n- Created LivechatVoipService that will handle custom cases for livechat (creating rooms, assigning chats to queue, actions when call is finished, etc)\r\n- Created Basic interfaces to support new services and new model\r\n- Created Endpoints for management interfaces\r\n- Implemented asterisk connector on VoIP service\r\n- Created UI components to show calls incoming and to allow answering/rejecting calls\r\n- Added new settings to control call server/management server connection values\r\n- Added endpoints to associate Omnichannel Agents with PBX Extensions\r\n- Added support for event listening on server side, to get metadata about calls being received/ongoing\r\n- Created new pages to update settings & to see user-extension association\r\n- Created new page to see ongoing calls (and past calls)\r\n- Added support for remote hangup/hold on calls\r\n- Implemented call metrics calculation (hold time, waiting time, talk time)\r\n- Show a notificaiton when call is received", + "milestone": "4.5.0", + "contributors": [ + "KevLehman", + "amolghode1981", + "web-flow", + "tiagoevanp", + "murtaza98", + "MartinSchoeler" + ] + }, + { + "pr": "24562", + "title": "Regression: Fix room not getting created due to null visitor status", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24573", + "title": "Chore: Bump Fuselage packages", + "userLogin": "tassoevan", + "description": "It uses the last stable version of Fuselage packages.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "24558", + "title": "i18n: Language update from LingoHub 🤖 on 2022-02-21Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24572", + "title": "[FIX] 2FA via email when logging in using OAuth", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24568", + "title": "Chore: Update Apps-Engine", + "userLogin": "d-gubert", + "milestone": "4.5.0", + "contributors": [ + "d-gubert", + "web-flow" + ] + }, + { + "pr": "24536", + "title": "Chore: roomTypes: Stop mixing client and server code together", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.5.0", + "contributors": [ + "pierre-lehnen-rc", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "24529", + "title": "[IMPROVE] Replace AutoComplete in UserAutoComplete & UserAutoCompleteMultiple components", + "userLogin": "juliajforesti", + "description": "This PR replaces a deprecated fuselage's component `AutoComplete` in favor of `Select` and `MultiSelect` which fixes some of UX/UI issues in selecting users\r\n\r\n### before\r\n![Screen Shot 2022-02-19 at 13 33 28](https://user-images.githubusercontent.com/27704687/154809737-8181a06c-4f20-48ea-90f7-01e828b9a452.png)\r\n\r\n### after\r\n![Screen Shot 2022-02-19 at 13 30 58](https://user-images.githubusercontent.com/27704687/154809653-a8ec9a80-c0dd-4a25-9c00-0f96147d79e9.png)", + "contributors": [ + "juliajforesti", + "dougfabris", + "tassoevan" + ] + }, + { + "pr": "24513", + "title": "Chore: Run tests using microservices deployment on CI", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "24556", + "title": "Bump @types/ws from 8.2.2 to 8.2.3 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24501", + "title": "Chore: Update fuselage deps to match monolith versions", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24538", + "title": "Bump adm-zip from 0.4.14 to 0.5.9", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24454", + "title": "[IMPROVE] Purchase Type Filter for marketplace apps and Categories filter anchor refactoring", + "userLogin": "rique223", + "description": "Implemented a filter by purchase type(free or paid) component for the apps screen of the marketplace. Besides that, new entries on the dictionary, fixed some parts of the App type (purchaseType was typed as unknown and price as string), and created some helpers to work alongside the filter. Will be refactoring the categories filter anchor and then will open this PR for reviews.\r\n\r\nDemo gif:\r\n![purchaseTypeFIlter](https://user-images.githubusercontent.com/43561537/153101228-7b7ebdc3-2d34-420f-aa9d-f7cbc8d4b53f.gif)\r\n\r\nRefactored the categories filter anchor from a plain fuselage select to a select button with dynamic colors.\r\nDemo gif:\r\n![New categories filter anchor(PR)](https://user-images.githubusercontent.com/43561537/153422427-28012b7d-e0ec-45f4-861d-c9368c57ad04.gif)", + "contributors": [ + "rique223", + "dougfabris", + "web-flow" + ] + }, + { + "pr": "24475", + "title": "[IMPROVE] Skip encryption for slash commands in E2E rooms", + "userLogin": "yash-rajpal", + "description": "Currently Slash Commands don't work in an E2EE room, as we encrypt the message before slash command is detected by the server, So removed encryption for slash commands in e2e rooms.", + "contributors": [ + "yash-rajpal", + "albuquerquefabio", + "web-flow" + ] + }, + { + "pr": "24304", + "title": "Chore: Js to ts slash commands archive", + "userLogin": "eduardofcabrera", + "description": "Convert Slash Commands archive files to typescript", + "contributors": [ + "eduardofcabrera", + "web-flow" + ] + }, + { + "pr": "24114", + "title": "[NEW] E2E password generator", + "userLogin": "ostjen", + "contributors": [ + "ostjen", + "web-flow", + "eduardofcabrera", + "tassoevan" + ] + }, + { + "pr": "24553", + "title": "[FIX] Omnichannel managers can't join chats in progress", + "userLogin": "renatobecker", + "milestone": "4.5.0", + "contributors": [ + "renatobecker", + "murtaza98", + "web-flow" + ] + }, + { + "pr": "24559", + "title": "[FIX] Room context tabs not working in Omnichannel current chats page", + "userLogin": "murtaza98", + "milestone": "4.5.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "24173", + "title": "[FIX] respect `Accounts_Registration_Users_Default_Roles` setting", + "userLogin": "debdutdeb", + "description": "- Fix `user` role being added as default regardless of the `Accounts_Registration_Users_Default_Roles` setting.", + "milestone": "4.5.0", + "contributors": [ + "debdutdeb", + "web-flow", + "matheusbsilva137" + ] + }, + { + "pr": "24485", + "title": "[FIX] Skip admin info in setup wizard for servers with admin registered", + "userLogin": "dougfabris", + "contributors": [ + "dougfabris", + "tassoevan" + ] + }, + { + "pr": "24537", + "title": "Bump pm2 from 5.1.2 to 5.2.0 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24209", + "title": "[IMPROVE] Team system messages feedback", + "userLogin": "ostjen", + "description": "- Delete some keys that aren't being used (eg: User_left_female).\r\n- Add new Teams' system messages:\r\n - `added-user-to-team`: **added** @\\user to this Team;\r\n - `removed-user-from-team`: **removed** @\\user from this Team;\r\n - `user-converted-to-team`: **converted** #\\room to a Team;\r\n - `user-converted-to-channel`: **converted** #\\room to a Channel;\r\n - `user-removed-room-from-team`: **removed** @\\user from this Team;\r\n - `user-deleted-room-from-team`: **deleted** #\\room from this Team;\r\n - `user-added-room-to-team`: **deleted** #\\room to this Team;\r\n- Add the corresponding options to hide each new system message and the missing `ujt` and `ult` hide options.", + "milestone": "4.5.0", + "contributors": [ + "ostjen", + "tassoevan", + "web-flow", + "dougfabris", + "matheusbsilva137" + ] + }, + { + "pr": "24467", + "title": "Chore: Improve PR title validation regex", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow", + "debdutdeb" + ] + }, + { + "pr": "24058", + "title": "Bump date-fns from 2.24.0 to 2.28.0", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24508", + "title": "[FIX] Read receipts showing first messages of the room as read even if not read by everyone", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow", + "dougfabris" + ] + }, + { + "pr": "24530", + "title": "Chore: Remove storybook build job from CI", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24528", + "title": "Bump url-parse from 1.5.3 to 1.5.7", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24333", + "title": "Chore: Add description to global OTR setting", + "userLogin": "pedrogssouza", + "contributors": [ + "pedrogssouza", + "yash-rajpal", + "web-flow" + ] + }, + { + "pr": "24382", + "title": "[IMPROVE] OTR system messages", + "userLogin": "yash-rajpal", + "description": "OTR system messages to indicate key refresh and joining chat to users.", + "contributors": [ + "yash-rajpal", + "web-flow" + ] + }, + { + "pr": "24121", + "title": "[IMPROVE] Descriptive tooltip for Encrypted Key on Room Header", + "userLogin": "yash-rajpal", + "milestone": "4.5.0", + "contributors": [ + "yash-rajpal", + "web-flow" + ] + }, + { + "pr": "24522", + "title": "Bump express from 4.17.2 to 4.17.3 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24518", + "title": "Chore: `twoFactorRequired` signature", + "userLogin": "tassoevan", + "description": "Improved type checking for decorator `twoFactorRequired`.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "24517", + "title": "Bump body-parser from 1.19.1 to 1.19.2 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24441", + "title": "[FIX] GDPR action to forget visitor data on request", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman", + "murtaza98", + "web-flow" + ] + }, + { + "pr": "24306", + "title": "Chore: Convert to typescript the slash commands create files", + "userLogin": "eduardofcabrera", + "description": "Convert Slash Commands create files to typescript.", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow" + ] + }, + { + "pr": "24325", + "title": "Chore: Convert to typescript the mute and unmute slash commands files", + "userLogin": "eduardofcabrera", + "description": "Convert to typescript the mute and unmute slash commands files", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow" + ] + }, + { + "pr": "24321", + "title": "Chore: Convert to typescript the me slashCommands files", + "userLogin": "eduardofcabrera", + "description": "Convert to typescript the me slashCommands files", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow" + ] + }, + { + "pr": "23512", + "title": "Bump sodium-native from 3.2.1 to 3.3.0 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24311", + "title": "Chore: Convert to typescript the slash commands invite files", + "userLogin": "eduardofcabrera", + "description": "Convert to typescript the slash commands invite files", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow" + ] + }, + { + "pr": "24509", + "title": "Bump vm2 from 3.9.5 to 3.9.7 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24451", + "title": "[IMPROVE] ChatBox Text to File Description", + "userLogin": "eduardofcabrera", + "description": "The text content from chatbox goes to the file description when drag and drop a file.", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow", + "dougfabris" + ] + }, + { + "pr": "24461", + "title": "Chore: Update Meteor to 2.5.6", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "24477", + "title": "Chore: Update ws package", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24498", + "title": "Bump underscore.string from 3.3.5 to 3.3.6 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24491", + "title": "Bump follow-redirects from 1.14.7 to 1.14.8 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24493", + "title": "i18n: Language update from LingoHub 🤖 on 2022-02-14Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null + ] + }, + { + "pr": "24331", + "title": "Chore: Convert to typescript the unarchive slash commands files", + "userLogin": "eduardofcabrera", + "description": "Convert to typescript the unarchive slash commands files", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow" + ] + }, + { + "pr": "24483", + "title": "[IMPROVE] Add tooltips on action buttons of Canned Response message composer", + "userLogin": "LucasFASouza", + "description": "The tooltips were missing on the action buttons of CR message composer.\r\n\r\n![image](https://user-images.githubusercontent.com/32396925/153620327-91107245-4b47-4d39-a99a-6da6d1cf5734.png)\r\n\r\nUsers can now feel more encouraged to use these actions knowing what they are supposed to do.", + "contributors": [ + "LucasFASouza", + "tiagoevanp", + "web-flow" + ] + }, + { + "pr": "24196", + "title": "Chore: Delete unused file (NewAdminInfoPage.js)", + "userLogin": "gabriellsh", + "description": "Just removing a duplicated/unused file.", + "milestone": "4.5.0", + "contributors": [ + "gabriellsh", + "web-flow" + ] + }, + { + "pr": "24388", + "title": "[IMPROVE][ENTERPRISE] Improve how micro services are loaded", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "24458", + "title": "[IMPROVE] Add return button in chats opened from the list of current chats", + "userLogin": "LucasFASouza", + "description": "The new return button for Omnichannel chats came out with release 3.15 but the feature was only available for chats that were opened from Omnichannel Contact Center.\r\nNow, the same UI/UX is supported for chats opened from Current Chats list.\r\n\r\n![image](https://user-images.githubusercontent.com/32396925/153283190-bd5c9748-c36b-4874-a704-6043afc7e3a1.png)\r\n\r\nThe chat now opens in the Omnichannel settings and has the return button so the user can go back to the Current Chats list.\r\n\r\n![image](https://user-images.githubusercontent.com/32396925/153285591-fad8e4a0-d2ea-4a02-8b2a-15e383b3c876.png)", + "contributors": [ + "LucasFASouza", + "tiagoevanp", + "web-flow" + ] + }, + { + "pr": "24469", + "title": "Bump express from 4.17.1 to 4.17.2 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24472", + "title": "Bump cookie from 0.4.1 to 0.4.2 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24275", + "title": "[IMPROVE] Close modal on esc and outside click", + "userLogin": "gabriellsh", + "description": "This is a QUICK change in order to close modals pressing Esc button and clicking outside of it **intentionally**.", + "milestone": "4.5.0", + "contributors": [ + "gabriellsh", + "dougfabris", + "tassoevan" + ] + }, + { + "pr": "24435", + "title": "Chore(deps-dev): Bump ts-node from 10.0.0 to 10.5.0 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24041", + "title": "[IMPROVE] Add user to room on \"Click to Join!\" button press", + "userLogin": "matheusbsilva137", + "description": "- Add user to room on \"Click to Join!\" button press;\r\n- Display the \"Join\" button in discussions inside channels (keeping the behavior consistent with discussions inside groups).", + "contributors": [ + "matheusbsilva137", + "web-flow", + "tassoevan", + "pierre-lehnen-rc", + "ostjen" + ] + }, + { + "pr": "24310", + "title": "[FIX] Implement client errors on ddp-streamer", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "23963", + "title": "Bump body-parser from 1.19.0 to 1.19.1 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23961", + "title": "Bump jaeger-client from 3.18.1 to 3.19.0 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24466", + "title": "[FIX] typo on register server tooltip of setup wizard", + "userLogin": "filipemarins", + "milestone": "4.5.0", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "24037", + "title": "[FIX] Inconsistent validation of user's access to rooms", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc", + "ostjen", + "web-flow" + ] + }, + { + "pr": "24450", + "title": "[FIX] OAuth mismatch redirect_uri error", + "userLogin": "sampaiodiego", + "milestone": "4.4.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24305", + "title": "[FIX] Prevent Apps Bridge to remove visitor status from room", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman", + "d-gubert" + ] + }, + { + "pr": "24453", + "title": "Chore: bump fuselage version", + "userLogin": "dougfabris", + "milestone": "4.4.2", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "24253", + "title": "[FIX] Issues on selecting users when importing CSV", + "userLogin": "guijun13", + "description": "* Fix users selecting by fixing their _id\r\n* Add condition to disable 'Start importing' button if `usersCount`, `channelsCount` and `messageCount` equals 0, or if messageCount is alone\r\n* Remove `disabled={usersCount === 0}` on user Tab", + "contributors": [ + "guijun13", + "tassoevan", + "web-flow", + "pierre-lehnen-rc" + ] + }, + { + "pr": "24299", + "title": "Chore(deps): Bump node-fetch from 2.6.1 to 2.6.7 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24418", + "title": "[FIX] Oembed request not respecting payload limit", + "userLogin": "sampaiodiego", + "milestone": "4.4.1", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24429", + "title": "i18n: Language update from LingoHub 🤖 on 2022-02-07Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24407", + "title": "[FIX] Skip cloud steps for registered servers on setup wizard", + "userLogin": "dougfabris", + "milestone": "4.4.1", + "contributors": [ + "dougfabris", + "tassoevan", + "gabriellsh", + "web-flow" + ] + }, + { + "pr": "24410", + "title": "Chore: Convert JS files to Typescript", + "userLogin": "felipe-rod123", + "description": "This pull request converts 26 more files from Javascript to Typescript, to check variable types and increase validation on the code.", + "contributors": [ + "felipe-rod123", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "24369", + "title": "[IMPROVE] Convert tag edit with department data to tsx", + "userLogin": "LucasFASouza", + "contributors": [ + "LucasFASouza", + "tiagoevanp", + "web-flow" + ] + }, + { + "pr": "24401", + "title": "[FIX] Outgoing webhook without scripts not saving messages", + "userLogin": "sampaiodiego", + "milestone": "4.4.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24334", + "title": "[IMPROVE] CloudLoginModal visual consistency", + "userLogin": "dougfabris", + "description": "### before\r\n![image](https://user-images.githubusercontent.com/27704687/151585064-dc6a1e29-9903-4241-8fbd-dfbe6c55fbef.png)\r\n\r\n### after\r\n![Screen Shot 2022-01-28 at 13 32 02](https://user-images.githubusercontent.com/27704687/151585101-75b98502-9aae-4198-bc3e-4956750e5d8b.png)", + "milestone": "4.5.0", + "contributors": [ + "dougfabris", + "gabriellsh", + "web-flow" + ] + }, + { + "pr": "24409", + "title": "[FIX] Startup errors creating indexes", + "userLogin": "sampaiodiego", + "description": "Fix `bio` and `prid` startup index creation errors.", + "milestone": "4.4.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24406", + "title": "Chore: Unify ILivechatAgent with ILivechatAgentRecord", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24381", + "title": "[FIX] Add ?close to OAuth callback url", + "userLogin": "sampaiodiego", + "milestone": "4.4.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24387", + "title": "[FIX] Slash commands previews not working", + "userLogin": "ostjen", + "milestone": "4.4.1", + "contributors": [ + "ostjen" + ] + }, + { + "pr": "24357", + "title": "i18n: Language update from LingoHub 🤖 on 2022-01-31Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null + ] + }, + { + "pr": "24341", + "title": "Bump simple-get from 4.0.0 to 4.0.1", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24366", + "title": "Chore: Set Docker image tag to latest only when really latest", + "userLogin": "debdutdeb", + "contributors": [ + "debdutdeb", + "web-flow" + ] + }, + { + "pr": "24109", + "title": "[IMPROVE] Added a new \"All\" tab which shows all integrations in Integrations", + "userLogin": "aswinidev", + "milestone": "4.5.0", + "contributors": [ + "aswinidev", + "dougfabris", + "web-flow" + ] + }, + { + "pr": "24363", + "title": "Merge master into develop & Set version to 4.5.0-develop", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + } + ] + }, + "4.5.7": { + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [] + }, + "5.0.0-rc.0": { + "node_version": "14.18.3", + "npm_version": "6.14.15", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "26107", + "title": "Chore: move fork of cas module to the monorepo", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "25681", + "title": "Chore: Add Agenda fork to the monorepo", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc", + "ggazzo" + ] + }, + { + "pr": "25624", + "title": "Chore: Bump deps", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "pierre-lehnen-rc", + "web-flow" + ] + }, + { + "pr": "25791", + "title": "[NEW][ENTERPRISE] Device Management", + "userLogin": "yash-rajpal", + "milestone": "5.0.0", + "contributors": [ + "yash-rajpal", + "web-flow", + "albuquerquefabio", + "debdutdeb" + ] + }, + { + "pr": "26040", + "title": "Chore: `refactor/tsc-perf`", + "userLogin": "tassoevan", + "milestone": "5.0.0", + "contributors": [ + "tassoevan", + "ggazzo", + "pierre-lehnen-rc", + "web-flow" + ] + }, + { + "pr": "26100", + "title": "[BREAK] Upgrade to version 5.0 can be done only from version 4.x", + "userLogin": "sampaiodiego", + "milestone": "5.0.0", + "contributors": [ + "sampaiodiego", + "pierre-lehnen-rc" + ] + }, + { + "pr": "26098", + "title": "[BREAK] Remove support to old MongoDB versions", + "userLogin": "sampaiodiego", + "description": "As per MongoDB Lifecycle Schedules (https://www.mongodb.com/support-policy/lifecycles) we're removing official support to MongoDB versions **3.6 and 4.0** that have already reached end-of-life.\r\n\r\nAs MongoDB 4.2 was a \"supported\" version before Rocket.Chat 5.0, we'll continue supporting it, but will be flagged as deprecated. We recommend upgrading to MongoDB 4.4+.\r\n\r\nHere are official docs on how to upgrade to some of the supported versions:\r\n\r\n- https://www.mongodb.com/docs/manual/release-notes/4.2-upgrade-replica-set/\r\n- https://www.mongodb.com/docs/v4.4/release-notes/4.4-upgrade-replica-set/\r\n- https://www.mongodb.com/docs/manual/release-notes/5.0-upgrade-replica-set/", + "milestone": "5.0.0", + "contributors": [ + "sampaiodiego", + "KevLehman", + "debdutdeb", + "web-flow" + ] + }, + { + "pr": "25847", + "title": "[NEW] Matrix Federation UX improvements", + "userLogin": "alansikora", + "milestone": "5.0.0", + "contributors": [ + "MarcosSpessatto", + "web-flow", + "carlosrodrigues94", + "alansikora" + ] + }, + { + "pr": "26081", + "title": "[NEW][ENTERPRISE] Introducing dial pad component into sidebar, calls table, contextual bar", + "userLogin": "aleksandernsilva", + "description": "This PR adds a new call button that can be used from Sidebar & Contact Center. This also enables Omnichannel agents to make outbound calls from within Rocket.Chat.\r\n\r\nDepending on your server and call server configuration, you can do international calling, national and domestic calling.\r\n\r\nThe buttons on Contact Center allows an agent to call an existing number without having to type the number again.", + "milestone": "5.0.0", + "contributors": [ + "ggazzo", + "aleksandernsilva", + "KevLehman" + ] + }, + { + "pr": "26053", + "title": "Chore: Settings UI issue", + "userLogin": "dougfabris", + "milestone": "5.0.0", + "contributors": [ + "dougfabris", + "ggazzo" + ] + }, + { + "pr": "26064", + "title": "Chore: Adding default message parser template", + "userLogin": "hugocostadev", + "milestone": "5.0.0", + "contributors": [ + "hugocostadev", + "gabriellsh", + "sampaiodiego" + ] + }, + { + "pr": "26099", + "title": "Regression: [VideoConference] If the caller loses connection, direct calls are never canceled", + "userLogin": "pierre-lehnen-rc", + "milestone": "5.0.0", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "26094", + "title": "Chore: Handle errors on index creation", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "26095", + "title": "Chore: fix watermark condition", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "24534", + "title": "[FIX] Validate room access", + "userLogin": "albuquerquefabio", + "description": "The request must be blocked If the user has no permission to view rooms.", + "milestone": "5.0.0", + "contributors": [ + "albuquerquefabio", + "web-flow", + "yash-rajpal", + "pierre-lehnen-rc" + ] + }, + { + "pr": "25570", + "title": "[BREAK] VideoConference", + "userLogin": "dougfabris", + "description": "In this PR we're deprecating the Video Conference functionality from the core of the application and introducing a **new video conference flow**:\r\n\r\n\r\n\r\nNow the video conference feature will be agnostic so you'll be able to set the provider such as **Jisti** and **BBB** as apps from our marketplace: \r\n\r\n\r\n\r\nVideo conferences settings are now global, allowing you to set the default provider\r\n\r\n\r\n\r\n### [Enterprise Features]\r\n- Video Conferences List\r\n\r\n\r\n- Ringing function for direct messages\r\n\r\n\r\n\r\n", + "milestone": "5.0.0", + "contributors": [ + "pierre-lehnen-rc", + "web-flow", + "dougfabris" + ] + }, + { + "pr": "26083", + "title": "[FIX] Undefined headers on API Client", + "userLogin": "yash-rajpal", + "milestone": "5.0.0", + "contributors": [ + "yash-rajpal" + ] + }, + { + "pr": "26067", + "title": "Regression: Add Error boundary to katex render component", + "userLogin": "gabriellsh", + "milestone": "5.0.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "26084", + "title": "Chore: Allow endpoints to optionally require authentication", + "userLogin": "pierre-lehnen-rc", + "milestone": "5.0.0", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "26088", + "title": "Regression: Unhandled Exceptions metric causing a secondary exception", + "userLogin": "pierre-lehnen-rc", + "milestone": "5.0.0", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "26057", + "title": "[FIX] Unable to close chats when comments is disabled", + "userLogin": "murtaza98", + "description": "Fixes https://github.com/RocketChat/Rocket.Chat/issues/25954", + "milestone": "5.0.0", + "contributors": [ + "murtaza98", + "web-flow" + ] + }, + { + "pr": "26086", + "title": "Chore: Room access validation may be called without user information", + "userLogin": "pierre-lehnen-rc", + "milestone": "5.0.0", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "25491", + "title": "[IMPROVE] Avoid using omnichannel-queue collection", + "userLogin": "KevLehman", + "milestone": "5.0.0", + "contributors": [ + "KevLehman", + "murtaza98" + ] + }, + { + "pr": "26087", + "title": "[FIX] Remove duplicated property _USERNAMES from createDirectRoom.ts", + "userLogin": "felipe-rod123", + "description": "This pull request removes the duplicated property `_USERNAMES` from `apps/meteor/app/lib/server/functions/createDirectRoom.ts`, using only the existing property `roomInfo.usernames`.", + "contributors": [ + "felipe-rod123" + ] + }, + { + "pr": "26085", + "title": "Chore: Improve footer Template", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "tiagoevanp" + ] + }, + { + "pr": "26055", + "title": "Regression: Fix call direction", + "userLogin": "murtaza98", + "milestone": "5.0.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "25949", + "title": "[NEW][APPS] Allow dispatchment of actions from input elements", + "userLogin": "thassiov", + "description": "This allows for apps receiving block actions when a user types on a plain text input field or selects an item from the static. A debounce of 700 ms is done when listening for typing action so the app is not flooded with actions.\r\n\r\n\r\nhttps://user-images.githubusercontent.com/733282/174858175-5ea53046-c791-493e-859b-b80431e94ffa.mp4", + "milestone": "5.0.0", + "contributors": [ + "ggazzo", + "thassiov", + "d-gubert" + ] + }, + { + "pr": "26077", + "title": "Regression: Revert Livechat packages upgrades/removals that were causing issues", + "userLogin": "murtaza98", + "milestone": "5.0.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "26079", + "title": "Regression: Users Table loading state", + "userLogin": "dougfabris", + "milestone": "5.0.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "26074", + "title": "Regression: Fix import endpoints", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "20913", + "title": "[BREAK] Suspend push notifications when login token is invalidated", + "userLogin": "g-thome", + "description": "link the auth token to the push token", + "milestone": "5.0.0", + "contributors": [ + "g-thome", + "sampaiodiego", + "pierre-lehnen-rc" + ] + }, + { + "pr": "25724", + "title": "[FIX] Not showing edit message button when blocking edit after N minutes", + "userLogin": "matthias4217", + "description": "Previously, in Rocketchat 4.7.0 and later, as mentioned in https://github.com/RocketChat/Rocket.Chat/issues/25478, the edit button was not displayed on the interface in the minute after having sent a message. This is now fixed : messages can be edited right after sending them.", + "milestone": "5.0.0", + "contributors": [ + "matthias4217", + "sampaiodiego" + ] + }, + { + "pr": "25331", + "title": "[FIX] Misaligned username on Room Info card for omnichannel chats", + "userLogin": "murtaza98", + "milestone": "5.0.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "26075", + "title": "Chore: Revert `yarn dev` implementation", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "26063", + "title": "Regression: Contact manager endpoint usage", + "userLogin": "KevLehman", + "milestone": "5.0.0", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25965", + "title": "[NEW] Create releases tab in the marketplace app info page", + "userLogin": "rique223", + "description": "Added a Releases tab to the app info page of installed marketplace apps. This tab will show all the released versions of a given app with its version number, release date in humanized form, and the changelog of this given release with the information provided by the publisher, this changelog accepts and renders markdown. Also refactored some component names and logic for maintainability reasons.\r\nDemo gif:\r\n![app-releases-tab-final](https://user-images.githubusercontent.com/43561537/176228928-651074ce-1f8b-4531-95be-1dd107938bf3.gif)", + "milestone": "5.0.0", + "contributors": [ + "rique223" + ] + }, + { + "pr": "26071", + "title": "Regression: `yarn dev` not working", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "26070", + "title": "Chore: Close tooltip on click", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "26069", + "title": "Chore: Make kodiak merge message empty", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25930", + "title": "[FIX] Too many watchers in dev environment.", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25855", + "title": "[FIX] Update subscription on update team member", + "userLogin": "LucianoPierdona", + "description": "Added update to subscription when a team member is updated on `teams.updateMember`", + "milestone": "5.0.0", + "contributors": [ + "LucianoPierdona", + "matheusbsilva137", + "web-flow" + ] + }, + { + "pr": "25988", + "title": "Regression: Add appId prop to slashcommand", + "userLogin": "tapiarafael", + "description": "Pass the appId when present to the slashcommand array. This avoid problems with contextual bar and modals not opening.", + "milestone": "5.0.0", + "contributors": [ + "tapiarafael" + ] + }, + { + "pr": "26065", + "title": "Chore: Convert useSidebarPaletteColor", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti" + ] + }, + { + "pr": "26058", + "title": "[FIX] Error \"numRequestsAllowed\" property in rateLimiter for REST API endpoint when upgrading", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "26051", + "title": "[FIX] Remove duplicated icon bell when is thread main message", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "26059", + "title": "Chore: Convert normalizeMessagesForUser", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25916", + "title": "Chore: ui-client package", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "26056", + "title": "Regression: Invalid Voip host issue preventing agents connecting to asterisk", + "userLogin": "murtaza98", + "milestone": "5.0.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "22588", + "title": "[FIX] Direct Reply", + "userLogin": "ggazzo", + "milestone": "5.0.0", + "contributors": [ + "ggazzo", + "dougfabris" + ] + }, + { + "pr": "25913", + "title": "[NEW][APPS] Allow apps to modify a subset of global settings", + "userLogin": "pierre-lehnen-rc", + "milestone": "5.0.0", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "25844", + "title": "[NEW] Community Edition Watermark", + "userLogin": "hugocostadev", + "milestone": "5.0.0", + "contributors": [ + "hugocostadev", + "tassoevan", + "ggazzo" + ] + }, + { + "pr": "25889", + "title": "[BREAK] remove unused endpoints and restify others", + "userLogin": "KevLehman", + "milestone": "5.0.0", + "contributors": [ + "KevLehman", + "murtaza98" + ] + }, + { + "pr": "25993", + "title": "[IMPROVE] VoIP admin page cleanup: remove unused settings", + "userLogin": "cauefcr", + "description": "https://app.clickup.com/t/2n4m61m", + "milestone": "5.0.0", + "contributors": [ + "cauefcr", + "murtaza98", + "web-flow", + "KevLehman" + ] + }, + { + "pr": "26054", + "title": "Regression: Fix micro services", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "26052", + "title": "Regression: Fix threads list", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25966", + "title": "[NEW] VoIP Input/Output Device Selection", + "userLogin": "MartinSchoeler", + "milestone": "5.0.0", + "contributors": [ + "amolghode1981", + "web-flow", + "MartinSchoeler", + "KevLehman", + "aleksandernsilva" + ] + }, + { + "pr": "25929", + "title": "Chore: Account/Profile to TS", + "userLogin": "yash-rajpal", + "contributors": [ + "yash-rajpal", + "ggazzo" + ] + }, + { + "pr": "26048", + "title": "Chore: Add missing Swedish livechat translations", + "userLogin": "joakimaho", + "description": "Added missing Swedish translations.", + "contributors": [ + "joakimaho", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "25970", + "title": "[IMPROVE] Expand the feature set of the new message rendering", + "userLogin": "tassoevan", + "description": "- Everything inside a new package (`@rocket.chat/gazzodown`);\r\n- KaTeX support;\r\n- Highlighted Words support;\r\n- Emoji rendering expanded;\r\n- Code rendering fixed", + "contributors": [ + "tassoevan", + "gabriellsh" + ] + }, + { + "pr": "26036", + "title": "Chore: Bump fuselage and update icon", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25937", + "title": "[NEW][APPS] Allowing apps to register authenticated routes", + "userLogin": "d-gubert", + "description": "Adds adaptations that allow apps to declare an API endpoint that requires authorization from Rocket.Chat prior to executing", + "milestone": "5.0.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "25960", + "title": "[NEW] Enable outbound calling for EE (#25843)", + "userLogin": "KevLehman", + "milestone": "5.0.0", + "contributors": [ + "amolghode1981", + "web-flow", + "KevLehman", + "murtaza98", + "aleksandernsilva", + "tiagoevanp", + "ggazzo" + ] + }, + { + "pr": "26047", + "title": "Chore: Introduce new index to query active livechat conversations for cloud scaling", + "userLogin": "murtaza98", + "milestone": "5.0.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "25934", + "title": "[FIX] Importer fails to download files from URLs with query string params", + "userLogin": "pierre-lehnen-rc", + "milestone": "5.0.0", + "contributors": [ + "pierre-lehnen-rc", + "ggazzo" + ] + }, + { + "pr": "26007", + "title": "[IMPROVE] Moved call hold/unhold to EE", + "userLogin": "aleksandernsilva", + "description": "This PR adds a restriction, enabling the feature to hold/unhold calls only for Enterprise Edition users.", + "milestone": "5.0.0", + "contributors": [ + "aleksandernsilva", + "murtaza98" + ] + }, + { + "pr": "25505", + "title": "[NEW] Engagement Metrics - Phase 2", + "userLogin": "matheusbsilva137", + "description": "Add the following new statistics (metrics):\r\n - Total Broadcast rooms\r\n - Total rooms with an active Livestream;\r\n - Total triggered emails;\r\n - Total subscription roles;\r\n - Total User Roles;\r\n - Total uncaught exceptions;\r\n - `homeTitleChanged`: boolean value to indicate whether the `Layout_Home_Title` setting has been changed;\r\n - `homeBodyChanged`: boolean value to indicate whether the `Layout_Home_Body` setting has been changed;\r\n - `customCSSChanged`: boolean value to indicate whether the `theme-custom-css` setting has been changed;\r\n - `onLogoutCustomScriptChanged`: boolean value to indicate whether the `Custom_Script_On_Logout` setting has been changed;\r\n - `loggedOutCustomScriptChanged`: boolean value to indicate whether the `Custom_Script_Logged_Out` setting has been changed;\r\n - `loggedInCustomScriptChanged`: boolean value to indicate whether the `Custom_Script_Logged_In` setting has been changed;\r\n - `matrixBridgeEnabled`: boolean value to indicate whether the Matrix bridge has been enabled;", + "milestone": "5.0.0", + "contributors": [ + "matheusbsilva137", + "web-flow" + ] + }, + { + "pr": "26035", + "title": "Chore: Convert usePreventDefault, useQueryOptions, useShortcutOpenMenu", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti" + ] + }, + { + "pr": "25919", + "title": "[FIX] Importer files are unnecessarily transferred over the network.", + "userLogin": "pierre-lehnen-rc", + "milestone": "5.0.0", + "contributors": [ + "pierre-lehnen-rc", + "ggazzo" + ] + }, + { + "pr": "26038", + "title": "Chore: test turbo params", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "26023", + "title": "Chore: Create a token for each action", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25622", + "title": "Chore: Migrate oembed to ts", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman", + "ggazzo" + ] + }, + { + "pr": "26024", + "title": "Regression: Fix voip call wrap-up model not working", + "userLogin": "murtaza98", + "milestone": "5.0.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "26001", + "title": "Chore: Updating Apps-Engine ", + "userLogin": "d-gubert", + "milestone": "5.0.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "25643", + "title": "[IMPROVE] Differ Voip calls from Incoming and Outgoing", + "userLogin": "murtaza98", + "description": "Updated this column and its respective endpoints to support inbound/outfound call definitions\r\n![image](https://user-images.githubusercontent.com/34130764/170512008-34202ed8-3ed4-4c28-baa5-25efc17543d5.png)", + "milestone": "5.0.0", + "contributors": [ + "murtaza98", + "web-flow", + "KevLehman" + ] + }, + { + "pr": "24379", + "title": "[FIX] Append path To Route For Custom Emoji", + "userLogin": "nishant23122000", + "milestone": "5.0.0", + "contributors": [ + "nishant23122000", + "web-flow" + ] + }, + { + "pr": "25875", + "title": "[IMPROVE] Moved call wrap up modal to EE", + "userLogin": "aleksandernsilva", + "description": "This PR adds a restriction, enabling the feature to display the call wrap up modal only for Enterprise Edition users.", + "contributors": [ + "aleksandernsilva", + "tiagoevanp" + ] + }, + { + "pr": "26015", + "title": "Chore: Major refactors in pageobjects", + "userLogin": "souzaramon", + "contributors": [ + "souzaramon" + ] + }, + { + "pr": "26002", + "title": "[BREAK] Remove show message in main thread preference", + "userLogin": "dougfabris", + "description": "This PR removes the confusion between the `show message in main thread` and the function `also to send to channel`. In the past, we used the `show message in main thread` as a solution to help users to understand the thread feature, as this feature is now mature enough there's no reason to maintain this preference. \r\n\r\nSend the thread message to the main channel or just inside of the thread, should be a decision from the user where the function `also send to channel` appears. Because of that, and because of a bunch of requests and issues we received, we're introducing a new preference `also send thread to channel` where users will be able to decide the behavior of the checkbox. \r\n\r\n![image](https://user-images.githubusercontent.com/27704687/175655594-023c5907-adc8-4924-ba7d-467608d06fec.png)\r\n\r\nNow there are three behavior options\r\n- `Default`: when it unchecks after sending the first message\r\n\r\n\r\n- `Always`: stay checked for all messages\r\n\r\n\r\n- `Never`: stay unchecked for all messages\r\n", + "milestone": "5.0.0", + "contributors": [ + "dougfabris", + "ggazzo" + ] + }, + { + "pr": "26008", + "title": "Regression: Webhook Integration Creation + string error toast msg", + "userLogin": "dudanogueira", + "milestone": "5.0.0", + "contributors": [ + "dudanogueira", + "debdutdeb", + "web-flow" + ] + }, + { + "pr": "25958", + "title": "Chore: convert e2e to ts", + "userLogin": "felipe-rod123", + "description": "Converted the `apps/meteor/app/api/server/v1/e2e.js` to ts and created endpoint typings on the `packages/rest-typings/src/v1/e2e` folder.", + "contributors": [ + "felipe-rod123", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25928", + "title": "Regression: Room Endpoint Call Issues", + "userLogin": "LucianoPierdona", + "description": "This PR fixes small management bugs related with channels, rooms and teams", + "milestone": "5.0.0", + "contributors": [ + "LucianoPierdona" + ] + }, + { + "pr": "25984", + "title": "Chore: Fixes e2e playwright intermittences", + "userLogin": "souzaramon", + "contributors": [ + "souzaramon", + "ggazzo", + "weslley543" + ] + }, + { + "pr": "26004", + "title": "Chore: Fuselage update", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti" + ] + }, + { + "pr": "25963", + "title": "Chore: Small fix on callProvider", + "userLogin": "tiagoevanp", + "contributors": [ + "tiagoevanp", + "ggazzo" + ] + }, + { + "pr": "26000", + "title": "[FIX] Initial members value on Create Channel Modal", + "userLogin": "dougfabris", + "description": "#### before\r\n![Screen Shot 2022-06-24 at 11 58 22](https://user-images.githubusercontent.com/27704687/175562315-221dbc9a-5695-4259-a8f7-644e2ff0ab36.png)\r\n\r\n#### after\r\n![Screen Shot 2022-06-24 at 11 59 38](https://user-images.githubusercontent.com/27704687/175562510-a4a6be49-bbd2-4aeb-aedb-a5a7a6f1159d.png)", + "milestone": "5.0.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25756", + "title": "Chore: Migrate LivechatVisitors model to raw", + "userLogin": "KevLehman", + "milestone": "5.0.0", + "contributors": [ + "KevLehman", + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "25994", + "title": "Chore: VoIP Context", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25983", + "title": "Chore: Fuselage update", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti", + "ggazzo" + ] + }, + { + "pr": "25987", + "title": "[FIX] sidebar colors", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti", + "ggazzo" + ] + }, + { + "pr": "25990", + "title": "Regression: Non-reactive routes", + "userLogin": "tassoevan", + "description": "When `Tracker.autorun()` calls are nested, it's possible that an invalidation at the parent render the children non-reactive due to synchronous calls. To avoid that under the callback given by `useSyncExternalStore`, we schedule an `onStoreChange` callback call to not make it reside at the same backtrace.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "25982", + "title": "[BREAK] use urlParams on omnichannel/agent/extension/", + "userLogin": "MartinSchoeler", + "contributors": [ + "MartinSchoeler", + "ggazzo" + ] + }, + { + "pr": "25925", + "title": "[FIX] toolbox menu behind thread component", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "25475", + "title": "[FIX] Sort by scope or creation date not working on canned responses list", + "userLogin": "KevLehman", + "milestone": "5.0.0", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25969", + "title": "Chore: Colors", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti", + "ggazzo" + ] + }, + { + "pr": "25980", + "title": "Revert \"[BREAK] use urlParams on omnichannel/agent/extension/\"", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25874", + "title": "[BREAK] use urlParams on omnichannel/agent/extension/", + "userLogin": "MartinSchoeler", + "milestone": "5.0.0", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "25974", + "title": "Regression: Fix e2e CI", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25964", + "title": "Chore: Update poplib", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25961", + "title": "Regression: Set `offset` and `count` optional on `ChatGetThreadsListSchema`", + "userLogin": "LucianoPierdona", + "contributors": [ + "LucianoPierdona" + ] + }, + { + "pr": "25758", + "title": "Chore: Model Typings", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "25962", + "title": "Chore: Introduce Modal Region", + "userLogin": "dougfabris", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "11744", + "title": "[NEW] Accept quoted slash command arguments", + "userLogin": "Hudell", + "milestone": "5.0.0", + "contributors": [ + "pierre-lehnen-rc", + "Hudell", + "web-flow", + "d-gubert", + "sampaiodiego" + ] + }, + { + "pr": "25956", + "title": "Chore: convert import.js endpoints to TS", + "userLogin": "felipe-rod123", + "description": "Converted the `apps/meteor/app/api/server/v1/import.js` to ts and created endpoint typings on the `packages/rest-typings/src/v1/import` folder.", + "contributors": [ + "felipe-rod123" + ] + }, + { + "pr": "25626", + "title": "[NEW] Colors Palette - Buttons", + "userLogin": "juliajforesti", + "contributors": [ + null, + "juliajforesti", + "ggazzo" + ] + }, + { + "pr": "25672", + "title": "Chore: Upgrade and remove unnecessary Livechat dependencies", + "userLogin": "tiagoevanp", + "contributors": [ + "tiagoevanp", + "ggazzo" + ] + }, + { + "pr": "25739", + "title": "[NEW] Marketplace security tab app info page", + "userLogin": "rique223", + "description": "Created a new security tab for installed apps that displays information related to the given app security policies, terms of services, and necessary permissions for the use of the app.\r\nDemo gif:\r\n![privacy-tab](https://user-images.githubusercontent.com/43561537/173878394-333057d4-3c7e-434e-a3ca-d3e08f33c7bc.gif)", + "contributors": [ + "rique223" + ] + }, + { + "pr": "25950", + "title": "Regression: Fix blackscreen after app install", + "userLogin": "rique223", + "description": "Fixed an error where the client screen would go black after installing an app. This was hapenning because the handleAppAddedOrUpdated function from the AppsProvider had a wrong type for the return of the getAppFromMarketplace function.\r\n\r\nDemo gifs:\r\n\r\nBefore\r\n![app-install-error-before](https://user-images.githubusercontent.com/43561537/174861410-024dff74-b5d9-49ba-ae67-849f1ff239a9.gif)\r\n\r\nAfter:\r\n![app-install-error-after](https://user-images.githubusercontent.com/43561537/174861448-58b071e6-8e1b-4391-b49a-44b68249acbf.gif)", + "contributors": [ + "rique223" + ] + }, + { + "pr": "25907", + "title": "Chore: Improve CI cache", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "25876", + "title": "Regression: Re-add view logs button", + "userLogin": "rique223", + "description": "Re-added the view logs button to the appMenu component so that the user can go directly from the marketplace list of apps to the app info page with the logs tab already open.\r\nDemo gif:\r\n![re-add-view-logs-button](https://user-images.githubusercontent.com/43561537/173681990-86c8a93c-bb2e-4540-824d-b7fbb3161356.gif)", + "contributors": [ + "rique223" + ] + }, + { + "pr": "25920", + "title": "Chore: `@rocket.chat/favicon`", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "25947", + "title": "[FIX] VOIP CallContext snapshot infinite loop", + "userLogin": "hugocostadev", + "description": "The application was crashing due to an error on the `useCallerInfo()` hook.\r\nThe error was: \r\n![image](https://user-images.githubusercontent.com/20212776/174823914-4832e5dd-c91a-4ae4-9d1f-1b960bcd372c.png)\r\n![image](https://user-images.githubusercontent.com/20212776/174823982-cb543fe0-663f-4530-bb94-0720653ca897.png)\r\n\r\nTo prevent this issue to happen it was added a cached and out-of-scope snapshot variable to the hook using `useSyncExternalStore`", + "contributors": [ + "hugocostadev", + "web-flow" + ] + }, + { + "pr": "25931", + "title": "Regression: Docker image publish", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25936", + "title": "Revert: \"Chore: Collect e2e coverage\"", + "userLogin": "souzaramon", + "contributors": [ + "souzaramon", + "web-flow" + ] + }, + { + "pr": "25743", + "title": "Chore: Collect e2e coverage", + "userLogin": "souzaramon", + "contributors": [ + "souzaramon" + ] + }, + { + "pr": "25923", + "title": "Regression: Unable to edit user details via admin panel", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "25871", + "title": "[FIX] Members selection field on creating team modal", + "userLogin": "dougfabris", + "description": "- Fix: add members breaking when searching users\r\n\r\n![image](https://user-images.githubusercontent.com/27704687/121788070-b792f700-cba0-11eb-92b9-5833e1213c74.png)", + "milestone": "5.0.0", + "contributors": [ + "pierre-lehnen-rc", + "dougfabris" + ] + }, + { + "pr": "25911", + "title": "Chore: Remove Imperative Modal from context ", + "userLogin": "MartinSchoeler", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "25915", + "title": "Chore: Keep the option to run only the meteor app", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "25873", + "title": "[FIX] Update chartjs usage to v3", + "userLogin": "KevLehman", + "milestone": "5.0.0", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25830", + "title": "Chore: Rewrite AddUsers to TS", + "userLogin": "csuadev", + "contributors": [ + "csuadev", + "ggazzo", + "web-flow", + "kodiakhq[bot]", + "dougfabris", + "yash-rajpal" + ] + }, + { + "pr": "25909", + "title": "Chore: Replace `useSubscription` with `useSyncExternalStore`", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "25556", + "title": "Chore: Run tests on docker", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "25914", + "title": "Chore: Convert RoomMenu", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti" + ] + }, + { + "pr": "25868", + "title": "[NEW] Create Team with a member list of usernames", + "userLogin": "pierre-lehnen-rc", + "milestone": "5.0.0", + "contributors": [ + "pierre-lehnen-rc", + "dougfabris" + ] + }, + { + "pr": "25754", + "title": "Chore: Convert apps/meteor/client/sidebar/search", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti", + "ggazzo" + ] + }, + { + "pr": "25747", + "title": "Chore: Split useUserInfoActions into small hooks", + "userLogin": "dougfabris", + "milestone": "5.0.0", + "contributors": [ + "dougfabris", + "gabriellsh" + ] + }, + { + "pr": "25910", + "title": "Chore: Watch for package changes", + "userLogin": "gabriellsh", + "description": "With the current `dev` pipeline, whenever we modify a package (e.g. `api-client`), we have to kill the meteor proccess and run `yarn dev` again in order for the changes to be compiled and the new output to be used by meteor.\r\n\r\nThis has the drawback of taking a little longer to run the dev environment, since we can't cache a watched buid. In the other hand, it reduces the friction of modifying internal packages since we don't need to rebuild the project for changes to take effect.\r\n\r\nThis will enable us to move more things to separate packages without affecting the dev experience too much.", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25358", + "title": "Chore: Convert assets endpoint to Typescript", + "userLogin": "MarcosSpessatto", + "contributors": [ + "MarcosSpessatto", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25635", + "title": "Chore: Convert users endpoints ", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow", + "tassoevan", + "felipe-rod123" + ] + }, + { + "pr": "25891", + "title": "[FIX] Settings not being overwritten to their default values", + "userLogin": "sampaiodiego", + "milestone": "4.8.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25872", + "title": "[FIX] Update import from `csv-parse`", + "userLogin": "LucianoPierdona", + "description": "This PR updates the importing of `csv-parse` because the used method wasn't working anymore, we were receiving the following error:\r\n\r\n`error: \"this.csvParser is not a function\"`", + "contributors": [ + "LucianoPierdona" + ] + }, + { + "pr": "25893", + "title": "Regression: TOTP Modal with new rest api package", + "userLogin": "yash-rajpal", + "milestone": "5.0.0", + "contributors": [ + "yash-rajpal", + "ggazzo" + ] + }, + { + "pr": "25884", + "title": "Chore: create a e2e test guideline", + "userLogin": "souzaramon", + "contributors": [ + "souzaramon" + ] + }, + { + "pr": "25870", + "title": "Chore: Fix correct unit test to api files", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25840", + "title": "Chore: convert apps/meteor/app/api/server/lib/ files to TS", + "userLogin": "felipe-rod123", + "description": "This pull request converts files on `apps/meteor/app/api/server/lib/` to Typescript.", + "contributors": [ + "felipe-rod123", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25869", + "title": "[FIX] Kebab menu clicking issue ", + "userLogin": "dougfabris", + "milestone": "5.0.0", + "contributors": [ + "dougfabris", + "tassoevan" + ] + }, + { + "pr": "25887", + "title": "Regression: Fix extended sidebar item", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "dougfabris" + ] + }, + { + "pr": "25690", + "title": "Chore: Translate admin helpers to TS", + "userLogin": "rique223", + "contributors": [ + "rique223", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "25503", + "title": "Chore: convert communication methods to Typescript", + "userLogin": "felipe-rod123", + "description": "Convert files from `apps/meteor/app/apps/server/communication/` to ts.", + "contributors": [ + "felipe-rod123", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25867", + "title": "Chore: update pageobjects to use es6 getters and remove export default", + "userLogin": "souzaramon", + "contributors": [ + "souzaramon" + ] + }, + { + "pr": "25016", + "title": "[BREAK] Deactivated team members are added to auto-join rooms", + "userLogin": "matheusbsilva137", + "description": "- Do not add deactivated users to auto-join rooms.", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "25714", + "title": "Chore: Broken Storybook", + "userLogin": "tiagoevanp", + "description": "There is another small improvement on the way we got storybook files.", + "contributors": [ + "tiagoevanp", + "tassoevan", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "25603", + "title": "[FIX] User avatar reseting and getting random image", + "userLogin": "guijun13", + "description": "- fixes user avatar not being saved after editing the user profile issue\r\n- fixes user avatar not getting another user picture due to database deletion error", + "contributors": [ + "guijun13", + "filipemarins", + "matheusbsilva137" + ] + }, + { + "pr": "25863", + "title": "Chore: Remove old rest api code", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25719", + "title": "Chore(deps): Bump sharp from 0.30.4 to 0.30.6", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "25634", + "title": "Chore: Convert sidebar/item", + "userLogin": "jeanfbrito", + "contributors": [ + "jeanfbrito", + "ggazzo" + ] + }, + { + "pr": "25858", + "title": "Chore: Rewrite RoomWithData", + "userLogin": "dougfabris", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25860", + "title": "Chore: remove unused locators from e2e tests", + "userLogin": "souzaramon", + "contributors": [ + "souzaramon" + ] + }, + { + "pr": "25410", + "title": "[FIX] fixes HTML sanitizing error.", + "userLogin": "MartinSchoeler", + "description": "If the user sent a HTML message over our product to a livechat user the HTML would get rendered on the message box, this prevents it from happening.", + "milestone": "5.0.0", + "contributors": [ + "MartinSchoeler", + "dougfabris", + "cauefcr" + ] + }, + { + "pr": "25698", + "title": "Chore: Rewrite Admin UsersTable to Typescript", + "userLogin": "dougfabris", + "milestone": "5.0.0", + "contributors": [ + "dougfabris", + "tassoevan", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "25813", + "title": "Chore: add _id and name options to JSON Schemas", + "userLogin": "felipe-rod123", + "description": "This pull request adds the `roomId` and `roomName` options for the Ajv JSON Schemas on the `packages/rest-typings/src/v1/channels/` and `packages/rest-typings/src/v1/dm/` folders.", + "contributors": [ + "felipe-rod123", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "25831", + "title": "[BREAK] Chore: Remove unused tokenpass integration code ", + "userLogin": "pierre-lehnen-rc", + "milestone": "5.0.0", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "25842", + "title": "Chore: Fix version on develop branch ", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25632", + "title": "Chore: RouteGroup for My Account sidebar ", + "userLogin": "yash-rajpal", + "description": "Refactoring My Accounts routes. Allows to add \"my account\" routes for EE.", + "milestone": "5.0.0", + "contributors": [ + "yash-rajpal", + "dougfabris" + ] + }, + { + "pr": "25713", + "title": "[FIX] Attachments and OEmbed margins", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25835", + "title": "Chore: Typescript Sidebar RoomList", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25835", + "title": "Chore: Typescript Sidebar RoomList", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25835", + "title": "Chore: Typescript Sidebar RoomList", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25768", + "title": "[FIX] Client-generated sort parameters in channel directory ", + "userLogin": "BenWiederhake", + "contributors": [ + "BenWiederhake", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25637", + "title": "Chore: Add tests for agents screens", + "userLogin": "weslley543", + "contributors": [ + "weslley543", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25827", + "title": "Chore: Notification Preferences to TS", + "userLogin": "yash-rajpal", + "description": "- Notifications Preferences to TS.\r\n- Fix broken save action.", + "contributors": [ + "yash-rajpal", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "25572", + "title": "Chore: Convert MemoizedSetting, Setting, Section", + "userLogin": "juliajforesti", + "contributors": [ + null, + "juliajforesti", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "25834", + "title": "Regression: Fix users.create call", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25829", + "title": "Chore: Add auto label and improve Kodiak configuration", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25824", + "title": "Regression: Fix apps wrong typing", + "userLogin": "rique223", + "contributors": [ + "rique223", + "ggazzo" + ] + }, + { + "pr": "23426", + "title": "Chore: Remove compose from main repo", + "userLogin": "debdutdeb", + "contributors": [ + "debdutdeb" + ] + }, + { + "pr": "25733", + "title": "[FIX] `You and @yourUsername reacted with`title on reactions", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25820", + "title": "[FIX] AgentsPage pagination", + "userLogin": "tiagoevanp", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "25160", + "title": "Chore: Move voip's Wrap-up and On-hold functionality to EE (Backend)", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98", + "web-flow" + ] + }, + { + "pr": "25750", + "title": "[FIX] Access issue on chat.getThreadsList", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "25819", + "title": "Chore: Remove snap files from Houston config", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "25783", + "title": "[FIX] Voip endpoint permissions", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "25451", + "title": "[FIX] allow only livechat-agents to be contact manager for any omnichannel contact ", + "userLogin": "murtaza98", + "milestone": "5.0.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "25810", + "title": "Chore: use params instead of URL building on livechat endpoints", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25809", + "title": "Regression: fix apps path", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25774", + "title": "[BREAK] Remove RDStation integration", + "userLogin": "KevLehman", + "milestone": "5.0.0", + "contributors": [ + "KevLehman", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25469", + "title": "Chore: RestApiClient as Package", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25723", + "title": "[FIX] Wrong argument name preventing Omnichannel Chat Forward to User ", + "userLogin": "dudanogueira", + "milestone": "4.8.1", + "contributors": [ + "dudanogueira" + ] + }, + { + "pr": "25708", + "title": "[FIX] AccountBox checks for condition", + "userLogin": "tiagoevanp", + "milestone": "4.8.1", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "25797", + "title": "Chore: Fix CI", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25781", + "title": "[FIX] Fix prom-client new promise usage", + "userLogin": "KevLehman", + "milestone": "4.8.1", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25788", + "title": "[FIX] Discussion alphabetical ordering", + "userLogin": "hugocostadev", + "description": "Added a validation in the prop used for sorting (loweCaseName) checking for a prop that only exists in discussions (prid)", + "contributors": [ + "hugocostadev" + ] + }, + { + "pr": "25794", + "title": "Chore: Testing Kodiak feature", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25731", + "title": "[FIX] Broken Omnichannel>Agents page", + "userLogin": "tiagoevanp", + "contributors": [ + "tiagoevanp", + "web-flow" + ] + }, + { + "pr": "25744", + "title": "[FIX] Sanitize styles in message", + "userLogin": "yash-rajpal", + "contributors": [ + "yash-rajpal" + ] + }, + { + "pr": "25536", + "title": "Chore: Convert to TS RoomAutoComplete", + "userLogin": "jeanfbrito", + "contributors": [ + "jeanfbrito", + "ggazzo" + ] + }, + { + "pr": "25769", + "title": "Chore: API test on method GET with params as a number.", + "userLogin": "albuquerquefabio", + "milestone": "5.0.0", + "contributors": [ + "albuquerquefabio", + "web-flow" + ] + }, + { + "pr": "25350", + "title": "Chore: convert invites, misc and subscriptions to TS and create definitions", + "userLogin": "felipe-rod123", + "description": "Converted `apps/meteor/app/api/server/v1/invites.js`, `misc.js` and `subscriptions.js` to Typescript and created their endpoint definitions on the rest-typings folder.", + "contributors": [ + "felipe-rod123", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25787", + "title": "Chore: Remove toastr package", + "userLogin": "dougfabris", + "milestone": "5.0.0", + "contributors": [ + "dougfabris", + "ggazzo" + ] + }, + { + "pr": "25649", + "title": "[BREAK] Remove Blockstack authentication", + "userLogin": "tassoevan", + "description": "Blockstack authentication is broken and is preventing some dependencies to be up to date. As a migration to Stacks authentication is not trivial, we've opted for removing the authentication service.", + "milestone": "5.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "25748", + "title": "[FIX] getUserMentionsByChannel method room permission", + "userLogin": "gabriellsh", + "milestone": "5.0.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25583", + "title": "[NEW] Fuselage ToastBar", + "userLogin": "dougfabris", + "description": "![Kapture 2022-05-20 at 14 50 19](https://user-images.githubusercontent.com/27704687/169584462-270e73aa-6dbe-4045-9847-d429125f15a6.gif)", + "milestone": "5.0.0", + "contributors": [ + "dougfabris", + "ggazzo" + ] + }, + { + "pr": "25709", + "title": "[FIX] Thread Message Preview", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan", + "gabriellsh" + ] + }, + { + "pr": "25669", + "title": "[FIX] Bump meteor-node-stubs to version 1.2.3", + "userLogin": "Sh0uld", + "description": "With meteor-node-stubs version 1.2.3 a bug was fixed, which occured in issue #25460 and probably #25513 (last one not tested).\r\nFor the issue in meteor see: https://github.com/meteor/meteor/issues/11974", + "milestone": "4.8.1", + "contributors": [ + "Sh0uld", + "ggazzo" + ] + }, + { + "pr": "25680", + "title": "[IMPROVE] Refactor + unit tests for federation-v2", + "userLogin": "MarcosSpessatto", + "description": "The main goal for this PR is to add the ability to add tests in our current federation-v2 implementation.\r\nIn this PR, I've added only unit tests (80%), but the goal is to add other kinds of tests in the near future.\r\n\r\nAlso, I've created a diagram to show how this refactor was done, and how is the structure of the code\r\n\r\n![image](https://user-images.githubusercontent.com/15324204/171039619-22168000-3626-424e-b408-18dea540f786.png)", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "24796", + "title": "[FIX] user status Offline misnamed as Invisible in Custom Status edit dropdown menu", + "userLogin": "Kunalvrm555", + "milestone": "5.0.0", + "contributors": [ + "Kunalvrm555", + "web-flow", + "debdutdeb", + "dougfabris" + ] + }, + { + "pr": "25761", + "title": "Chore: Messages raw model rewrite to ts", + "userLogin": "debdutdeb", + "contributors": [ + "debdutdeb", + "pierre-lehnen-rc" + ] + }, + { + "pr": "25501", + "title": "Chore: migrate katex to ts", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25751", + "title": "Chore: AutoTranslate contextualBar rewrite", + "userLogin": "dougfabris", + "milestone": "5.0.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25752", + "title": "Chore: Replace AnnouncementModal in favor of GenericModal", + "userLogin": "dougfabris", + "milestone": "5.0.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25753", + "title": "Chore: Keyboard shortcuts contextualBar rewrite", + "userLogin": "dougfabris", + "milestone": "5.0.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25757", + "title": "Chore: Prune Messages contextualBar rewrite", + "userLogin": "dougfabris", + "milestone": "5.0.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25601", + "title": "Chore: add Ajv JSON Schema to api/v1", + "userLogin": "felipe-rod123", + "description": "This pull request adds Ajv JSON Schema validation to `apps/meteor/app/api/server/v1/` and `packages/rest-typings/src/v1/`, where needed.", + "contributors": [ + "felipe-rod123", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25755", + "title": "Chore: Update package.json update tsc memory ", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25749", + "title": "Chore: remove duplicated NotFoundPage.js", + "userLogin": "dougfabris", + "milestone": "5.0.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25630", + "title": "Chore: command's endpoints", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25741", + "title": "Chore: Fix incorrect checksum for agenda package (cause of breaking develop builds)", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "25730", + "title": "Chore: Remove duplicate checksumBehavior key from yarn file", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25393", + "title": "[FIX] Custom emoji reaction size", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins", + "gabriellsh" + ] + }, + { + "pr": "25696", + "title": "Chore: Test for department screen", + "userLogin": "weslley543", + "contributors": [ + "weslley543" + ] + }, + { + "pr": "25697", + "title": "Chore: Taking out Blaze from routes with `MainLayout` ", + "userLogin": "tassoevan", + "description": "While working with @guijun13 on the new homepage I saw we're still rendering a Blaze template even to just embedded components into `MainLayout`. This PR addresses it.", + "milestone": "5.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "25564", + "title": "Chore: Remove all cypress tests, configs and references", + "userLogin": "souzaramon", + "contributors": [ + "souzaramon" + ] + }, + { + "pr": "25612", + "title": "Chore: adjust in some configurations", + "userLogin": "weslley543", + "contributors": [ + "weslley543", + "web-flow" + ] + }, + { + "pr": "25567", + "title": "Chore: migrate-to-pw-16-discussion", + "userLogin": "weslley543", + "contributors": [ + "weslley543" + ] + }, + { + "pr": "25712", + "title": "[FIX] Unnecessary padding on teams channels footer", + "userLogin": "dougfabris", + "description": "#### before\r\n\r\n\r\n### after\r\n", + "milestone": "5.0.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25631", + "title": "[FIX] Messages spacing", + "userLogin": "hugocostadev", + "description": "Adding `sequential` prop to Message component from Fuselage", + "contributors": [ + "hugocostadev", + "gabriellsh" + ] + }, + { + "pr": "25633", + "title": "Chore: Custom Sounds Endpoints", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25682", + "title": "[FIX] User's with non-agent role shown on voip agent association model", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "25667", + "title": "Chore: Convert CreateChannelWithData", + "userLogin": "juliajforesti", + "contributors": [ + null, + "juliajforesti" + ] + }, + { + "pr": "25587", + "title": "Chore: Convert UserAutoCompleteMultiple", + "userLogin": "juliajforesti", + "contributors": [ + null, + "juliajforesti" + ] + }, + { + "pr": "25658", + "title": "Chore: Converting files from app/livechat folder from JS to TS", + "userLogin": "amolghode1981", + "description": "Converting files from apps/meteor/app/livechat/lib/ from JS to TS", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "25581", + "title": "Chore: Convert sidebar/header/actions", + "userLogin": "jeanfbrito", + "contributors": [ + "jeanfbrito", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25665", + "title": "Chore: Converting omnichannel installation files to ts", + "userLogin": "aleksandernsilva", + "description": "This PR converts the omnichannel/installation folder from js to ts", + "contributors": [ + "aleksandernsilva" + ] + }, + { + "pr": "25511", + "title": "Chore: Convert to TS omnichannel/agent", + "userLogin": "jeanfbrito", + "contributors": [ + "jeanfbrito", + "ggazzo" + ] + }, + { + "pr": "25429", + "title": "Chore: Convert components/sidebar to TS", + "userLogin": "jeanfbrito", + "contributors": [ + "jeanfbrito", + "ggazzo" + ] + }, + { + "pr": "25671", + "title": "Chore: Convert apps/meteor/client/sidebar/header/index", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti" + ] + }, + { + "pr": "25666", + "title": "Chore: Migrate some small helper functions to TypeScript", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "25702", + "title": "Merge master into develop & Set version to 5.0.0", + "userLogin": "d-gubert", + "contributors": [ + "d-gubert", + "web-flow", + "felipe-menelau", + "pierre-lehnen-rc", + "tiagoevanp", + "MartinSchoeler", + "ggazzo", + "cauefcr", + "geekgonecrazy" + ] + }, + { + "pr": "25700", + "title": "Chore: Update Apps-Engine and Fuselage", + "userLogin": "d-gubert", + "milestone": "4.8.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "25689", + "title": "Regression: App event listeners broke Slackbridge integration and importers", + "userLogin": "d-gubert", + "description": "Some event listeners triggered by Apps were calling `Meteor.user()` in functions that could run outside of Meteor environment", + "milestone": "4.8.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "25686", + "title": "[FIX] Fix max-width message block", + "userLogin": "ggazzo", + "milestone": "4.8.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25673", + "title": "[FIX] Change form body parameter charset to UTF-8 to fix issue #25456", + "userLogin": "divinespear", + "description": "since [mscdex/busboy](https://github.com/mscdex/busboy) 1.5.0, new option named `defParamCharset` for form body parameter encoding is added with default value `latin1`, so unicode filenames are broken since 4.7.0.\r\n\r\n![Screenshot from 2022-05-28 16-26-06](https://user-images.githubusercontent.com/126630/170815447-1f3bd579-243a-42d3-86f6-814aeaa30ce9.png)", + "milestone": "4.8.0", + "contributors": [ + "divinespear" + ] + }, + { + "pr": "25687", + "title": "Regression: Fix sort field files.list", + "userLogin": "ggazzo", + "milestone": "4.8.0", + "contributors": [ + "ggazzo", + "albuquerquefabio", + "web-flow" + ] + }, + { + "pr": "25684", + "title": "[IMPROVE] add warnings for federation setup", + "userLogin": "carlosrodrigues94", + "contributors": [ + "carlosrodrigues94" + ] + }, + { + "pr": "25683", + "title": "[FIX] Prevent federation crash on invite users as a non-owner user", + "userLogin": "MarcosSpessatto", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "25653", + "title": "Regression: Broken components on Federation and Engagement dashboards", + "userLogin": "tassoevan", + "description": "For reasons I've no clue, any client import path matching `**/data/**` will not be included in the final bundle, failing silently on transpiling/bundling.", + "milestone": "4.8.0", + "contributors": [ + "tassoevan", + "gabriellsh" + ] + }, + { + "pr": "25663", + "title": "Regression: Update settings groups description", + "userLogin": "dougfabris", + "milestone": "4.8.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25569", + "title": "[FIX] Click to join button Jitsi Call", + "userLogin": "hugocostadev", + "description": "Added `ToolboxProvider` to `MessageListProvider` and fixed actionLink.js open function exec", + "milestone": "4.8.0", + "contributors": [ + "hugocostadev", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "25644", + "title": "Regression: Endpoint types with Ajv Coercing data types", + "userLogin": "albuquerquefabio", + "description": "Ajv Coercing data types should be `true` to accept all kinds of data requested.", + "contributors": [ + "albuquerquefabio" + ] + }, + { + "pr": "25618", + "title": "Regression: Change logic to check if connection is online on unstable networks", + "userLogin": "KevLehman", + "milestone": "4.8.0", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25639", + "title": "Regression: Missing settings group descriptions", + "userLogin": "dougfabris", + "description": "", + "milestone": "4.8.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25648", + "title": "Chore: Rest API query parameters handling", + "userLogin": "ggazzo", + "milestone": "5.0.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25651", + "title": "Regression: VoIp wrap up modal not opening after call disconnect", + "userLogin": "aleksandernsilva", + "description": "This PR fixes a bug preventing the wrap up call modal from being displayed after caller or agent ends the call.", + "milestone": "4.8.0", + "contributors": [ + "aleksandernsilva" + ] + }, + { + "pr": "25638", + "title": "[FIX] Remove 'total' text in admin info page", + "userLogin": "guijun13", + "description": "- Remove initial 'total' text from rooms and messages groups in the admin info page\r\n- Add 'total' before 'rooms' and 'messages' title on the same section. To use the new 'Total Rooms', was created a new key in the en.i18n.json file.", + "contributors": [ + "guijun13" + ] + }, + { + "pr": "25641", + "title": "Chore: Increase performance and security of integrations’ scripts", + "userLogin": "rodrigok", + "description": "Replace internal VM implementation with VM2 which implements many more mechanisms to ensure timeout, security and allow easier configuration for future improvements on the integrations' feature.", + "contributors": [ + "rodrigok", + "ggazzo" + ] + }, + { + "pr": "25613", + "title": "[FIX] Quote message spacing", + "userLogin": "hugocostadev", + "contributors": [ + "hugocostadev" + ] + }, + { + "pr": "25629", + "title": "Regression: Assets & Slack Bridge Setting Page not rendering", + "userLogin": "dougfabris", + "milestone": "4.8.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25627", + "title": "Regression: Subscription menu not appearing for non installed but subscribed apps", + "userLogin": "rique223", + "description": "Fixed a problem on which the AppMenu component did not appear for apps that had an active subscription but weren't installed, now the rendering of the component is also based on the isSubscribed flag, and the appearance of the uninstall and enable/disable options are based on the app.installed flag so that the correct options appear on all the edge cases.\r\nDemo gif:\r\n![subscription-manager-fix](https://user-images.githubusercontent.com/43561537/170132040-dc8535c0-8056-4fb2-b008-afaece744868.gif)", + "milestone": "4.8.0", + "contributors": [ + "rique223" + ] + }, + { + "pr": "25521", + "title": "Chore: Rewrite im and dm endpoints to ts", + "userLogin": "albuquerquefabio", + "description": "- Endpoints rewritten to TS\r\n - dm.create\r\n - dm.delete\r\n - dm.close\r\n - dm.counters\r\n - dm.files\r\n - dm.history\r\n - dm.members\r\n - dm.messages\r\n - dm.messages.others\r\n - dm.list\r\n - dm.list.everyone\r\n - dm.open\r\n - dm.setTopic\r\n - im.create\r\n - im.delete\r\n - im.close\r\n - im.counters\r\n - im.files\r\n - im.history\r\n - im.members\r\n - im.messages\r\n - im.messages.others\r\n - im.list\r\n - im.list.everyone\r\n - im.open\r\n - im.setTopic\r\n- Some lines of code was refactored on `apps/meteor/app/api/server/v1/im.ts`\r\n- Unnecessary functions were deleted on `apps/meteor/app/lib/server/functions/getDirectMessageByNameOrIdWithOptionToJoin.ts`\r\n- New types was added on `apps/meteor/app/api/server/api.d.ts`", + "contributors": [ + "albuquerquefabio", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25617", + "title": "Chore: Update Apps-Engine version", + "userLogin": "d-gubert", + "milestone": "4.8.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "25616", + "title": "[FIX] Message menu dropdown not working on Mobile Web", + "userLogin": "gabriellsh", + "milestone": "4.8.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25615", + "title": "[FIX] Fixing app contextual bar functionality", + "userLogin": "AllanPazRibeiro", + "milestone": "4.8.0", + "contributors": [ + "AllanPazRibeiro" + ] + }, + { + "pr": "25499", + "title": "[NEW] New button for network outage", + "userLogin": "amolghode1981", + "description": "When network outage happens it should be conveyed to the user with special icon. This icon should not be clickable.\r\nNetwork outage handling is handled in https://app.clickup.com/t/245c0d8 task.", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "24711", + "title": "[NEW] Marketplace new app details page", + "userLogin": "rique223", + "description": "Change the app details page layout for the new marketplace UI. General Task: [MKP12 - New UI - App Detail Page](https://app.clickup.com/t/1na769h)\r\n\r\n## [MKP12 - Tab Navigation](https://app.clickup.com/t/2452f5u)\r\nNew tab navigation layout for the app details page. Now the app details page is divided into three sections, details, logs, and settings, that can each be accessed through a Tabs fuselage component.\r\n\r\nDemo gif:\r\n![tab_navigation_demo_gif](https://user-images.githubusercontent.com/43561537/157276436-3dab34c5-20da-4f5d-99d0-54c1c718ac1f.gif)\r\n\r\n## [MKP12 - Header](https://app.clickup.com/t/25rhm0x)\r\nImplemented a new header for the marketplaces app details page.\r\n-Changed the size of the app name;\r\n-Implemented the app description field on the header;\r\n-Changed the \"metadata\" section of the header(The part with the version and author information) now it also shows the last time the app was updated;\r\n-Created a chip that will show when an app is part of one or more bundles and inform which are the bundles;\r\n-Implemented a tooltip for the bundle chips;\r\n-Created a new button + data badge component to substitute the current App Status;\r\n-Changed the title of the \"purchase button\". Now it shows different text based on the \"purchase type\" of the app;\r\n-Created a new Pricing & Status display which shows the price when the app is not bought/installed and shows the app status(Enabled/Disabled) when it is bought/installed;\r\n-Changed the way the tabs are rendered, now if the app is not installed(and consequently doesn't have logs and settings tab) it will not render these tabs;\r\n\r\nDemo gif:\r\n![new-header-gif](https://user-images.githubusercontent.com/43561537/159064599-fd64dfe2-86a3-47da-81ba-1e83f1b87432.gif)\r\n\r\n## [MKP12 - Configuration Tab](https://app.clickup.com/t/2452gh4)\r\nDelivered together with the tab-navigation task. Changed the app settings from the details of the app to the new settings tab.\r\nDemo image:\r\n![New configuration tab](https://user-images.githubusercontent.com/43561537/160211324-95db0566-85bf-4dde-a814-3c6f23dcee4d.png)\r\n\r\n## [MKP12 - Log Tab](https://app.clickup.com/t/2452gg1)\r\nChanged the place of the app logs from the page to the new logs tab. Also changed some styles of the logs accordions to fit better with the new container.\r\n\r\nBefore:\r\n![Before](https://user-images.githubusercontent.com/43561537/160210302-148ce584-604f-40ff-8209-141667016163.png)\r\n\r\nAfter\r\n![After](https://user-images.githubusercontent.com/43561537/160210984-d4060c5a-f912-4ef9-87e3-fa459080e2d4.png)\r\n\r\n## [MKP12 - Page Header](https://app.clickup.com/t/29b0b12)\r\nChanged the design for the page header of the app details page from a title on the left with a save and back button on the right to a back arrow icon on the left side of the title with the save button still on the right. Also changed the title of the page from App details to Back.\r\nEdit: After some design reconsideration, the page title was changed to App Info.\r\nDemo gif:\r\n![new_page_header_app_details](https://user-images.githubusercontent.com/43561537/160937741-f5514f70-f43b-4400-8b2f-a5a26f95de9d.gif)\r\n\r\n## [MKP12 - Detail Tab](https://app.clickup.com/t/2452gf7)\r\nImplemented markdown on the description section of the app details page, now the description will show the detailedDescription.rendered (as rendered JSX) information in case it exists and show the description (a.k.a. short description) information in case it doesn't. Unfortunately, as of right now no app has a visual example of a markdown description and because of that, I will not be able to provide a demo image/gif for this PR.\r\n\r\n## [MKP12 - Slider Component](https://app.clickup.com/t/2452h26)\r\nCreated an image carousel component on the app details page. This component receives images from the apps/appId/screenshots endpoint and shows them on the content section of the app details of any apps that have screenshots registered, if the app has no screenshots it simply shows nothing where the carousel should be. This component is complete with keyboard arrow navigation on the \"open\" carousel, hover highlight on the carousel preview and close on esc press.\r\nDemo gif:\r\n![new_carousel_component](https://user-images.githubusercontent.com/43561537/167415212-9d8359c7-4132-4afa-a698-8be4ab1e1393.gif)", + "milestone": "4.8.0", + "contributors": [ + "rique223", + "web-flow", + "ggazzo", + "dougfabris" + ] + }, + { + "pr": "25108", + "title": "[IMPROVE] Unify voip streams into single stream", + "userLogin": "KevLehman", + "milestone": "4.8.0", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25444", + "title": "[FIX] Removing user also removes them from Omni collections", + "userLogin": "cauefcr", + "contributors": [ + "cauefcr", + "web-flow", + "KevLehman" + ] + }, + { + "pr": "25398", + "title": "[FIX] Upgrade tab loader in incorrect position", + "userLogin": "guijun13", + "description": "- Add invisible prop to iframe when loading state is active.", + "milestone": "4.8.0", + "contributors": [ + "guijun13", + "tassoevan" + ] + }, + { + "pr": "25436", + "title": "[NEW] Ability for RC server to check the business hour for a specific department", + "userLogin": "murtaza98", + "milestone": "4.8.0", + "contributors": [ + "murtaza98", + "tiagoevanp" + ] + }, + { + "pr": "25606", + "title": "Chore: Code Improvements for #25391", + "userLogin": "MartinSchoeler", + "milestone": "4.8.0", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "25604", + "title": "[FIX] useCurrentChatTags is not a function", + "userLogin": "MartinSchoeler", + "milestone": "4.8.0", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "25535", + "title": "[FIX] Pinned Message display cutting off information", + "userLogin": "hugocostadev", + "milestone": "4.8.0", + "contributors": [ + "hugocostadev", + "gabriellsh" + ] + }, + { + "pr": "25290", + "title": "Chore: Dependencies upgrade", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25605", + "title": "Chore: bump fuselage", + "userLogin": "dougfabris", + "milestone": "4.8.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25457", + "title": "[NEW] Federation (Alpha Stabilization)", + "userLogin": "alansikora", + "milestone": "4.8.0", + "contributors": [ + "alansikora", + "MarcosSpessatto", + "web-flow", + "geekgonecrazy" + ] + }, + { + "pr": "24519", + "title": "Chore: Convert to typescript some functions from app/lib/server/functions", + "userLogin": "eduardofcabrera", + "description": "Convert to typescript some functions from app/lib/server/functions and transfered theses files to server/lib", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "25329", + "title": "[NEW] Add option to show mentions badge when show counter is disabled", + "userLogin": "marceloschmidt", + "contributors": [ + "marceloschmidt", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "25391", + "title": "[FIX] Fixing Network connectivity issues with SIP client.", + "userLogin": "amolghode1981", + "description": "The previous PR https://github.com/RocketChat/Rocket.Chat/pull/25170 did not handle the issues completely.\r\nThis PR is expected to handle\r\n1. Clearing call related UI when the network is disconnected or switched.\r\n2. Do clean connectivity. There were few issues discovered in earlier implementation. e.g endpoint would randomly\r\nget disconnected after a while. This was due to the fact that the earlier socket disconnection caused the\r\nremoval of contact on asterisk. This should be fixed in this PR.\r\n3. This PR contains a lot of logs. This will be removed before the final merge.", + "milestone": "4.8.0", + "contributors": [ + "amolghode1981" + ] + }, + { + "pr": "25494", + "title": "[FIX] Ordered and unordered list styles, Line breaks.", + "userLogin": "gabriellsh", + "description": "Also removed the message.md cache from server, since changes in the parser might break messages in the future (and will in this specific case).", + "milestone": "4.8.0", + "contributors": [ + "gabriellsh", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "25592", + "title": "Chore: Convert slashCommands to typescript", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "eduardofcabrera", + "ostjen", + "web-flow" + ] + }, + { + "pr": "25514", + "title": "[NEW] Get user's preferred language via apps", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98", + "d-gubert" + ] + }, + { + "pr": "25383", + "title": "[NEW] Star message, report and delete message events", + "userLogin": "tapiarafael", + "contributors": [ + "tapiarafael", + "d-gubert" + ] + }, + { + "pr": "25234", + "title": "[NEW] Add new events after user login, logout and change his status", + "userLogin": "tapiarafael", + "contributors": [ + "tapiarafael", + "d-gubert" + ] + }, + { + "pr": "25337", + "title": "[NEW] Add new app events for pin, react and follow message", + "userLogin": "tapiarafael", + "contributors": [ + "tapiarafael", + "d-gubert" + ] + }, + { + "pr": "25591", + "title": "Chore: Convert AutoTranslate", + "userLogin": "PedroRorato", + "contributors": [ + "PedroRorato" + ] + }, + { + "pr": "25582", + "title": "Chore: Migrate retention-policy to ts", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "24307", + "title": "Chore: Convert to typescript the slash commands help files", + "userLogin": "eduardofcabrera", + "description": "Convert to typescript the slash commands help files", + "contributors": [ + "eduardofcabrera", + "web-flow", + "pierre-lehnen-rc" + ] + }, + { + "pr": "25589", + "title": "Chore: Convert Create Channel", + "userLogin": "juliajforesti", + "contributors": [ + null + ] + }, + { + "pr": "25586", + "title": "Chore: Convert additionalForms", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "MartinSchoeler" + ] + }, + { + "pr": "25425", + "title": "Chore: Rewrite autotranslate to ts", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25165", + "title": "[NEW] Add user events for apps", + "userLogin": "tapiarafael", + "contributors": [ + "tapiarafael", + "d-gubert" + ] + }, + { + "pr": "25283", + "title": "[FIX] Integrations avatar attribute misuse", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "25367", + "title": "Chore: Converting orchestrator.js to ts", + "userLogin": "AllanPazRibeiro", + "contributors": [ + "AllanPazRibeiro" + ] + }, + { + "pr": "25504", + "title": "Chore: convert marketplace price display component to use typescript", + "userLogin": "matheuslc", + "description": "**Marketplace apps listing page**\r\n![Screen Shot 2022-05-13 at 12 57 43](https://user-images.githubusercontent.com/4161171/168322189-67990fdf-a447-46dc-8f88-08b16c2a5416.png)\r\n\r\n**Apps detail page**\r\n![Screen Shot 2022-05-13 at 12 58 56](https://user-images.githubusercontent.com/4161171/168322241-505ee5bb-d3d8-4b0e-8757-873a1a65a6a6.png)", + "contributors": [ + "matheuslc" + ] + }, + { + "pr": "25554", + "title": "Chore: Convert apps/meteor/client/components/UserAutoComplete", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti", + "ggazzo" + ] + }, + { + "pr": "25544", + "title": "[FIX] Initial User not added to default channel", + "userLogin": "geekgonecrazy", + "description": "If injecting initial user. The user wasn’t added to the default General channel", + "milestone": "4.7.2", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "25078", + "title": "[NEW] New stats rewrite", + "userLogin": "ostjen", + "description": "Add the following new statistics (**metrics**):\r\n\r\n- Total users with TOTP enabled;\r\n- Total users with 2FA enabled;\r\n- Total pinned messages;\r\n- Total starred messages;\r\n- Total email messages;\r\n- Total rooms with at least one starred message;\r\n- Total rooms with at least one pinned message;\r\n- Total encrypted rooms;\r\n- Total link invitations;\r\n- Total email invitations;\r\n- Logo change;\r\n- Number of rooms inside teams;\r\n- Number of default (auto-join) rooms inside teams;\r\n- Number of users created through link invitation;\r\n- Number of users created through manual entry;\r\n- Number of imported users (by import type);", + "contributors": [ + "ostjen", + "matheusbsilva137", + "sampaiodiego" + ] + }, + { + "pr": "25565", + "title": "Chore: Convert apps/meteor/client/views/admin/settings", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti" + ] + }, + { + "pr": "25520", + "title": "[FIX] User abandonment setting was not working doe to failing event hook", + "userLogin": "cauefcr", + "description": "A setting watcher and the query for grabbing abandoned chats were broken, now they're not.", + "milestone": "4.7.2", + "contributors": [ + "cauefcr", + "tiagoevanp" + ] + }, + { + "pr": "25558", + "title": "Test: Migrate 13-permissions from cypress to playwright", + "userLogin": "souzaramon", + "contributors": [ + "souzaramon" + ] + }, + { + "pr": "25445", + "title": "[FIX] Add open user card to user avatar", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "25495", + "title": "[FIX] Dynamic load matrix is enabled and handle failure ", + "userLogin": "ggazzo", + "milestone": "4.7.2", + "contributors": [ + "ggazzo", + "geekgonecrazy" + ] + }, + { + "pr": "25409", + "title": "[FIX] One of the triggers was not working correctly", + "userLogin": "MartinSchoeler", + "milestone": "4.7.2", + "contributors": [ + "MartinSchoeler", + "tiagoevanp" + ] + }, + { + "pr": "25555", + "title": "Regression: CI services build", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25381", + "title": "Chore: User set UTC offset", + "userLogin": "albuquerquefabio", + "contributors": [ + "albuquerquefabio", + "web-flow" + ] + }, + { + "pr": "24612", + "title": "[FIX] Rooms' names turn lower case on CSV import", + "userLogin": "guijun13", + "description": "* Change 'Settings' import to not get cached configs\r\n* Remove update `UI_Allow_room_names_with_special_chars` value", + "contributors": [ + "guijun13" + ] + }, + { + "pr": "25542", + "title": "Chore: migrate-to-pw-adjust-in-intermitences", + "userLogin": "weslley543", + "contributors": [ + "weslley543" + ] + }, + { + "pr": "23849", + "title": "[IMPROVE][ENTERPRISE] Allow mapping LDAP groups to multiple RC roles", + "userLogin": "matheusbsilva137", + "description": "- Add support to mapping LDAP groups to multiple roles (by specifying arrays in the \"User Data Group Map\" enterprise setting.", + "contributors": [ + "matheusbsilva137", + "pierre-lehnen-rc" + ] + }, + { + "pr": "25522", + "title": "Chore: Livechat change output level", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25326", + "title": "[NEW] Adding app button on user dropdown", + "userLogin": "AllanPazRibeiro", + "contributors": [ + "AllanPazRibeiro", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "25523", + "title": "Chore: migrate from cypress to pw 14-setting-permission", + "userLogin": "weslley543", + "contributors": [ + "weslley543" + ] + }, + { + "pr": "25253", + "title": "Chore: Tests with Playwright (task: ROC-31, 12-settings)", + "userLogin": "souzaramon", + "contributors": [ + "souzaramon", + "web-flow" + ] + }, + { + "pr": "25462", + "title": "Chore: Migrate 15-message-popup from cypress to playwright", + "userLogin": "souzaramon", + "contributors": [ + "souzaramon" + ] + }, + { + "pr": "25427", + "title": "Chore: Convert apps/meteor/client/views/admin/settings/inputs folder", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti" + ] + }, + { + "pr": "25407", + "title": "[FIX] UI/UX issues on Live Chat widget", + "userLogin": "MartinSchoeler", + "milestone": "4.7.2", + "contributors": [ + "MartinSchoeler", + "dougfabris" + ] + }, + { + "pr": "25348", + "title": "Chore: Convert Admin -> Rooms to TS", + "userLogin": "yash-rajpal", + "contributors": [ + "yash-rajpal", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25509", + "title": "Chore: Migrate NotFoundPage to TS", + "userLogin": "hugocostadev", + "contributors": [ + "hugocostadev" + ] + }, + { + "pr": "25412", + "title": "[FIX] Unable to see channel member list by authorized channel roles", + "userLogin": "hugocostadev", + "contributors": [ + "hugocostadev" + ] + }, + { + "pr": "25519", + "title": "Regression: Fix services-image-build-check", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "25507", + "title": "Chore: Migrate spotify to ts", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25508", + "title": "Chore: Reorder unreleased migrations", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "25471", + "title": "[FIX] Spotlight results showing usernames instead of real names", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.7.1", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "25434", + "title": "[FIX] LDAP sync removing users from channels when multiple groups are mapped to it", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.7.1", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "25413", + "title": "Chore: Move markdown message parser to a `callback`", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25448", + "title": "[FIX] Settings listeners not receiving overwritten values from env vars", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "25246", + "title": "Chore: Move ddp-streamer micro service to its own sub-repo", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25441", + "title": "[NEW] Use setting to determine if initial general channel is needed", + "userLogin": "felipe-menelau", + "description": "- Adds flag responsible for overwriting #general channel creation", + "milestone": "4.7.1", + "contributors": [ + "felipe-menelau", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "25439", + "title": "[IMPROVE] New admin settings Page", + "userLogin": "dougfabris", + "description": "![Screen Shot 2022-05-09 at 11 31 58](https://user-images.githubusercontent.com/27704687/167432811-f4970f23-5dae-48a0-a427-92269d08a859.png)", + "milestone": "4.8.0", + "contributors": [ + "dougfabris", + "ggazzo" + ] + }, + { + "pr": "25473", + "title": "[FIX] Failure to update Integration History index", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "25285", + "title": "Chore: Rewrite 2fa to typescript", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25468", + "title": "Chore: solve yarn issues from env var", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25446", + "title": "Chore: REST query and body params validation", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25416", + "title": "Chore: Tests with Playwright (task: ROC-66, Intermittent resolution in tests)", + "userLogin": "weslley543", + "contributors": [ + "weslley543", + "souzaramon" + ] + }, + { + "pr": "25298", + "title": "Chore: Convert email inbox feature to TypeScript", + "userLogin": "ujorgeleite", + "contributors": [ + "ujorgeleite", + "albuquerquefabio", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "25442", + "title": "Chore: Move admin sidebarItems registration to the main file", + "userLogin": "dougfabris", + "milestone": "4.8.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25449", + "title": "[FIX] Sanitize customUserStatus and fix infinite loop", + "userLogin": "dougfabris", + "description": "### Additional improves:\r\n- usage of RHF to avoid unnecessary Add and Edit components separately and form validation\r\n- usage of `GenericTableV2` and some hooks to avoid unnecessary code\r\n- fix `IUserStatus` type\r\n- improves in UI design\r\n- improves **empty** and **loading** state\r\n- improves files structure\r\n\r\n[LOOP ERROR ATTACHMENT]\r\n![Screen Shot 2022-05-09 at 19 42 53](https://user-images.githubusercontent.com/27704687/167510439-1980461c-a885-46d2-9a49-79da432c7521.png)", + "milestone": "4.8.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25318", + "title": "[IMPROVE] Fix multiple bugs with Matrix bridge", + "userLogin": "MarcosSpessatto", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "25265", + "title": "Chore: Convert `UserStatusMenu` to TS", + "userLogin": "debdutdeb", + "contributors": [ + "debdutdeb", + "tassoevan" + ] + }, + { + "pr": "25443", + "title": "Chore: Chore add validation option to rest endpoints", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25279", + "title": "Chore: Add channel endpoints (rest-typings)", + "userLogin": "debdutdeb", + "contributors": [ + "debdutdeb", + "ggazzo" + ] + }, + { + "pr": "25432", + "title": "Chore: Dedicated package for UI contexts", + "userLogin": "tassoevan", + "description": "Moving our React contexts to a different package on the monorepo enable us to deliver components from another packages, because they work as a loose connection to the core APIs.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "25424", + "title": "Chore: Convert RoomForeword, TextCopy and RoomAvatarEditor to TS", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25418", + "title": "Chore: Rewrite action-links to ts", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25421", + "title": "Chore: Rewrite mail-messages to ts", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25430", + "title": "Chore: Convert useUpdateAvatar to TS and type avatar endpoints", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25423", + "title": "[FIX] Change NPS Vote identifier + nps index to unique", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "22374", + "title": "[IMPROVE] Pass allowDiskUse to channel aggregations on engagement dashboard", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25431", + "title": "Chore: Manager Page Rewrite", + "userLogin": "MartinSchoeler", + "contributors": [ + "MartinSchoeler", + "ggazzo" + ] + }, + { + "pr": "25426", + "title": "Chore: Convert useFileInput to TS", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25420", + "title": "Chore: convert info to typescript", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "25395", + "title": "Chore: Enable marketplace screenshots endpoint", + "userLogin": "matheuslc", + "contributors": [ + "matheuslc", + "web-flow" + ] + }, + { + "pr": "25312", + "title": "Chore: Add Livechat repo into Monorepo packages", + "userLogin": "tiagoevanp", + "milestone": "4.7.2", + "contributors": [ + "tiagoevanp", + "ggazzo", + "web-flow", + "MartinSchoeler" + ] + }, + { + "pr": "25303", + "title": "Chore: Rewrite Jitsi Contextualbar to TS", + "userLogin": "dougfabris", + "milestone": "4.8.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25372", + "title": "Chore: Convert AdminSideBar to ts", + "userLogin": "jeanfbrito", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25347", + "title": "Chore: Convert push endpoints to TS", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25397", + "title": "Chore: Add client folder to CODEOWNERS ", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25394", + "title": "Chore: Update Volta configuration", + "userLogin": "tassoevan", + "description": "[Volta](https://volta.sh/) need some extra configuration to work on monorepos.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "25359", + "title": "Chore: Rewrite some Omnichannel files to TypeScript", + "userLogin": "tiagoevanp", + "description": "apps/meteor/client/components/Omnichannel/modals/*\r\napps/meteor/client/components/Omnichannel/Tags.js", + "contributors": [ + "tiagoevanp", + "ggazzo" + ] + }, + { + "pr": "25288", + "title": "Chore: Convert customUserStatus folder to ts", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti" + ] + }, + { + "pr": "25343", + "title": "Chore: Convert federationDashboard folder to ts", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti" + ] + }, + { + "pr": "25252", + "title": "Chore: Tests with Playwright (task: ROC-25, 06-message)", + "userLogin": "weslley543", + "contributors": [ + "weslley543", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "25345", + "title": "Chore: Convert client/views/admin/settings/groups folder to ts", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti" + ] + }, + { + "pr": "25342", + "title": "Chore: Convert getStatistics", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25276", + "title": "Chore: Add typings for /v1/webdav.getMyAccounts", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25274", + "title": "Chore: Convert customSounds folder to ts", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti", + "dougfabris", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25277", + "title": "Chore: Convert Admin/OAuthApps to TS", + "userLogin": "yash-rajpal", + "description": "- Converts Admin/OAuthApps to TS.\r\n- migrated forms to react-hook-form", + "contributors": [ + "yash-rajpal", + "felipe-rod123", + "ggazzo" + ] + }, + { + "pr": "25278", + "title": "Chore: Add /v1/video-conference endpoint types", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "pierre-lehnen-rc", + "ggazzo" + ] + }, + { + "pr": "25380", + "title": "Regression: Fix clicking on visitor's chat in the sidebar does not display the chat window", + "userLogin": "filipemarins", + "description": "Fix: livechat room not opening.", + "milestone": "4.7.0", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "25314", + "title": "Regression: Fix size of custom emoji and render emoji on thread message preview", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins", + "gabriellsh" + ] + }, + { + "pr": "25371", + "title": "Chore: Bump fuselage", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25336", + "title": "Chore: Add options to debug stdout and rate limiter", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25368", + "title": "Regression: Fix English i18n react text", + "userLogin": "d-gubert", + "description": "Incorrect text in reaction tooltip has been fixed", + "milestone": "4.7.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "25349", + "title": "Regression: Rocket.Chat Webapp not loading.", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc", + "gabriellsh" + ] + }, + { + "pr": "25317", + "title": "Regression: Fix multi line is not showing an empty line between lines", + "userLogin": "filipemarins", + "milestone": "4.7.0", + "contributors": [ + "filipemarins", + "gabriellsh" + ] + }, + { + "pr": "25320", + "title": "Regression: bump onboarding-ui version", + "userLogin": "guijun13", + "description": "- Bump to 'next' the onboarding-ui package from fuselage.\r\n- Update from 'companyEmail' to 'email' adminData usage types", + "contributors": [ + "guijun13" + ] + }, + { + "pr": "25335", + "title": "Chore: Create README.md for Rest Typings", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25327", + "title": "Regression: Messages in new message template Crashing.", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25323", + "title": "Regression: Better MongoDB connection management for micro services", + "userLogin": "sampaiodiego", + "milestone": "4.7.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25250", + "title": "Regression: Validate empty fields for Message template", + "userLogin": "gabriellsh", + "milestone": "4.8.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25319", + "title": "Regression: Fix the alpine image and dev UX installing matrix-rust-sdk-bindings", + "userLogin": "geekgonecrazy", + "description": "The package only included a few pre-built which caused all macs to have to compile every time they installed and also caused our alpine not to work.\r\n\r\nThis temporarily switches to a fork of the matrix-appservice-bridge package.\r\n\r\nMade changes to one of its child dependencies `matrix-rust-sdk-bindings` that adds pre-built binaries for mac and musl (for alpine).", + "milestone": "4.7.0", + "contributors": [ + "geekgonecrazy", + "web-flow", + "d-gubert" + ] + }, + { + "pr": "25255", + "title": "Regression: Change preference to be default legacy messages", + "userLogin": "gabriellsh", + "milestone": "4.8.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25306", + "title": "Regression: Fix reply button not working when hideFlexTab is enabled", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins", + "gabriellsh" + ] + }, + { + "pr": "25311", + "title": "Regression: Add eslint package to micro services Dockerfile", + "userLogin": "sampaiodiego", + "milestone": "4.7.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25218", + "title": "Chore: ensure scripts use cross-env and ignore some dirs (ROC-54)", + "userLogin": "souzaramon", + "description": "- data and test-failure should be ignored\r\n- ensure scripts use cross-env", + "contributors": [ + "souzaramon" + ] + }, + { + "pr": "25313", + "title": "Regression: Revert Bugsnag version", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25305", + "title": "Regression: eslint not running on packages", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc", + "ggazzo" + ] + }, + { + "pr": "25299", + "title": "Regression: Add `isPending` status to message", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "25301", + "title": "Regression: Shows error if micro service cannot connect to Mongo", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25287", + "title": "Regression: Use exact Node version on micro services Docker images", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25286", + "title": "Chore: Add root package.json to houston files", + "userLogin": "d-gubert", + "description": "See title", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "25284", + "title": "Chore: Sync with master", + "userLogin": "d-gubert", + "contributors": [ + "sampaiodiego", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "25269", + "title": "Chore: Minor dependency updates", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "25224", + "title": "Chore: Add yarn plugin to check node and yarn version", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25280", + "title": "Chore: Remove package-lock.json from houston files", + "userLogin": "d-gubert", + "description": "Houston config in the `package.json` file still mentioned `package-lock.json`, but it doesn't exist anymore", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "25260", + "title": "[FIX] Adjust email label in Setup Wizard i18n files", + "userLogin": "guijun13", + "description": "- remove 'Company' label on onboarding email keys in certain languages", + "contributors": [ + "guijun13" + ] + }, + { + "pr": "25275", + "title": "Chore: Fix return type warnings", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23870", + "title": "[NEW] Expand Apps Engine's environment variable allowed list", + "userLogin": "cuonghuunguyen", + "milestone": "4.7.0", + "contributors": [ + null, + "debdutdeb", + "web-flow", + "cuonghuunguyen", + "dougfabris" + ] + }, + { + "pr": "25273", + "title": "Regression: Fix federation Matrix bridge startup", + "userLogin": "sampaiodiego", + "milestone": "4.7.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25092", + "title": "[FIX] Message preview not available for queued chats", + "userLogin": "murtaza98", + "milestone": "4.7.0", + "contributors": [ + "murtaza98", + "KevLehman" + ] + }, + { + "pr": "23688", + "title": "[NEW] Alpha Matrix Federation", + "userLogin": "alansikora", + "description": "Experimental support for Matrix Federation with a Bridge\r\n\r\nhttps://user-images.githubusercontent.com/51996/164530391-e8b17ecd-a4d0-4ef8-a8b7-81230c1773d3.mp4", + "milestone": "4.7.0", + "contributors": [ + "alansikora", + "geekgonecrazy", + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "25259", + "title": "Chore: Bump Fuselage packages", + "userLogin": "dougfabris", + "milestone": "4.7.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25261", + "title": "[FIX] Incorrect websocket url in livechat widget", + "userLogin": "debdutdeb", + "milestone": "4.7.0", + "contributors": [ + "debdutdeb" + ] + }, + { + "pr": "25007", + "title": "[FIX] Showing Blank Message Inside Report", + "userLogin": "nishant23122000", + "description": "https://user-images.githubusercontent.com/53515714/161038085-4a86c7ae-6751-4996-9767-b1c9e0331a6c.mp4", + "contributors": [ + "nishant23122000" + ] + }, + { + "pr": "25251", + "title": "Regression: Add select message to system message and thread preview and allow select on legacy template", + "userLogin": "filipemarins", + "milestone": "4.7.0", + "contributors": [ + "filipemarins", + "ggazzo", + "web-flow", + "gabriellsh", + "dougfabris" + ] + }, + { + "pr": "25239", + "title": "[FIX] Add katex render to new message react template", + "userLogin": "filipemarins", + "milestone": "4.7.0", + "contributors": [ + "filipemarins", + "ggazzo", + "dougfabris" + ] + }, + { + "pr": "25257", + "title": "Chore: Update Livechat to the last version", + "userLogin": "tiagoevanp", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "24515", + "title": "[FIX] Custom sound error toast messages", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664", + "dougfabris" + ] + }, + { + "pr": "25211", + "title": "Regression: Avatar not loading on first direct message", + "userLogin": "filipemarins", + "description": "fix avatar not loading on a first direct message", + "milestone": "4.7.0", + "contributors": [ + "filipemarins", + "ggazzo" + ] + }, + { + "pr": "25254", + "title": "Regression: Show username and real name on the message system", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "25217", + "title": "[IMPROVE] Performance for some Omnichannel features", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25200", + "title": "[FIX] room creation fails if app framework is disabled", + "userLogin": "debdutdeb", + "milestone": "4.7.0", + "contributors": [ + "debdutdeb" + ] + }, + { + "pr": "24565", + "title": "[IMPROVE] Add OTR Room States", + "userLogin": "yash-rajpal", + "description": "Earlier OTR room uses only 2 states, we need more states to support future features. \r\nThis adds more states for the OTR contextualBar.\r\n\r\n- Expired\r\n\"Screen\r\n\r\n- Declined\r\nScreen Shot 2022-04-20 at 13 49 28\r\n\r\n- Error\r\n\"Screen", + "milestone": "4.7.0", + "contributors": [ + "yash-rajpal", + "dougfabris" + ] + }, + { + "pr": "25170", + "title": "[FIX] Client disconnection on network loss", + "userLogin": "amolghode1981", + "description": "Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online\r\nunless agent explicitly logs off.\r\nAgent can stop receiving calls in multiple ways due to network loss. Network loss can happen in following ways.\r\n1. User tries to switch the network. User experiences a glitch of disconnectivity. This can be simulated by turning the network off\r\nin the network tab of chrome's dev tool. This can disconnect the UA if the disconnection happens just before the registration refresh.\r\n2. Second reason is when computer goes in sleep mode.\r\n3. Third reason is that when asterisk is crashed/in maintenance mode/explicitly stopped.\r\n\r\nSolution:\r\nThe idea is to detect the network disconnection and start the start the attempts to reconnect.\r\nThe detection of the disconnection does not happen in case#1. The SIPUA's UserAgent transport does not\r\ncall onDisconnected when network loss of such kind happens. To tackle this problem, window's online and offline event handlers are\r\nused.\r\n\r\nThe number of retries is configurable but ideally it is to be kept at -1. Whenever disconnection happens, it should keep on trying to\r\nreconnect with increasing backoff time. This behaviour is useful when the asterisk is stopped.\r\n\r\nWhen the server is disconnected, it should be indicated on the phone button.", + "contributors": [ + "amolghode1981", + "KevLehman" + ] + }, + { + "pr": "25244", + "title": "[FIX] Read receipts show with color gray when not read yet", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins", + "gabriellsh" + ] + }, + { + "pr": "25230", + "title": "[FIX] VoIP disabled/enabled sequence puts voip agent in error state", + "userLogin": "amolghode1981", + "description": "Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side)\r\n\r\nIt was realised that the race does not happen because of the database or server noticing the changes late. But because of the time taken to establish the AMI connection with Asterisk.\r\n\r\nSolution:\r\n\r\n1. Change apps/meteor/app/voip/server/startup.ts. When VoIP_Enabled is changed, await for Voip.init() to complete and then broadcast connector.statuschanged with changed value.\r\n2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance.\r\n3. in apps/meteor/client/providers/CallProvider/hooks/useVoipClient.ts add the event handler that receives this event. Change voipEnabled from constant to state. Change this state based on the 'value' that is received by the handler.", + "contributors": [ + "amolghode1981", + "KevLehman" + ] + }, + { + "pr": "25087", + "title": "[NEW] Add expire index to integration history", + "userLogin": "geekgonecrazy", + "milestone": "4.7.0", + "contributors": [ + "geekgonecrazy" + ] + }, + { + "pr": "24521", + "title": "Chore: update OTR icon", + "userLogin": "kibonusp", + "description": "I changed the shredder icon in OTR contextual bar to the stopwatch icon, recently added to the fuselage.", + "milestone": "4.7.0", + "contributors": [ + "kibonusp", + "yash-rajpal", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "25237", + "title": "[FIX] Toolbox hiding under contextual bar", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25231", + "title": "[IMPROVE] Added MaxNickNameLength and MaxBioLength constants", + "userLogin": "aakash-gitdev", + "contributors": [ + "aakash-gitdev", + "web-flow", + "gabriellsh" + ] + }, + { + "pr": "25220", + "title": "[FIX] Desktop notification on multi-instance environments", + "userLogin": "sampaiodiego", + "milestone": "4.6.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25175", + "title": "[FIX] Reply button behavior on broadcast channel", + "userLogin": "filipemarins", + "description": "Hide reply button for the user that sent the message", + "contributors": [ + "filipemarins", + "web-flow" + ] + }, + { + "pr": "25216", + "title": "[FIX] Read receipts showing before message read", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "25222", + "title": "[FIX] Add reaction not working in legacy messages", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25223", + "title": "Chore: Add error boundary to message component", + "userLogin": "gabriellsh", + "description": "Not crash the whole application if something goes wrong in the MessageList component.\r\n\r\n![image](https://user-images.githubusercontent.com/40830821/162269915-931c5c3c-c979-4234-b74c-371f67467ce0.png)", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25130", + "title": "Chore: Update Livechat version", + "userLogin": "tiagoevanp", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "25073", + "title": "[FIX] AgentOverview analytics wrong departmentId parameter", + "userLogin": "paulobernardoaf", + "description": "When filtering the analytics charts by department, data would not appear because the object:\r\n```js\r\n{\r\n value: \"department-id\",\r\n label: \"department-name\"\r\n}\r\n```\r\nwas being used in the `departmentId` parameter.\r\n\r\n- Before:\r\n![image](https://user-images.githubusercontent.com/30026625/161832057-d96ffd21-a7dd-421e-bfaa-3b9f4a9127b2.png)\r\n\r\n- After:\r\n![image](https://user-images.githubusercontent.com/30026625/161831092-9ee77b51-b083-4f45-9c48-ab2e0511c4d6.png)", + "milestone": "4.7.0", + "contributors": [ + "paulobernardoaf" + ] + }, + { + "pr": "25056", + "title": "[FIX] Close room when dismiss wrap up call modal", + "userLogin": "tiagoevanp", + "milestone": "4.7.0", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "25208", + "title": "Regression: yarn dev triggers build dependencies", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "24714", + "title": "[FIX] Added invalid password error message", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664", + "dougfabris" + ] + }, + { + "pr": "25196", + "title": "Chore: Tests with Playwright (task: ROC-28, 09-channels)", + "userLogin": "tmontini", + "contributors": [ + "tmontini" + ] + }, + { + "pr": "25174", + "title": "Chore: Template to generate packages", + "userLogin": "ggazzo", + "description": "```\r\nnpx hygen package new test\r\n```", + "contributors": [ + "ggazzo", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "25193", + "title": "Regression: Fix micro services Docker build", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "25180", + "title": "Chore: Remove duplicated useUserRoom", + "userLogin": "dougfabris", + "milestone": "4.7.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25167", + "title": "Chore: TS migration SortList", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "24933", + "title": "[FIX] Deactivating user breaks if user is the only room owner", + "userLogin": "sidmohanty11", + "description": "## Before\r\n\r\nhttps://user-images.githubusercontent.com/73601258/160000871-cfc2f2a5-2a59-4d27-8049-7754d003dd48.mp4\r\n\r\n\r\n\r\n## After\r\nhttps://user-images.githubusercontent.com/73601258/159998287-681ab475-ff33-4282-82ff-db751c59a392.mp4", + "milestone": "4.6.2", + "contributors": [ + "sidmohanty11", + "sampaiodiego" + ] + }, + { + "pr": "25181", + "title": "Regression: Fix services Docker build on CI", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25089", + "title": "[FIX] UserCard sanitization", + "userLogin": "dougfabris", + "description": "- Rewrites the component to TS\r\n- Fixes some visual issues\r\n\r\n### before\r\n![Screen Shot 2022-04-07 at 00 23 11](https://user-images.githubusercontent.com/27704687/162113925-5c9484d1-23e9-4623-8b86-3fbc71b461a1.png)\r\n\r\n### after\r\n![Screen Shot 2022-04-07 at 00 07 13](https://user-images.githubusercontent.com/27704687/162112353-afd6aac6-b27c-4470-a642-631b8080d59e.png)", + "milestone": "4.7.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25085", + "title": "Chore: move definitions to packages", + "userLogin": "ggazzo", + "milestone": "3.7.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25168", + "title": "Regression: CI playwright", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25125", + "title": "Chore: Convert NotificationStatus to TS", + "userLogin": "jeanfbrito", + "contributors": [ + "jeanfbrito", + "ggazzo" + ] + }, + { + "pr": "25148", + "title": "[FIX] Message menu action not working on legacy messages.", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25122", + "title": "Chore: Tests with Playwright (task: All works)", + "userLogin": "weslley543", + "contributors": [ + "weslley543" + ] + }, + { + "pr": "25129", + "title": "Chore: Remove old files from removed Omnichannel feature", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25128", + "title": "Chore: Convert admin custom sound to tsx", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25126", + "title": "Chore: Migrate oauth2server to typescript", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "25123", + "title": "Chore: Convert LivechatAgentActivity to raw model and TS", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25124", + "title": "Chore: Remove unused Drone CI files", + "userLogin": "geekgonecrazy", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "25121", + "title": "Chore: Convert Mailer to TS", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti", + "sampaiodiego" + ] + }, + { + "pr": "25107", + "title": "Regression: Fix CI monorepo build", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "25074", + "title": "Chore: Monorepo ", + "userLogin": "ggazzo", + "milestone": "3.7.0", + "contributors": [ + "sampaiodiego", + "ggazzo" + ] + }, + { + "pr": "25097", + "title": "[IMPROVE] Rename upgrade tab routes", + "userLogin": "guijun13", + "description": "Change 'upgrade tab' routes names from camelCase ('goFullyFeatured') to kebab-case ('go-fully-featured') due to URL naming consistency. Changed types, main function and test.", + "contributors": [ + "guijun13" + ] + }, + { + "pr": "25076", + "title": "Bump eslint-plugin-anti-trojan-source from 1.0.6 to 1.1.0", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "24936", + "title": "[FIX] End call button disappearing when on-hold", + "userLogin": "tiagoevanp", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "24932", + "title": "[FIX] Use correct room property for call ended at", + "userLogin": "MartinSchoeler", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "25022", + "title": "[FIX] Proxy settings being ignored", + "userLogin": "pierre-lehnen-rc", + "description": "Modify Meteor's `HTTP.call` to add back proxy support", + "milestone": "4.6.1", + "contributors": [ + "pierre-lehnen-rc", + "sampaiodiego" + ] + }, + { + "pr": "25082", + "title": "[FIX] Invitation links don't redirect to the registration form", + "userLogin": "yash-rajpal", + "milestone": "4.6.1", + "contributors": [ + "yash-rajpal" + ] + }, + { + "pr": "23971", + "title": "[NEW] Message Template React Component", + "userLogin": "ggazzo", + "description": "Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template).\r\n\r\n\r\n\r\n![Screen Shot 2022-04-05 at 11 14 18](https://user-images.githubusercontent.com/27704687/161774027-38dd9c7b-eeeb-45e2-b9d8-ea2a9be8486d.png)\r\nIn case you encounter any problems, or want to compare, temporarily it is possible to use the old version\r\n\r\n\"image\"", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "25069", + "title": "[FIX] FormData uploads not working", + "userLogin": "gabriellsh", + "milestone": "4.6.1", + "contributors": [ + "gabriellsh", + "dougfabris" + ] + }, + { + "pr": "19866", + "title": "[FIX] Video and Audio not skipping forward", + "userLogin": "MartinSchoeler", + "contributors": [ + "MartinSchoeler", + "tassoevan", + "web-flow", + "dougfabris" + ] + }, + { + "pr": "25067", + "title": "[FIX] NPS never finishing sending results", + "userLogin": "sampaiodiego", + "milestone": "4.6.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "24405", + "title": "[IMPROVE] Add tooltip to sidebar room menu", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664", + "web-flow", + "dougfabris" + ] + }, + { + "pr": "24431", + "title": "[IMPROVE] Added tooltip options for message menu", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664", + "dougfabris" + ] + }, + { + "pr": "24166", + "title": "[FIX] Replace encrypted text to Encrypted Message Placeholder", + "userLogin": "yash-rajpal", + "description": "### before \r\n![image](https://user-images.githubusercontent.com/27704687/150807900-154a9cdb-ee13-4333-8628-f287ab914b40.png)\r\n\r\n### after\r\n\"Screenshot", + "milestone": "4.7.0", + "contributors": [ + "yash-rajpal", + "albuquerquefabio", + "web-flow" + ] + }, + { + "pr": "24984", + "title": "[FIX] Prevent sequential messages edited icon to hide on hover", + "userLogin": "dougfabris", + "description": "### before\r\n\"Screen\r\n\r\n### after\r\n\"Screen", + "milestone": "4.7.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25024", + "title": "[IMPROVE] Improve active/hover colors in account sidebar", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664" + ] + }, + { + "pr": "24856", + "title": "[FIX] Full error message is visible", + "userLogin": "Himanshu664", + "milestone": "4.7.0", + "contributors": [ + "Himanshu664", + "tassoevan" + ] + }, + { + "pr": "24708", + "title": "Chore: Cancel running jobs if PR is updated", + "userLogin": "debdutdeb", + "contributors": [ + "debdutdeb", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "24900", + "title": "Chore: organize test files and fix code coverage", + "userLogin": "tmontini", + "contributors": [ + null, + "tmontini", + "rodrigok" + ] + }, + { + "pr": "24464", + "title": "Chore: Missing keys in APIsDisplay", + "userLogin": "dougfabris", + "milestone": "4.7.0", + "contributors": [ + "dougfabris", + "tassoevan" + ] + }, + { + "pr": "25057", + "title": "Bump ejson from 2.2.1 to 2.2.2", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25053", + "title": "Chore: Remove Alpine image deps after using them", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25052", + "title": "Bump pino and pino-pretty", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25050", + "title": "[FIX] Upgrade Tab showing for a split second", + "userLogin": "gabriellsh", + "milestone": "4.6.1", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "25055", + "title": "[FIX] UserAutoComplete not rendering UserAvatar correctly", + "userLogin": "dougfabris", + "description": "### before\r\n![Screen Shot 2022-04-04 at 16 50 21](https://user-images.githubusercontent.com/27704687/161620921-800bf66a-806d-4f83-b2e1-073c34215001.png)\r\n\r\n### after\r\n![Screen Shot 2022-04-04 at 16 49 00](https://user-images.githubusercontent.com/27704687/161620720-3e27774d-c241-46ca-b764-932a9295d709.png)", + "milestone": "4.6.1", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "25031", + "title": "Chore: TS conversion folder client", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "24991", + "title": "Bump minimist from 1.2.5 to 1.2.6 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25002", + "title": "Bump template-file from 6.0.0 to 6.0.1", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25042", + "title": "Bump body-parser from 1.19.2 to 1.20.0 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "25043", + "title": "i18n: Language update from LingoHub 🤖 on 2022-04-04Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null + ] + }, + { + "pr": "25028", + "title": "Merge master into develop & Set version to 4.7.0-develop", + "userLogin": "sampaiodiego", + "contributors": [ + "AllanPazRibeiro", + "sampaiodiego", + "web-flow" + ] + } + ] + }, + "4.6.4": { + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [] + }, + "4.7.3": { + "node_version": "14.18.3", + "npm_version": "6.14.15", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [] + }, + "4.7.4": { + "node_version": "14.18.3", + "npm_version": "6.14.15", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "553", + "title": "Load missed messages from opened rooms when reconnect", + "userLogin": "rodrigok", + "contributors": [ + "rodrigok" + ] + } + ] + }, + "4.8.1": { + "node_version": "14.18.3", + "npm_version": "6.14.15", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "25723", + "title": "[FIX] Wrong argument name preventing Omnichannel Chat Forward to User ", + "userLogin": "dudanogueira", + "milestone": "4.8.1", + "contributors": [ + "dudanogueira" + ] + }, + { + "pr": "25669", + "title": "[FIX] Bump meteor-node-stubs to version 1.2.3", + "userLogin": "Sh0uld", + "description": "With meteor-node-stubs version 1.2.3 a bug was fixed, which occured in issue #25460 and probably #25513 (last one not tested).\r\nFor the issue in meteor see: https://github.com/meteor/meteor/issues/11974", + "milestone": "4.8.1", + "contributors": [ + "Sh0uld", + "ggazzo" + ] + }, + { + "pr": "25708", + "title": "[FIX] AccountBox checks for condition", + "userLogin": "tiagoevanp", + "milestone": "4.8.1", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "25781", + "title": "[FIX] Fix prom-client new promise usage", + "userLogin": "KevLehman", + "milestone": "4.8.1", + "contributors": [ + "KevLehman" + ] + } + ] + }, + "5.0.0-rc.1": { + "node_version": "14.18.3", + "npm_version": "6.14.15", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "26127", + "title": "Regression: Close button on modals created via apps not working", + "userLogin": "murtaza98", + "milestone": "5.0.0", + "contributors": [ + "murtaza98", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "26125", + "title": "Regression: Unable to click on UiKit buttons provided by apps", + "userLogin": "murtaza98", + "milestone": "5.0.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "26140", + "title": "Regression: Fix assets format", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "ggazzo" + ] + }, + { + "pr": "26062", + "title": "Regression: Update error message on `useEndpointActionExperimental`", + "userLogin": "LucianoPierdona", + "description": "This PR changes the way we show an error message to the user on the `useEndpointActionExperimental` hook, previously for `Object` error messages it was being shown as undefined", + "contributors": [ + "LucianoPierdona", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "26129", + "title": "Regression: All users in members list showing as federated", + "userLogin": "gabriellsh", + "milestone": "5.0.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "26115", + "title": "Regression: Do not show federated tooltip on non-federated rooms", + "userLogin": "MarcosSpessatto", + "milestone": "5.0.0", + "contributors": [ + "MarcosSpessatto", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "26117", + "title": "Regression: Users on new sessions are forced to re-configure 2fa", + "userLogin": "pierre-lehnen-rc", + "milestone": "5.0.0", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "26080", + "title": "Regression: Fix marketplace app apis visibility problem", + "userLogin": "rique223", + "description": "Solved a problem that showed an unwanted zero in place of the APIs section for apps that weren't installed/did not have an APIs section.\r\nBefore:\r\n![image](https://user-images.githubusercontent.com/43561537/176743542-8f5d2e97-48e7-4947-a82a-43c3a15ea0ac.png)\r\n\r\nAfter(non installed app):\r\n![image](https://user-images.githubusercontent.com/43561537/176744082-0139e15b-b03b-4c03-9267-9a17d14b70e9.png)\r\n\r\nAfter(installed app)\r\n![image](https://user-images.githubusercontent.com/43561537/176772870-c5382edc-59e6-42e4-8dfa-f1e4fd0267c0.png)", + "milestone": "5.0.0", + "contributors": [ + "rique223" + ] + }, + { + "pr": "26116", + "title": "Regression: Changing isEnterprise useQuery name to prevent conflict of queries", + "userLogin": "hugocostadev", + "description": "Changed the name of useQuery hook to prevent conflict of queries with same name.", + "milestone": "5.0.0", + "contributors": [ + "hugocostadev" + ] + }, + { + "pr": "26101", + "title": "Regression: [VideoConference] Callee client behaves improperly when accepting a call from someone who lost the connection", + "userLogin": "pierre-lehnen-rc", + "description": "If the caller loses connection and the callee accepts the call anyway, the client will wait for five seconds for confirmation that they can join the call. This PR improves the UI behavior during those five seconds.", + "milestone": "5.0.0", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "26113", + "title": "Chore: Change stats to daily", + "userLogin": "sampaiodiego", + "milestone": "5.0.0", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "5.0.0-rc.2": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "26184", + "title": "Regression: Align TypeScript version across workspaces", + "userLogin": "tassoevan", + "description": "Some places were still referring to TypeScript 4.3.4 instead of 4.5.5, so this PR targets it.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "25991", + "title": "Chore: Update Meteor 2.7.3", + "userLogin": "sampaiodiego", + "milestone": "5.0.0", + "contributors": [ + "sampaiodiego", + "ggazzo" + ] + }, + { + "pr": "26153", + "title": "Chore: update avatar colors", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti" + ] + }, + { + "pr": "26119", + "title": "Regression: Added missing call button to contact center calls list", + "userLogin": "aleksandernsilva", + "description": "This PR adds a call button to the contact center calls list rows.", + "milestone": "5.0.0", + "contributors": [ + "aleksandernsilva", + "ggazzo" + ] + }, + { + "pr": "26138", + "title": "Regression: Calling info on VoipFooter when performing an outbound call", + "userLogin": "tiagoevanp", + "description": "![image](https://user-images.githubusercontent.com/17487063/177395438-a0b2d30a-e0e2-4a31-9b55-2c6c3216bbd7.png)", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "26135", + "title": "Regression: Added missing call button in the contact info contextual bar", + "userLogin": "aleksandernsilva", + "contributors": [ + "aleksandernsilva" + ] + }, + { + "pr": "26141", + "title": "Regression: Emojis displaying as `:undefined:`", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "26111", + "title": "Regression: Correct call ringtones", + "userLogin": "murtaza98", + "description": "- outbound-call-ringing ringtone: Should be played when the outbound call is initiated and not yet established(Current implementation is playing the incoming-call ringtone)\r\n- call-ended ringtone: Should be played whenever a call ends.", + "milestone": "5.0.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "26097", + "title": "Regression: Update message reaction text", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "26134", + "title": "Regression: Add better error messages when call fails", + "userLogin": "KevLehman", + "milestone": "5.0.0", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "26128", + "title": "Regression: Broken emoji picker on Livechat", + "userLogin": "murtaza98", + "milestone": "5.0.0", + "contributors": [ + "murtaza98" + ] + } + ] + }, + "5.0.0-rc.3": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "26201", + "title": "Chore: Info page", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "26199", + "title": "Regression: Fix command previews", + "userLogin": "d-gubert", + "description": "Fixes slash command previews not being showed", + "milestone": "5.0.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "26205", + "title": "Chore: Change Apps-Engine version source for info", + "userLogin": "d-gubert", + "description": "Now that we're using `yarn`, the version stored in the `package.json` is no longer the resolved one, but it matches the input. This means that when we ran `yarn add @rocket.chat/apps-engine@alpha`, yarn saves `\"alpha\"` as the version of the package, while NPM added the resolved version for the tag, e.g. `\"1.33.0-alpha.6507\"`. This ends up breaking a few places where we need the Apps-Engine version for communication with the Marketplace.\r\n\r\nWith this PR we change the source of that info so the problem doesn't happen anymore.", + "milestone": "5.0.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "26148", + "title": "Regression: moving Community Watermark to `ee` folder", + "userLogin": "hugocostadev", + "description": "Due to legal reasons, the Watermark used in community Edition was moved to Enterprise folder `ee`", + "contributors": [ + "hugocostadev" + ] + }, + { + "pr": "26136", + "title": "Regression: Send files with `enter` key", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + } + ] + }, + "5.0.0-rc.4": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "26226", + "title": "Regression: Fix files list endpoints", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "26194", + "title": "Regression: Fix Omnichannel not working after meteor update", + "userLogin": "KevLehman", + "description": "Fixed things:\r\n- Omnichannel Directory\r\n- Omnichannel Current Chats\r\n- Auto Selection Algo\r\n- Load Balance Algo\r\n- Manual Selection Algo\r\n- Livechat New Conversations\r\n\r\nOther fixed things:\r\n- Warning on fields deprecation\r\n- Warning on \"remove\" deprecation\r\n- Remove findAndModify usage", + "milestone": "5.0.0", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "26160", + "title": "Regression: Empty URL previews in messages.", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "26179", + "title": "Regression: OTR with new React Messages", + "userLogin": "yash-rajpal", + "description": "This PR solves 2 OTR issues with new react message components\r\n\r\n- disable the server side message parser for OTR messages\r\n- adds the stopwatch icon for otr messages\r\n\r\n### Before\r\n\"Screenshot\r\n\r\n### After\r\n\"Screenshot", + "contributors": [ + "yash-rajpal", + "web-flow" + ] + }, + { + "pr": "26216", + "title": "Regression: Replace contact center icon", + "userLogin": "filipemarins", + "milestone": "5.0.0", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "26093", + "title": "Regression: Fix rendered markdown styling on app info page details section", + "userLogin": "rique223", + "description": "Fixed two styling problems on the AppDetails markdown. The first one was a misuse of flex and the second was the fact that the withRichContent flag was missing on the box that received the markdown.\r\nDemo images:\r\nBefore:\r\n![image](https://user-images.githubusercontent.com/43561537/177857346-54476879-2618-452f-8585-1922dcbfa9c1.png)\r\n\r\nAfter:\r\n![image](https://user-images.githubusercontent.com/43561537/177857376-e96e4ad3-3410-4847-89b7-df074ff87b2f.png)\r\n\r\nClickup task: https://app.clickup.com/t/2rwq0q7", + "milestone": "5.0.0", + "contributors": [ + "rique223" + ] + }, + { + "pr": "26225", + "title": "[BREAK] Remove webRTC for channels/dm/groups", + "userLogin": "dougfabris", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "26223", + "title": "Regression: Meteor uses `projection` for its observes", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "26163", + "title": "Chore: Do not log integrations using `name` key", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "26219", + "title": "Chore: Check for env var values and not just if they are set", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "26171", + "title": "Regression: UIKit buttons auth user validation", + "userLogin": "tapiarafael", + "description": "Fix the validation to match the new feature that allows apps to register auth-required routes.", + "milestone": "5.0.0", + "contributors": [ + "tapiarafael", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "26158", + "title": "Regression: Cannot logout when CallProvider is unregistered and mounted", + "userLogin": "KevLehman", + "milestone": "5.0.0", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "26159", + "title": "Regression: Change Audio settings for device settings as modal title", + "userLogin": "KevLehman", + "milestone": "5.0.0", + "contributors": [ + "KevLehman", + "murtaza98", + "web-flow" + ] + }, + { + "pr": "26173", + "title": "Regression: Inline code and copyonly tag styles", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "26152", + "title": "Regression: remove italic from reaction translation", + "userLogin": "filipemarins", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "26197", + "title": "Regression: Reverting @rocket.chat/mp3-encoder version to fix Audio Message", + "userLogin": "hugocostadev", + "description": "An unknow breaking change (still investigating) happened when upgrading the [@rocket.chat/mp3-encoder](https://github.com/RocketChat/fuselage/tree/develop/packages/mp3-encoder) package to version 0.25.0, because of that we revert the version to 0.24.0 the last know working version.", + "milestone": "5.0.0", + "contributors": [ + "hugocostadev" + ] + } + ] + }, + "5.0.0-rc.5": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "26170", + "title": "Regression: Burger menu showing arrow instead of burguer", + "userLogin": "gabriellsh", + "milestone": "5.0.0", + "contributors": [ + "gabriellsh", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "26203", + "title": "Regression: Last_login translation key", + "userLogin": "dougfabris", + "milestone": "5.0.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "26209", + "title": "Regression: Livechat rooms not opening due to route desync", + "userLogin": "aleksandernsilva", + "description": "Due to route information only updating on `Tracker.afterFlush` (https://github.com/RocketChat/Rocket.Chat/pull/25990), we found out that calling the `tabBar.openUserInfo()` method at this point will cause a route change to the previous route instead of the current one, preventing livechat rooms from being opened.\r\n\r\nAs a provisory solution, we're delaying the opening of the contextual bar, which then ensures that the route info is up to date. Although this solution works, we need to find a more reliable way of ensuring consistent route changes with up-to-date information.\r\n\r\n### I'm definitely open for better looking alternatives. Please leave a comment if you have a better solution to share.", + "milestone": "5.0.0", + "contributors": [ + "aleksandernsilva", + "ggazzo", + "web-flow", + "murtaza98" + ] + }, + { + "pr": "26232", + "title": "Regression: Admin Avatar Edit endpoint fix", + "userLogin": "hugocostadev", + "milestone": "5.0.0", + "contributors": [ + "hugocostadev" + ] + }, + { + "pr": "26234", + "title": "Regression: Don't open mdm feature modal on registration page", + "userLogin": "yash-rajpal", + "milestone": "5.0.0", + "contributors": [ + "yash-rajpal", + "web-flow" + ] + }, + { + "pr": "26238", + "title": "Regression: Revert replace contact center icon", + "userLogin": "filipemarins", + "milestone": "5.0.0", + "contributors": [ + "filipemarins" + ] + }, + { + "pr": "26233", + "title": "Regression: Fix routing for public queue and visitor abandonment fiber usage", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "26174", + "title": "Regression: Unavailable devices in unsupported browsers", + "userLogin": "MartinSchoeler", + "contributors": [ + "MartinSchoeler", + "web-flow", + "KevLehman" + ] + }, + { + "pr": "26102", + "title": "Chore: Remove unused migrations", + "userLogin": "debdutdeb", + "description": "After giving it some thought:\r\n\r\n- 234 through 240 are not going to be run anymore. Keeping them does not affect behavior of course, but this (removing) makes it easier to quickly glance at and understand what migrations are actually included in 5.x.y (especially in tag compare view or in general just checking the ref).\r\n\r\n- Also changed the file name of 233 to be more explicit at what it does so to not confuse with actual \"migrations\" without having to open the file. \r\n\r\n- The redirect to the documentation page (go.rocket....) is not yet set up, jfyi.", + "milestone": "5.0.0", + "contributors": [ + "debdutdeb", + "sampaiodiego" + ] + } + ] + }, + "5.0.0-rc.6": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "26162", + "title": "Regression: Fix marketplace releases tab crash bug", + "userLogin": "rique223", + "description": "Fixed a bug where RC would crash because the marketplace releases tab was trying to display undefined data from manually installed apps. \r\nDemo gif:\r\n![app-releases-tab-crash-error](https://user-images.githubusercontent.com/43561537/177656489-325790d3-49e0-46c8-8ac2-1f74c6a309ad.gif)", + "milestone": "5.0.0", + "contributors": [ + "rique223", + "ggazzo", + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "26257", + "title": "Chore: Disabled icon colors on sidebar", + "userLogin": "juliajforesti", + "contributors": [ + "juliajforesti", + "web-flow" + ] + }, + { + "pr": "26256", + "title": "Regression: get user from `req` on `ui.interaction` endpoints", + "userLogin": "sampaiodiego", + "milestone": "5.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "26253", + "title": "Chore: Avoid unneeded permission updates when EE license is applied", + "userLogin": "sampaiodiego", + "milestone": "4.8.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "26242", + "title": "Regression: Fix scheduler not working", + "userLogin": "tapiarafael", + "milestone": "5.0.0", + "contributors": [ + "tapiarafael" + ] + }, + { + "pr": "26073", + "title": "Regression: Link not scrolling to message", + "userLogin": "filipemarins", + "milestone": "5.0.0", + "contributors": [ + "filipemarins", + "gabriellsh" + ] + } + ] + }, + "5.0.0-rc.7": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "26267", + "title": "Regression: Omni-chats not getting routed automatically to bots", + "userLogin": "murtaza98", + "milestone": "5.0.0", + "contributors": [ + "murtaza98", + "web-flow" + ] + }, + { + "pr": "26172", + "title": "Regression: Cannot open Menu in searched message.", + "userLogin": "gabriellsh", + "milestone": "5.0.0", + "contributors": [ + "gabriellsh", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "26235", + "title": "Regression: REST API calls at Engagement Dashboard", + "userLogin": "tassoevan", + "description": "Parameters for GET requests are *not* serialized as for other methods, therefore sending `Date` objects is not viable due to the way `Date.prototype.toString` works. This PR uses `Date.prototype.toISOString` explicitly to serialize dates.", + "milestone": "5.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "26237", + "title": "Regression: Call toggle missing network disconnection state", + "userLogin": "aleksandernsilva", + "description": "This PR brings back the network disconnection state to the voip call toggle button\r\n\r\n![image (4)](https://user-images.githubusercontent.com/6494543/178564719-f436505e-3ae3-4d69-ba5a-27ce8e8c5fba.png)", + "milestone": "5.0.0", + "contributors": [ + "aleksandernsilva" + ] + }, + { + "pr": "26258", + "title": "Chore: Update Apps-Engine version", + "userLogin": "d-gubert", + "description": "Bumping Apps-Engine version", + "milestone": "5.0.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "26270", + "title": "Chore: Avoid set useless set UTC Offset", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "26139", + "title": "Regression: Sidebar icons spacing", + "userLogin": "guijun13", + "description": "- Fixed the sidebar icons ('display' and 'create new') spacing issue\r\n\r\nbefore:\r\n![image](https://user-images.githubusercontent.com/5263975/178897210-50615ea9-28d5-4b35-a93a-c5facea365e5.png)\r\n\r\n\r\n\r\nafter:\r\n\r\n![image](https://user-images.githubusercontent.com/5263975/178896945-1bf71112-8a01-4db6-9f9b-20ea778496f7.png)", + "milestone": "5.0.0", + "contributors": [ + "guijun13", + "ggazzo" + ] + }, + { + "pr": "26188", + "title": "Chore: Hide deprecation query log on production", + "userLogin": "ggazzo", + "milestone": "5.0.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + } + ] + }, + "5.0.0-rc.8": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "26049", + "title": "Regression: AutoTranslate on new message template", + "userLogin": "filipemarins", + "milestone": "5.0.0", + "contributors": [ + "filipemarins", + "tassoevan" + ] + }, + { + "pr": "26224", + "title": "Chore: Plan tag", + "userLogin": "gabriellsh", + "description": "Now we only have one plan tag for all plans \\/\r\n![image](https://user-images.githubusercontent.com/40830821/178366367-12388c4c-6822-4e41-be8d-ca306718be98.png)", + "milestone": "5.0.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "26248", + "title": "Regression: Remove alpha tag and fix initialization process", + "userLogin": "MarcosSpessatto", + "milestone": "5.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "26255", + "title": "Regression: Device management table missing device icon and ip text ellipsis", + "userLogin": "csuadev", + "contributors": [ + "csuadev", + "yash-rajpal" + ] + }, + { + "pr": "26252", + "title": "Regression: UserInfo/RoomInfo Menu", + "userLogin": "dougfabris", + "description": "**note**: next fuselage's version needed\r\n\r\n#### before\r\n![Screen Shot 2022-07-13 at 12 24 38](https://user-images.githubusercontent.com/27704687/178771262-d482b300-de80-4961-be2e-8c034480d237.png)\r\n\r\n#### after\r\n![Screen Shot 2022-07-13 at 12 25 39](https://user-images.githubusercontent.com/27704687/178771460-db10883b-aa6d-4254-82d4-8cadd6991ae8.png)", + "milestone": "5.0.0", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "26249", + "title": "Regression: Federated users not showing as federated in Room Members", + "userLogin": "gabriellsh", + "milestone": "5.0.0", + "contributors": [ + "gabriellsh", + "MarcosSpessatto" + ] + }, + { + "pr": "26239", + "title": "Regression: Removed CE watermark from VoipFooter", + "userLogin": "aleksandernsilva", + "description": "The objective of this change is to remove the CE watermark **only** during an active call. The CE watermark will be displayed normally in all other scenarios. Bellow you can see a demonstration of the expected behavior:\r\n\r\n![ce-watermark-removed-voip](https://user-images.githubusercontent.com/6494543/178615342-8049a2a8-d331-46a9-a8f1-8461ae341b50.gif)", + "milestone": "5.0.0", + "contributors": [ + "aleksandernsilva" + ] + }, + { + "pr": "26154", + "title": "Regression: Parse outbound phone number removing * putting + char", + "userLogin": "tiagoevanp", + "milestone": "5.0.0", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "26273", + "title": "Regression: Search on Member List", + "userLogin": "tassoevan", + "milestone": "5.0.0", + "contributors": [ + "tassoevan", + "sampaiodiego" + ] + } + ] + }, + "5.0.0-rc.9": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "26050", + "title": "[FIX] Users without the `view-other-user-info` permission can't use the `users.list` endpoint", + "userLogin": "LucianoPierdona", + "description": "This PR fix the query when a normal users access `users.list`", + "milestone": "5.0.0", + "contributors": [ + "LucianoPierdona", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "26274", + "title": "Chore: Upgrade Fuselage packages to `next` dist-tag", + "userLogin": "tassoevan", + "description": "Upgrade Fuselage packages to the latest development versions.", + "milestone": "5.0.0", + "contributors": [ + "tassoevan" + ] + } + ] + }, + "5.0.0-rc.10": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "26311", + "title": "Regression: Add v1 to licenses.add endpoint", + "userLogin": "KevLehman", + "milestone": "5.0.0", + "contributors": [ + "KevLehman", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "26183", + "title": "Chore: VideoConference UX/UI Refactor 1st Interaction ", + "userLogin": "pierre-lehnen-rc", + "milestone": "5.0.0", + "contributors": [ + "pierre-lehnen-rc", + "web-flow", + "debdutdeb", + "dougfabris", + "ggazzo" + ] + }, + { + "pr": "26295", + "title": "Regression: Clear user selection filter after selecting desired user.", + "userLogin": "gabriellsh", + "milestone": "5.0.0", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "26304", + "title": "Regression: Fix permissions page pagination", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "26305", + "title": "Regression: Fix breaking omnichannel tests", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98", + "web-flow" + ] + }, + { + "pr": "26251", + "title": "Regression: Remove 4.0 version banner", + "userLogin": "hugocostadev", + "description": "Created a migration to disable and dismiss for all users the old 4.0 version banner.\r\nIt happened when a new admin user has been added.", + "milestone": "5.0.0", + "contributors": [ + "hugocostadev", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "26092", + "title": "Chore: Fix Omnichannel E2E tests not running", + "userLogin": "murtaza98", "contributors": [ - "renatobecker", "murtaza98", "web-flow" ] }, { - "pr": "24592", - "title": "Regression: Fix in-correct room status shown to agents", + "pr": "26294", + "title": "Chore: Remove TimeSync usage", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "26155", + "title": "Regression: Contact manager edit/view not working", + "userLogin": "KevLehman", + "description": "Basically, the Contact Center was working, but not the right way. This PR fixes:\r\n- Ability to select Contact Managers from dropdown\r\n- Ability to validate Contact Edits without requesting data a ton of times\r\n- Ability to remove Contact manager from a contact\r\n- Ability to see Contacts and Contact Managers on Contact View\r\n- Fix endpoints validation\r\n- Add validators (ajv) to endpoint, thou not being used yet (since we hit a special endpoint)", + "milestone": "5.0.0", + "contributors": [ + "KevLehman", + "murtaza98", + "web-flow" + ] + }, + { + "pr": "26245", + "title": "Chore: Tests refactor pageobjects", + "userLogin": "souzaramon", + "contributors": [ + "souzaramon", + "weslley543", + "web-flow" + ] + }, + { + "pr": "26298", + "title": "Regression: Adjusted priority to run canned responses replace before new parser", + "userLogin": "aleksandernsilva", + "description": "Canned responses placeholders were not being replaced properly after we changed to the new md parser. \r\nThis fix changes the priority so that the canned responses replace logic runs before the parser, thus bringing back this functionality.\r\n\r\nBefore:\r\n\"Screen\r\n\r\nAfter:\r\n\"Screen", + "milestone": "5.0.0", + "contributors": [ + "aleksandernsilva" + ] + }, + { + "pr": "26278", + "title": "Regression: Fix app icons breaking UI", "userLogin": "murtaza98", - "milestone": "4.5.0", + "milestone": "5.0.0", "contributors": [ - "murtaza98" + "murtaza98", + "web-flow", + "tassoevan" ] } ] }, - "4.4.4": { - "node_version": "14.18.3", - "npm_version": "6.14.15", - "mongo_versions": [ - "3.6", - "4.0", - "4.2", - "4.4", - "5.0" - ], - "pull_requests": [] - }, - "4.4.5": { - "mongo_versions": [ - "3.6", - "4.0", - "4.2", - "4.4", - "5.0" - ], - "pull_requests": [] - }, - "4.5.7": { - "mongo_versions": [ - "3.6", - "4.0", - "4.2", - "4.4", - "5.0" - ], - "pull_requests": [] - }, - "4.6.4": { - "mongo_versions": [ - "3.6", - "4.0", - "4.2", - "4.4", - "5.0" - ], - "pull_requests": [] - }, - "4.7.3": { + "4.7.5": { "node_version": "14.18.3", "npm_version": "6.14.15", "mongo_versions": [ @@ -77097,7 +90662,7 @@ ], "pull_requests": [] }, - "4.7.4": { + "4.8.2": { "node_version": "14.18.3", "npm_version": "6.14.15", "mongo_versions": [ @@ -77109,116 +90674,222 @@ ], "pull_requests": [ { - "pr": "553", - "title": "Load missed messages from opened rooms when reconnect", - "userLogin": "rodrigok", + "pr": "26326", + "title": "Release 4.8.2", + "userLogin": "sampaiodiego", "contributors": [ - "rodrigok" + "sampaiodiego", + "matthias4217" + ] + }, + { + "pr": "26253", + "title": "Chore: Avoid unneeded permission updates when EE license is applied", + "userLogin": "sampaiodiego", + "milestone": "4.8.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25724", + "title": "[FIX] Not showing edit message button when blocking edit after N minutes", + "userLogin": "matthias4217", + "description": "Previously, in Rocketchat 4.7.0 and later, as mentioned in https://github.com/RocketChat/Rocket.Chat/issues/25478, the edit button was not displayed on the interface in the minute after having sent a message. This is now fixed : messages can be edited right after sending them.", + "milestone": "4.8.2", + "contributors": [ + "matthias4217", + "sampaiodiego" + ] + }, + { + "pr": "26058", + "title": "[FIX] Error \"numRequestsAllowed\" property in rateLimiter for REST API endpoint when upgrading", + "userLogin": "sampaiodiego", + "milestone": "4.8.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25891", + "title": "[FIX] Settings not being overwritten to their default values", + "userLogin": "sampaiodiego", + "milestone": "4.8.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "25814", + "title": "Release 4.8.1", + "userLogin": "ggazzo", + "contributors": [ + "KevLehman", + "ggazzo", + "tiagoevanp", + "Sh0uld", + "dudanogueira" ] } ] }, - "4.8.1": { - "node_version": "14.18.3", - "npm_version": "6.14.15", + "5.0.0-rc.11": { + "node_version": "14.19.3", + "npm_version": "6.14.17", "mongo_versions": [ - "3.6", - "4.0", "4.2", "4.4", "5.0" ], "pull_requests": [ { - "pr": "25723", - "title": "[FIX] Wrong argument name preventing Omnichannel Chat Forward to User ", - "userLogin": "dudanogueira", - "milestone": "4.8.1", + "pr": "26310", + "title": "Regression: fix `directory` endpoint not listing teams", + "userLogin": "carlosrodrigues94", + "milestone": "5.0.0", "contributors": [ - "dudanogueira" + "carlosrodrigues94", + "matheusbsilva137", + "web-flow", + "ggazzo" ] }, { - "pr": "25669", - "title": "[FIX] Bump meteor-node-stubs to version 1.2.3", - "userLogin": "Sh0uld", - "description": "With meteor-node-stubs version 1.2.3 a bug was fixed, which occured in issue #25460 and probably #25513 (last one not tested).\r\nFor the issue in meteor see: https://github.com/meteor/meteor/issues/11974", - "milestone": "4.8.1", + "pr": "26309", + "title": "Regression: Options overlapping input in Users Autocomplete", + "userLogin": "gabriellsh", + "milestone": "5.0.0", "contributors": [ - "Sh0uld", + "gabriellsh" + ] + }, + { + "pr": "26283", + "title": "Regression: Matrix Federation regressions", + "userLogin": "MarcosSpessatto", + "milestone": "5.0.0", + "contributors": [ + "MarcosSpessatto", + "web-flow", + "carlosrodrigues94", + "gabriellsh", "ggazzo" ] }, { - "pr": "25708", - "title": "[FIX] AccountBox checks for condition", + "pr": "26319", + "title": "Regression: Use fname instead real unique name for Voip", "userLogin": "tiagoevanp", - "milestone": "4.8.1", + "description": "Affect:\r\n- Voip room header\r\n- Contacts table\r\n- Contact info", + "milestone": "5.0.0", "contributors": [ "tiagoevanp" ] }, { - "pr": "25781", - "title": "[FIX] Fix prom-client new promise usage", - "userLogin": "KevLehman", - "milestone": "4.8.1", + "pr": "26269", + "title": "Regression: Channel `type` icon on Engagement Dashboard", + "userLogin": "LucianoPierdona", + "description": "This PR fixes a bug on which the channel type is inverted.", + "milestone": "5.0.0", "contributors": [ - "KevLehman" + "LucianoPierdona", + "tassoevan", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "26315", + "title": "Regression: Fix job Id not returned by agenda", + "userLogin": "murtaza98", + "milestone": "5.0.0", + "contributors": [ + "murtaza98", + "web-flow" + ] + }, + { + "pr": "26241", + "title": "Regression: Special characters on phone number", + "userLogin": "tiagoevanp", + "description": "PR Includes:\r\n- Keep focus on phone input of dial pad\r\n- Handle submit with \"Enter\" key\r\n- Remove mask and mandatory \"+\" char\r\n- Long press for \"0\"/\"+\" button", + "milestone": "5.0.0", + "contributors": [ + "tiagoevanp", + "ggazzo", + "web-flow", + "filipemarins", + "KevLehman", + "aleksandernsilva" ] } ] }, - "4.8.2": { - "node_version": "14.18.3", - "npm_version": "6.14.15", + "5.0.0-rc.12": { + "node_version": "14.19.3", + "npm_version": "6.14.17", "mongo_versions": [ - "3.6", - "4.0", "4.2", "4.4", "5.0" ], "pull_requests": [ { - "pr": "26253", - "title": "Chore: Avoid unneeded permission updates when EE license is applied", - "userLogin": "sampaiodiego", - "milestone": "4.8.2", + "pr": "26327", + "title": "Regression: Livechat not rendering UiKit messages with action buttons", + "userLogin": "murtaza98", + "milestone": "5.0.0", "contributors": [ - "sampaiodiego" + "murtaza98", + "ggazzo", + "web-flow", + "aleksandernsilva" ] }, { - "pr": "25724", - "title": "[FIX] Not showing edit message button when blocking edit after N minutes", - "userLogin": "matthias4217", - "description": "Previously, in Rocketchat 4.7.0 and later, as mentioned in https://github.com/RocketChat/Rocket.Chat/issues/25478, the edit button was not displayed on the interface in the minute after having sent a message. This is now fixed : messages can be edited right after sending them.", - "milestone": "4.8.2", + "pr": "26325", + "title": "Chore: bump fuselage packages", + "userLogin": "dougfabris", + "milestone": "5.0.0", "contributors": [ - "matthias4217", - "sampaiodiego" + "dougfabris", + "ggazzo" ] }, { - "pr": "26058", - "title": "[FIX] Error \"numRequestsAllowed\" property in rateLimiter for REST API endpoint when upgrading", - "userLogin": "sampaiodiego", - "milestone": "4.8.2", + "pr": "26322", + "title": "Chore: Update useSidebarPalette selectors", + "userLogin": "juliajforesti", + "milestone": "5.0.0", "contributors": [ - "sampaiodiego" + "juliajforesti", + "web-flow", + "ggazzo", + "murtaza98" ] }, { - "pr": "25891", - "title": "[FIX] Settings not being overwritten to their default values", + "pr": "26328", + "title": "Regression: Fix get myself user data", "userLogin": "sampaiodiego", - "milestone": "4.8.2", + "milestone": "5.0.0", "contributors": [ "sampaiodiego" ] } ] + }, + "5.0.0": { + "node_version": "14.19.3", + "npm_version": "6.14.17", + "mongo_versions": [ + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [] } } -} +} \ No newline at end of file diff --git a/.github/no-js-action-config.json b/.github/no-js-action-config.json index 5dd97fccaf67..45e82790b82e 100644 --- a/.github/no-js-action-config.json +++ b/.github/no-js-action-config.json @@ -1,5 +1,13 @@ { "added": { - "ignore": ["packages/accounts-linkedin/**/*", "packages/linkedin-oauth/**/*", "tests/cypress/integration/08-resolutions.spec.js", "**/.eslintrc.js", "packages/eslint-config/**", "**/babel.config.js"] + "ignore": [ + "packages/node-poplib/**/*", + "packages/accounts-linkedin/**/*", + "packages/linkedin-oauth/**/*", + "tests/cypress/integration/08-resolutions.spec.js", + "**/.eslintrc.js", + "packages/eslint-config/**", + "**/babel.config.js" + ] } } diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml new file mode 100644 index 000000000000..b02b09a62a91 --- /dev/null +++ b/.github/workflows/auto-label.yml @@ -0,0 +1,11 @@ +name: 'Auto label QA' +on: + pull_request: + types: [opened, synchronize, labeled, unlabeled] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: ggazzo/gh-action-auto-label@beta-5 + with: + GITHUB_TOKEN: ${{ secrets.RC_AUTOLABEL_TOKEN }} diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 58439d8b9c27..335cf9a25ee5 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -15,10 +15,49 @@ concurrency: env: CI: true - MONGO_URL: mongodb://localhost:27017 + MONGO_URL: mongodb://localhost:27017/rocketchat?replicaSet=rs0&directConnection=true + MONGO_OPLOG_URL: mongodb://mongodb:27017/local?replicaSet=rs0&directConnection=true TOOL_NODE_FLAGS: --max_old_space_size=4096 + TURBO_TEAM: ${{ secrets.TURBO_TEAM }} jobs: + release-versions: + runs-on: ubuntu-latest + outputs: + release: ${{ steps.by-tag.outputs.release }} + latest-release: ${{ steps.latest.outputs.latest-release }} + docker-tag: ${{ steps.docker.outputs.docker-tag }} + gh-docker-tag: ${{ steps.docker.outputs.gh-docker-tag }} + steps: + - id: by-tag + run: | + if echo "$GITHUB_REF_NAME" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' ; then + RELEASE="latest" + elif echo "$GITHUB_REF_NAME" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$' ; then + RELEASE="release-candidate" + fi + echo "RELEASE: ${RELEASE}" + echo "::set-output name=release::${RELEASE}" + + - id: latest + run: | + LATEST_RELEASE="$( + git -c 'versionsort.suffix=-' ls-remote -t --exit-code --refs --sort=-v:refname "https://github.com/$GITHUB_REPOSITORY" '*' | + sed -En '1!q;s/^[[:xdigit:]]+[[:space:]]+refs\/tags\/(.+)/\1/gp' + )" + echo "LATEST_RELEASE: ${LATEST_RELEASE}" + echo "::set-output name=latest-release::${LATEST_RELEASE}" + + - id: docker + run: | + if [[ '${{ github.event_name }}' == 'pull_request' ]]; then + DOCKER_TAG="pr-${{ github.event.number }}" + else + DOCKER_TAG="gh-${{ github.run_id }}" + fi + echo "DOCKER_TAG: ${DOCKER_TAG}" + echo "::set-output name=gh-docker-tag::${DOCKER_TAG}" + build: runs-on: ubuntu-20.04 @@ -33,11 +72,6 @@ jobs: echo "github.event_name: ${{ github.event_name }}" cat $GITHUB_EVENT_PATH - - name: Use Node.js 14.18.3 - uses: actions/setup-node@v3 - with: - node-version: '14.18.3' - - name: Set Swap Space uses: pierotofy/set-swap-space@master with: @@ -45,52 +79,34 @@ jobs: - uses: actions/checkout@v3 + - name: Use Node.js 14.19.3 + uses: actions/setup-node@v3 + with: + node-version: '14.19.3' + cache: 'yarn' + - name: Free disk space run: | sudo apt clean docker rmi $(docker image ls -aq) df -h - # TODO is this required? - # - name: check package-lock - # 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 - uses: actions/cache@v2 - with: - path: | - ./node_modules/.turbo - key: ${{ runner.OS }}-turbo-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-turbo- - ${{ runner.os }}- - - # TODO change to use turbo cache - name: Cache meteor local uses: actions/cache@v2 with: path: ./apps/meteor/.meteor/local - key: ${{ runner.OS }}-meteor_cache-${{ hashFiles('apps/meteor/.meteor/versions') }} + key: meteor-local-cache-${{ runner.OS }}-${{ hashFiles('apps/meteor/.meteor/versions') }} restore-keys: | - ${{ runner.os }}-meteor_cache- - ${{ runner.os }}- + meteor-local-cache-${{ runner.os }}- + - name: Cache meteor uses: actions/cache@v2 with: path: ~/.meteor - key: ${{ runner.OS }}-meteor-${{ hashFiles('apps/meteor/.meteor/release') }} + key: meteor-cache-${{ runner.OS }}-${{ hashFiles('apps/meteor/.meteor/release') }} restore-keys: | - ${{ runner.os }}-meteor- - ${{ runner.os }}- + meteor-cache-${{ runner.os }}- + - name: Install Meteor run: | # Restore bin from cache @@ -121,16 +137,22 @@ 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 + - name: TurboRepo local server + uses: felixmosh/turborepo-gh-artifacts@v1 + with: + repo-token: ${{ secrets.RC_TURBO_GH_TOKEN }} + server-token: ${{ secrets.TURBO_SERVER_TOKEN }} + + - name: Lint + run: yarn lint --api="http://127.0.0.1:9080" --token="${{ secrets.TURBO_SERVER_TOKEN }}" --team='rc' - - run: yarn turbo run translation-check + - name: Translation check + run: yarn turbo run translation-check --api="http://127.0.0.1:9080" --token="${{ secrets.TURBO_SERVER_TOKEN }}" --team='rc' - name: TS typecheck - run: | - yarn turbo run typecheck + run: yarn turbo run typecheck --api="http://127.0.0.1:9080" --token="${{ secrets.TURBO_SERVER_TOKEN }}" --team='rc' - name: Reset Meteor if: startsWith(github.ref, 'refs/tags/') == 'true' || github.ref == 'refs/heads/develop' @@ -142,144 +164,211 @@ jobs: if: startsWith(github.ref, 'refs/pull/') == true env: METEOR_PROFILE: 1000 - run: | - yarn build:ci -- --debug --directory /tmp/build-test + run: yarn build:ci --api="http://127.0.0.1:9080" -- --debug --directory dist - name: Build Rocket.Chat if: startsWith(github.ref, 'refs/pull/') != true - run: | - yarn build:ci -- --directory /tmp/build-test + run: yarn build:ci --api="http://127.0.0.1:9080" -- --directory dist - name: Prepare build run: | - mkdir /tmp/build/ - cd /tmp/build-test - tar czf /tmp/build/Rocket.Chat.tar.gz bundle - cd /tmp/build-test/bundle/programs/server - npm install --production - cd /tmp - tar czf Rocket.Chat.test.tar.gz ./build-test - - - name: Store build for tests - uses: actions/upload-artifact@v2 - with: - name: build-test - path: /tmp/Rocket.Chat.test.tar.gz + cd apps/meteor/dist + tar czf /tmp/Rocket.Chat.tar.gz bundle - name: Store build uses: actions/upload-artifact@v2 + with: + name: build + path: /tmp/Rocket.Chat.tar.gz + + build-docker-preview: + runs-on: ubuntu-20.04 + needs: [build, release-versions] + if: github.event_name == 'release' || github.ref == 'refs/heads/develop' + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'yarn' + + - name: Restore build + uses: actions/download-artifact@v2 with: name: build path: /tmp/build + - name: Unpack build + run: | + cd /tmp/build + tar xzf Rocket.Chat.tar.gz + rm Rocket.Chat.tar.gz + + - name: Build Docker image + id: build-docker-image-preview + uses: ./.github/actions/build-docker-image + with: + root-dir: /tmp/build + docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + release: preview + username: ${{ secrets.CR_USER }} + password: ${{ secrets.CR_PAT }} + test: runs-on: ubuntu-20.04 - needs: build + needs: [build, release-versions] strategy: matrix: - node-version: ['14.18.3'] - mongodb-version: ['3.6', '4.0', '4.2', '4.4', '5.0'] + node-version: ['14.19.3'] + mongodb-version: ['4.2', '4.4', '5.0'] steps: - name: Launch MongoDB - uses: wbari/start-mongoDB@v0.2 - with: - mongoDBVersion: ${{ matrix.mongodb-version }} --replSet=rs0 - - - name: Restore build for tests - uses: actions/download-artifact@v2 + uses: supercharge/mongodb-github-action@1.7.0 with: - name: build-test - path: /tmp + mongodb-version: ${{ matrix.mongodb-version }} + mongodb-replica-set: rs0 - - name: Decompress build - run: | - cd /tmp - tar xzf Rocket.Chat.test.tar.gz - cd - + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} + cache: 'yarn' - name: Setup Chrome - run: | - npm i chromedriver - - - name: Configure Replica Set - run: | - docker exec mongo mongo --eval 'rs.initiate({_id:"rs0", members: [{"_id":1, "host":"localhost:27017"}]})' - docker exec mongo mongo --eval 'rs.status()' + run: npm i chromedriver - - uses: actions/checkout@v3 + - name: yarn install + run: yarn - - name: Cache cypress - id: cache-cypress - uses: actions/cache@v2 + - name: TurboRepo local server + uses: felixmosh/turborepo-gh-artifacts@v1 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 - uses: actions/cache@v2 + repo-token: ${{ secrets.RC_TURBO_GH_TOKEN }} + server-token: ${{ secrets.TURBO_SERVER_TOKEN }} + + - name: Unit Test + run: yarn testunit --api="http://127.0.0.1:9080" --token="${{ secrets.TURBO_SERVER_TOKEN }}" --team='rc' + + - name: Restore build + uses: actions/download-artifact@v2 with: - path: | - ./node_modules/.turbo - key: ${{ runner.OS }}-turbo-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-turbo- - ${{ runner.os }}- + name: build + path: /tmp/build - - name: Yarn install - # if: steps.cache-nodemodules.outputs.cache-hit != 'true' || steps.cache-cypress.outputs.cache-hit != 'true' - run: yarn + - name: Unpack build + run: | + cd /tmp/build + tar xzf Rocket.Chat.tar.gz + rm Rocket.Chat.tar.gz - - name: Unit Test - run: yarn testunit + - name: Build Docker image + id: build-docker-image + if: matrix.mongodb-version != '5.0' + uses: ./.github/actions/build-docker-image + with: + root-dir: /tmp/build + docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + release: official + username: ${{ secrets.CR_USER }} + password: ${{ secrets.CR_PAT }} - - name: Install Playwright + - name: Build Alpine Docker image + id: build-docker-image-alpine + if: matrix.mongodb-version == '5.0' + uses: ./.github/actions/build-docker-image + with: + root-dir: /tmp/build + docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + release: alpine + username: ${{ secrets.CR_USER }} + password: ${{ secrets.CR_PAT }} + + # TODO move startup/restart to its own github action + - name: Start up Rocket.Chat run: | - cd ./apps/meteor - npx playwright install --with-deps + LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") + + # test alpine image on mongo 5.0 (no special reason to be mongo 5.0 but we need to test alpine at least once) + if [[ '${{ matrix.mongodb-version }}' = '5.0' ]]; then + IMAGE_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}.alpine" + else + IMAGE_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}.official" + fi; + + IMAGE_NAME="ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${IMAGE_TAG}" + + docker run --name rocketchat -d \ + --link mongodb \ + -p 3000:3000 \ + -e TEST_MODE=true \ + -e "MONGO_URL=mongodb://mongodb:27017/rocketchat?replicaSet=rs0&directConnection=true" \ + -e "MONGO_OPLOG_URL=mongodb://mongodb:27017/local?replicaSet=rs0&directConnection=true" \ + ${IMAGE_NAME} + + until echo "$(docker logs rocketchat)" | grep -q "SERVER RUNNING"; do + echo "Waiting Rocket.Chat to start up" + ((c++)) && ((c==10)) && exit 1 + sleep 10 + done - name: E2E Test API - env: - TEST_MODE: 'true' - MONGO_URL: mongodb://localhost:27017/rocketchat - MONGO_OPLOG_URL: mongodb://localhost:27017/local run: | + docker logs rocketchat --tail=50 + 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 5); do (docker exec mongo mongo rocketchat --eval 'db.dropDatabase()') && npm run testci -- --test=testapi && s=0 && break || s=$? && sleep 1; done; (exit $s) + for i in $(seq 1 5); do + docker stop rocketchat + docker exec mongodb mongo rocketchat --eval 'db.dropDatabase()' - - name: E2E Test UI (Legacy - Cypress) - env: - TEST_MODE: 'true' - MONGO_URL: mongodb://localhost:27017/rocketchat - MONGO_OPLOG_URL: mongodb://localhost:27017/local + NOW=$(date "+%Y-%m-%dT%H:%M:%SZ") + echo $NOW + + docker start rocketchat + + until echo "$(docker logs rocketchat --since $NOW)" | grep -q "SERVER RUNNING"; do + echo "Waiting Rocket.Chat to start up" + ((c++)) && ((c==10)) && exit 1 + sleep 10 + done + + npm run testapi && s=0 && break || s=$? && docker logs rocketchat --tail=100; + done; + exit $s + + - name: Install Playwright 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) + npx playwright install --with-deps - name: E2E Test UI - 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 & - docker exec mongo mongo rocketchat --eval 'db.dropDatabase()' && npm run testci -- --test=test:playwright + + docker logs rocketchat --tail=50 + + docker stop rocketchat + docker exec mongodb mongo rocketchat --eval 'db.dropDatabase()' + + NOW=$(date "+%Y-%m-%dT%H:%M:%SZ") + echo $NOW + + docker start rocketchat + + until echo "$(docker logs rocketchat --since $NOW)" | grep -q "SERVER RUNNING"; do + echo "Waiting Rocket.Chat to start up" + ((c++)) && ((c==10)) && exit 1 + sleep 10 + done + + cd ./apps/meteor + npm run test:e2e - name: Store playwright test trace uses: actions/upload-artifact@v2 @@ -288,383 +377,318 @@ 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 + needs: [build, release-versions] strategy: matrix: - node-version: ['14.18.3'] - mongodb-version: ['4.4'] + node-version: ['14.19.3'] + mongodb-version-ee: ['4.4'] steps: - name: Launch MongoDB - uses: wbari/start-mongoDB@v0.2 + uses: supercharge/mongodb-github-action@1.7.0 with: - mongoDBVersion: ${{ matrix.mongodb-version }} --replSet=rs0 + mongodb-version: ${{ matrix.mongodb-version-ee }} + mongodb-replica-set: rs0 - name: Launch NATS run: sudo docker run --name nats -d -p 4222:4222 nats:2.4 - - name: Restore build for tests - uses: actions/download-artifact@v2 - with: - name: build-test - path: /tmp - - - name: Decompress build - run: | - cd /tmp - tar xzf Rocket.Chat.test.tar.gz - cd - + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} + cache: 'yarn' - name: Setup Chrome - run: | - npm i chromedriver - - - name: Configure Replica Set - run: | - docker exec mongo mongo --eval 'rs.initiate({_id:"rs0", members: [{"_id":1, "host":"localhost:27017"}]})' - docker exec mongo mongo --eval 'rs.status()' - - - uses: actions/checkout@v3 + run: npm i chromedriver - - name: Cache cypress - id: cache-cypress - uses: actions/cache@v2 + - name: TurboRepo local server + uses: felixmosh/turborepo-gh-artifacts@v1 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 - uses: actions/cache@v2 - with: - path: | - ./node_modules/.turbo - key: ${{ runner.OS }}-turbo-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-turbo- - ${{ runner.os }}- + repo-token: ${{ secrets.RC_TURBO_GH_TOKEN }} + server-token: ${{ secrets.TURBO_SERVER_TOKEN }} - - name: Yarn install - # if: steps.cache-nodemodules.outputs.cache-hit != 'true' || steps.cache-cypress.outputs.cache-hit != 'true' + - name: yarn install run: yarn - - name: Build micro services - run: yarn build - - - name: E2E Test API - 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 - 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 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: Unit Test + run: yarn testunit --api="http://127.0.0.1:9080" --token="${{ secrets.TURBO_SERVER_TOKEN }}" --team='rc' - - name: Install Playwright - run: | - cd ./apps/meteor/ - npx playwright install --with-deps + - name: Restore build + uses: actions/download-artifact@v2 + with: + name: build + path: /tmp/build - - name: E2E Test UI - 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' + - name: Unpack build 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 - - docker exec mongo mongo rocketchat --eval 'db.dropDatabase()' && npm run testci -- --enterprise --test=test:playwright:ee + cd /tmp/build + tar xzf Rocket.Chat.tar.gz + rm Rocket.Chat.tar.gz - - name: Store playwright test trace - uses: actions/upload-artifact@v2 - if: failure() + - name: Build Docker image + id: build-docker-image + uses: ./.github/actions/build-docker-image with: - name: ee-playwright-test-trace - path: ./apps/meteor/tests/e2e/test-failures* + root-dir: /tmp/build + docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + release: official + username: ${{ secrets.CR_USER }} + password: ${{ secrets.CR_PAT }} - - name: Store cypress test screenshots - uses: actions/upload-artifact@v2 - if: failure() + - name: 'Build Docker image: account' + uses: ./.github/actions/build-docker-image-service with: - name: ee-cypress-test-screenshots - path: ./apps/meteor/tests/cypress/screenshots* + docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + service: account + username: ${{ secrets.CR_USER }} + password: ${{ secrets.CR_PAT }} - - name: Store cypress test videos - uses: actions/upload-artifact@v2 - if: failure() + - name: 'Build Docker image: authorization' + uses: ./.github/actions/build-docker-image-service with: - name: ee-cypress-test-videos - path: ./apps/meteor/tests/cypress/videos* - # notification: - # runs-on: ubuntu-20.04 - # needs: test - - # steps: - # - name: Rocket.Chat Notification - # uses: RocketChat/Rocket.Chat.GitHub.Action.Notification@1.1.1 - # with: - # type: ${{ job.status }} - # job_name: '**Build and Test**' - # url: ${{ secrets.ROCKETCHAT_WEBHOOK }} - # commit: true - # token: ${{ secrets.GITHUB_TOKEN }} - - build-image-pr: - runs-on: ubuntu-20.04 - if: github.event.pull_request.head.repo.full_name == github.repository - - strategy: - matrix: - release: ['official', 'preview'] - - steps: - - uses: actions/checkout@v3 + docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + service: authorization + username: ${{ secrets.CR_USER }} + password: ${{ secrets.CR_PAT }} - - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + - name: 'Build Docker image: ddp-streamer' + uses: ./.github/actions/build-docker-image-service with: - registry: ghcr.io + docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + service: ddp-streamer username: ${{ secrets.CR_USER }} password: ${{ secrets.CR_PAT }} - - name: Free disk space - run: | - sudo swapoff -a - sudo rm -f /swapfile - sudo apt clean - docker rmi $(docker image ls -aq) - df -h - - uses: c-hive/gha-yarn-cache@v2 - - name: Cache turbo - id: cache-turbo - uses: actions/cache@v2 + - name: 'Build Docker image: presence' + uses: ./.github/actions/build-docker-image-service with: - path: | - ./node_modules/.turbo - key: ${{ runner.OS }}-turbo-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-turbo- - ${{ runner.os }}- + docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + service: presence + username: ${{ secrets.CR_USER }} + password: ${{ secrets.CR_PAT }} - - name: Cache meteor local - uses: actions/cache@v2 - with: - path: ./apps/meteor/.meteor/local - key: ${{ runner.OS }}-meteor_cache-${{ hashFiles('.meteor/versions') }} - restore-keys: | - ${{ runner.os }}-meteor_cache- - ${{ runner.os }}- - - name: Cache meteor - uses: actions/cache@v2 - with: - path: ~/.meteor - key: ${{ runner.OS }}-meteor-${{ hashFiles('.meteor/release') }} - restore-keys: | - ${{ runner.os }}-meteor- - ${{ runner.os }}- - - name: Use Node.js 14.18.3 - uses: actions/setup-node@v3 + - name: 'Build Docker image: stream-hub' + uses: ./.github/actions/build-docker-image-service with: - node-version: '14.18.3' + docker-tag: ${{ needs.release-versions.outputs.gh-docker-tag }} + service: stream-hub + username: ${{ secrets.CR_USER }} + password: ${{ secrets.CR_PAT }} - - name: Install Meteor + - name: Launch Traefik run: | - # Restore bin from cache - set +e - METEOR_SYMLINK_TARGET=$(readlink ~/.meteor/meteor) - METEOR_TOOL_DIRECTORY=$(dirname "$METEOR_SYMLINK_TARGET") - set -e - LAUNCHER=$HOME/.meteor/$METEOR_TOOL_DIRECTORY/scripts/admin/launch-meteor - if [ -e $LAUNCHER ] - then - echo "Cached Meteor bin found, restoring it" - sudo cp "$LAUNCHER" "/usr/local/bin/meteor" - else - echo "No cached Meteor bin found." - fi - - # only install meteor if bin isn't found - command -v meteor >/dev/null 2>&1 || curl https://install.meteor.com | sed s/--progress-bar/-sL/g | /bin/sh + docker run --name traefik -d \ + -p 3000:80 \ + -v /var/run/docker.sock:/var/run/docker.sock \ + traefik:2.7 \ + --providers.docker=true - - name: Versions + # TODO move startup/restart to its own github action + - name: Start up Rocket.Chat run: | - npm --versions - yarn -v - node -v - meteor --version - meteor npm --versions - meteor node -v - git version + LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") - - name: Yarn install - # if: steps.cache-nodemodules.outputs.cache-hit != 'true' - run: yarn + docker run --name rocketchat -d \ + --link mongodb \ + --link nats \ + -e TEST_MODE=true \ + -e "MONGO_URL=mongodb://mongodb:27017/rocketchat?replicaSet=rs0&directConnection=true" \ + -e "MONGO_OPLOG_URL=mongodb://mongodb:27017/local?replicaSet=rs0&directConnection=true" \ + -e TRANSPORTER=nats://nats:4222 \ + -e MOLECULER_LOG_LEVEL=info \ + -e ENTERPRISE_LICENSE="${{ secrets.ENTERPRISE_LICENSE }}" \ + -e SKIP_PROCESS_EVENT_REGISTRATION=true \ + --label 'traefik.http.routers.rocketchat.rule=PathPrefix(`/`)' \ + ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${{ needs.release-versions.outputs.gh-docker-tag }}.official + + # spin up all micro services + docker run --name ddp-streamer -d \ + --link mongodb \ + --link nats \ + -e PORT=4000 \ + -e "MONGO_URL=mongodb://mongodb:27017/rocketchat?replicaSet=rs0&directConnection=true" \ + -e "MONGO_OPLOG_URL=mongodb://mongodb:27017/local?replicaSet=rs0&directConnection=true" \ + -e TRANSPORTER=nats://nats:4222 \ + -e MOLECULER_LOG_LEVEL=info \ + --label 'traefik.http.services.ddp-streamer.loadbalancer.server.port=4000' \ + --label 'traefik.http.routers.ddp-streamer.rule=PathPrefix(`/websocket`) || PathPrefix(`/sockjs`)' \ + ghcr.io/${LOWERCASE_REPOSITORY}/ddp-streamer-service:${{ needs.release-versions.outputs.gh-docker-tag }} + + - name: 'Start service: stream-hub' + run: | + LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") - # To reduce memory need during actual build, build the packages solely first - # - name: Build a Meteor cache - # run: | - # # to do this we can clear the main files and it build the rest - # echo "" > server/main.ts - # echo "" > client/main.ts - # sed -i.backup 's/rocketchat:livechat/#rocketchat:livechat/' .meteor/packages - # meteor build --server-only --debug --directory /tmp/build-temp - # git checkout -- server/main.ts client/main.ts .meteor/packages + docker run --name stream-hub -d \ + --link mongodb \ + --link nats \ + -e "MONGO_URL=mongodb://mongodb:27017/rocketchat?replicaSet=rs0&directConnection=true" \ + -e "MONGO_OPLOG_URL=mongodb://mongodb:27017/local?replicaSet=rs0&directConnection=true" \ + -e TRANSPORTER=nats://nats:4222 \ + -e MOLECULER_LOG_LEVEL=info \ + ghcr.io/${LOWERCASE_REPOSITORY}/stream-hub-service:${{ needs.release-versions.outputs.gh-docker-tag }} - - name: Build Rocket.Chat - run: yarn build:ci -- --directory /tmp/build-pr + until echo "$(docker logs stream-hub)" | grep -q "NetworkBroker started successfully"; do + echo "Waiting 'stream-hub' to start up" + ((c++)) && ((c==10)) && exit 1 + sleep 10 + done - - name: Build Docker image for PRs + - name: 'Start service: account' run: | - cd /tmp/build-pr + LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") + + docker run --name account -d \ + --link mongodb \ + --link nats \ + -e "MONGO_URL=mongodb://mongodb:27017/rocketchat?replicaSet=rs0&directConnection=true" \ + -e "MONGO_OPLOG_URL=mongodb://mongodb:27017/local?replicaSet=rs0&directConnection=true" \ + -e TRANSPORTER=nats://nats:4222 \ + -e MOLECULER_LOG_LEVEL=info \ + ghcr.io/${LOWERCASE_REPOSITORY}/account-service:${{ needs.release-versions.outputs.gh-docker-tag }} + until echo "$(docker logs account)" | grep -q "NetworkBroker started successfully"; do + echo "Waiting 'account' to start up" + ((c++)) && ((c==10)) && exit 1 + sleep 10 + done + + - name: 'Start service: authorization' + run: | LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") - IMAGE_NAME="rocket.chat" - if [[ '${{ matrix.release }}' = 'preview' ]]; then - IMAGE_NAME="${IMAGE_NAME}.preview" - fi; - IMAGE_NAME="ghcr.io/${LOWERCASE_REPOSITORY}/${IMAGE_NAME}:pr-${{ github.event.number }}" + docker run --name authorization -d \ + --link mongodb \ + --link nats \ + -e "MONGO_URL=mongodb://mongodb:27017/rocketchat?replicaSet=rs0&directConnection=true" \ + -e "MONGO_OPLOG_URL=mongodb://mongodb:27017/local?replicaSet=rs0&directConnection=true" \ + -e TRANSPORTER=nats://nats:4222 \ + -e MOLECULER_LOG_LEVEL=info \ + ghcr.io/${LOWERCASE_REPOSITORY}/authorization-service:${{ needs.release-versions.outputs.gh-docker-tag }} - echo "Build official Docker image ${IMAGE_NAME}" + until echo "$(docker logs authorization)" | grep -q "NetworkBroker started successfully"; do + echo "Waiting 'authorization' to start up" + ((c++)) && ((c==10)) && exit 1 + sleep 10 + done - DOCKER_PATH="${GITHUB_WORKSPACE}/apps/meteor/.docker" - if [[ '${{ matrix.release }}' = 'preview' ]]; then - DOCKER_PATH="${DOCKER_PATH}-mongo" - fi; + - name: 'Start service: presence' + run: | + LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") - echo "Build ${{ matrix.release }} Docker image" - cp ${DOCKER_PATH}/Dockerfile . - if [ -e ${DOCKER_PATH}/entrypoint.sh ]; then - cp ${DOCKER_PATH}/entrypoint.sh . - fi; + docker run --name presence -d \ + --link mongodb \ + --link nats \ + -e "MONGO_URL=mongodb://mongodb:27017/rocketchat?replicaSet=rs0&directConnection=true" \ + -e "MONGO_OPLOG_URL=mongodb://mongodb:27017/local?replicaSet=rs0&directConnection=true" \ + -e TRANSPORTER=nats://nats:4222 \ + -e MOLECULER_LOG_LEVEL=info \ + ghcr.io/${LOWERCASE_REPOSITORY}/presence-service:${{ needs.release-versions.outputs.gh-docker-tag }} + + until echo "$(docker logs presence)" | grep -q "NetworkBroker started successfully"; do + echo "Waiting 'presence' to start up" + ((c++)) && ((c==10)) && exit 1 + sleep 10 + done - docker build -t $IMAGE_NAME . - docker push $IMAGE_NAME + - name: E2E Test API + run: | + cd ./apps/meteor + for i in $(seq 1 5); do + docker stop rocketchat + docker stop stream-hub + docker stop account + docker stop authorization + docker stop ddp-streamer + docker stop presence + + docker exec mongodb mongo rocketchat --eval 'db.dropDatabase()' + + NOW=$(date "+%Y-%m-%dT%H:%M:%SZ") + echo $NOW + + docker start rocketchat + docker start stream-hub + docker start account + docker start authorization + docker start ddp-streamer + docker start presence + + until echo "$(docker logs rocketchat --since $NOW)" | grep -q "SERVER RUNNING"; do + echo "Waiting Rocket.Chat to start up" + ((c++)) && ((c==10)) && exit 1 + sleep 10 + done + + docker logs rocketchat --tail=50 + docker logs stream-hub --tail=50 + docker logs account --tail=50 + docker logs authorization --tail=50 + docker logs ddp-streamer --tail=50 + docker logs presence --tail=50 + + npm run testapi && s=0 && break || s=$? && docker logs rocketchat --tail=100 && docker logs authorization --tail=50; + done; + exit $s - services-image-build-check: - runs-on: ubuntu-20.04 - if: github.event.pull_request.head.repo.full_name == github.repository + - name: Install Playwright + run: | + cd ./apps/meteor + npx playwright install --with-deps - strategy: - matrix: - service: ['ddp-streamer'] + - name: E2E Test UI + 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 & - steps: - - uses: actions/checkout@v3 + docker logs rocketchat --tail=50 - - name: Use Node.js 14.18.3 - uses: actions/setup-node@v3 - with: - node-version: '14.18.3' + docker stop rocketchat + docker stop stream-hub + docker stop account + docker stop authorization + docker stop ddp-streamer + docker stop presence - - uses: c-hive/gha-yarn-cache@v2 - - name: Cache turbo - id: cache-turbo - uses: actions/cache@v2 - with: - path: | - ./node_modules/.turbo - key: ${{ runner.OS }}-turbo-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-turbo- - ${{ runner.os }}- + docker exec mongodb mongo rocketchat --eval 'db.dropDatabase()' - - name: Build Docker images - env: - IMAGE_TAG: check - run: | - yarn - yarn build + NOW=$(date "+%Y-%m-%dT%H:%M:%SZ") + echo $NOW - echo "Building Docker image for service: ${{ matrix.service }}:${IMAGE_TAG}" + docker start rocketchat + docker start stream-hub + docker start account + docker start authorization + docker start ddp-streamer + docker start presence - docker build \ - --build-arg SERVICE=${{ matrix.service }} \ - -t rocketchat/${{ matrix.service }}-service:${IMAGE_TAG} \ - -f ./ee/apps/ddp-streamer/Dockerfile \ - . + until echo "$(docker logs rocketchat --since $NOW)" | grep -q "SERVER RUNNING"; do + echo "Waiting Rocket.Chat to start up" + ((c++)) && ((c==10)) && exit 1 + sleep 10 + done - release-versions: - runs-on: ubuntu-latest - outputs: - release: ${{ steps.by-tag.outputs.release }} - latest-release: ${{ steps.latest.outputs.latest-release }} - steps: - - id: by-tag - run: | - if echo "$GITHUB_REF_NAME" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' ; then - RELEASE="latest" - elif echo "$GITHUB_REF_NAME" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$' ; then - RELEASE="release-candidate" - fi - echo "RELEASE: ${RELEASE}" - echo "::set-output name=release::${RELEASE}" + docker logs rocketchat --tail=50 + docker logs stream-hub --tail=50 + docker logs account --tail=50 + docker logs authorization --tail=50 + docker logs ddp-streamer --tail=50 + docker logs presence --tail=50 - - id: latest - run: | - LATEST_RELEASE="$( - git -c 'versionsort.suffix=-' ls-remote -t --exit-code --refs --sort=-v:refname "https://github.com/$GITHUB_REPOSITORY" '*' | - sed -En '1!q;s/^[[:xdigit:]]+[[:space:]]+refs\/tags\/(.+)/\1/gp' - )" - echo "LATEST_RELEASE: ${LATEST_RELEASE}" - echo "::set-output name=latest-release::${LATEST_RELEASE}" + cd ./apps/meteor + IS_EE=true npm run test:e2e + + - name: Store playwright test trace + uses: actions/upload-artifact@v2 + if: failure() + with: + name: playwright-test-trace + path: ./apps/meteor/tests/e2e/test-failures* deploy: runs-on: ubuntu-20.04 @@ -730,7 +754,7 @@ jobs: aws s3 cp $ROCKET_DEPLOY_DIR/ s3://download.rocket.chat/build/ --recursive curl -H "Content-Type: application/json" -H "X-Update-Token: $UPDATE_TOKEN" -d \ - "{\"nodeVersion\": \"14.18.3\", \"compatibleMongoVersions\": [\"3.6\", \"4.0\", \"4.2\", \"4.4\", \"5.0\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"$RC_RELEASE\"}" \ + "{\"nodeVersion\": \"14.19.3\", \"compatibleMongoVersions\": [\"4.2\", \"4.4\", \"5.0\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"$RC_RELEASE\"}" \ https://releases.rocket.chat/update # Makes build fail if the release isn't there @@ -745,114 +769,88 @@ jobs: -d '{"tag":"'$GIT_TAG'"}' fi - image-build: + docker-image-publish: runs-on: ubuntu-20.04 - needs: [deploy, release-versions] + needs: [deploy, build-docker-preview, release-versions] strategy: matrix: - # this is current a mix of variants and different images + # this is currently a mix of variants and different images release: ['official', 'preview', 'alpine'] env: IMAGE_NAME: 'rocketchat/rocket.chat' steps: - - uses: actions/checkout@v3 - - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASS }} - - name: Restore build - uses: actions/download-artifact@v2 + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 with: - name: build - path: /tmp/build + registry: ghcr.io + username: ${{ secrets.CR_USER }} + password: ${{ secrets.CR_PAT }} - - name: Unpack build and prepare Docker files + - name: Get Docker image name + id: gh-docker run: | - cd /tmp/build - tar xzf Rocket.Chat.tar.gz - rm Rocket.Chat.tar.gz + LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") - DOCKER_PATH="${GITHUB_WORKSPACE}/apps/meteor/.docker" - if [[ '${{ matrix.release }}' = 'preview' ]]; then - DOCKER_PATH="${DOCKER_PATH}-mongo" - fi; + GH_IMAGE_NAME="ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${{ needs.release-versions.outputs.gh-docker-tag }}.${{ matrix.release }}" - DOCKERFILE_PATH="${DOCKER_PATH}/Dockerfile" - if [[ '${{ matrix.release }}' = 'alpine' ]]; then - DOCKERFILE_PATH="${DOCKERFILE_PATH}.${{ matrix.release }}" - fi; + echo "GH_IMAGE_NAME: $GH_IMAGE_NAME" - echo "Copy Dockerfile for release: ${{ matrix.release }}" - cp $DOCKERFILE_PATH ./Dockerfile - if [ -e ${DOCKER_PATH}/entrypoint.sh ]; then - cp ${DOCKER_PATH}/entrypoint.sh . - fi; - - - name: Build Docker image for tag - if: github.event_name == 'release' - run: | - cd /tmp/build + echo "::set-output name=gh-image-name::${GH_IMAGE_NAME}" - DOCKER_TAG=$GITHUB_REF_NAME + - name: Pull Docker image + run: docker pull ${{ steps.gh-docker.outputs.gh-image-name }} + - name: Publish Docker image + run: | if [[ '${{ matrix.release }}' = 'preview' ]]; then IMAGE_NAME="${IMAGE_NAME}.preview" fi; + # 'develop' or 'tag' + DOCKER_TAG=$GITHUB_REF_NAME + # append the variant name to docker tag if [[ '${{ matrix.release }}' = 'alpine' ]]; then DOCKER_TAG="${DOCKER_TAG}-${{ matrix.release }}" fi; - RELEASE="${{ needs.release-versions.outputs.release }}" - - if [[ '${{ matrix.release }}' = 'alpine' ]]; then - RELEASE="${RELEASE}-${{ matrix.release }}" - fi; - echo "IMAGE_NAME: $IMAGE_NAME" echo "DOCKER_TAG: $DOCKER_TAG" - echo "RELEASE: $RELEASE" - # build and push the specific tag version - docker build -t $IMAGE_NAME:$DOCKER_TAG . + # tag and push the specific tag version + docker tag ${{ steps.gh-docker.outputs.gh-image-name }} $IMAGE_NAME:$DOCKER_TAG docker push $IMAGE_NAME:$DOCKER_TAG - if [[ $RELEASE == 'latest' ]]; then - if [[ '${{ needs.release-versions.outputs.latest-release }}' == $GITHUB_REF_NAME ]]; then - docker tag $IMAGE_NAME:$DOCKER_TAG $IMAGE_NAME:$RELEASE - docker push $IMAGE_NAME:$RELEASE - fi - else - docker tag $IMAGE_NAME:$DOCKER_TAG $IMAGE_NAME:$RELEASE - docker push $IMAGE_NAME:$RELEASE - fi - - - name: Build Docker image for develop - if: github.ref == 'refs/heads/develop' - run: | - cd /tmp/build - - DOCKER_TAG=develop + if [[ $GITHUB_REF == refs/tags/* ]]; then + RELEASE="${{ needs.release-versions.outputs.release }}" - if [[ '${{ matrix.release }}' = 'preview' ]]; then - IMAGE_NAME="${IMAGE_NAME}.preview" - fi; + if [[ '${{ matrix.release }}' = 'alpine' ]]; then + RELEASE="${RELEASE}-${{ matrix.release }}" + fi; - if [[ '${{ matrix.release }}' = 'alpine' ]]; then - DOCKER_TAG="${DOCKER_TAG}-${{ matrix.release }}" - fi; + echo "RELEASE: $RELEASE" - docker build -t $IMAGE_NAME:$DOCKER_TAG . - docker push $IMAGE_NAME:$DOCKER_TAG + if [[ $RELEASE == 'latest' ]]; then + if [[ '${{ needs.release-versions.outputs.latest-release }}' == $GITHUB_REF_NAME ]]; then + docker tag ${{ steps.gh-docker.outputs.gh-image-name }} $IMAGE_NAME:$RELEASE + docker push $IMAGE_NAME:$RELEASE + fi + else + docker tag ${{ steps.gh-docker.outputs.gh-image-name }} $IMAGE_NAME:$RELEASE + docker push $IMAGE_NAME:$RELEASE + fi + fi - services-image-build: + services-docker-image-publish: runs-on: ubuntu-20.04 needs: [deploy, release-versions] @@ -861,57 +859,35 @@ jobs: service: ['account', 'authorization', 'ddp-streamer', 'presence', 'stream-hub'] steps: - - uses: actions/checkout@v3 - - - name: Use Node.js 14.18.3 - uses: actions/setup-node@v3 - with: - node-version: '14.18.3' - - uses: c-hive/gha-yarn-cache@v2 - - name: Cache turbo - id: cache-turbo - uses: actions/cache@v2 - with: - path: | - ./node_modules/.turbo - key: ${{ runner.OS }}-turbo-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-turbo- - ${{ runner.os }}- - - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASS }} - - name: Build Docker images + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ secrets.CR_USER }} + password: ${{ secrets.CR_PAT }} + + - name: Publish Docker images run: | - # defines image tag - if [[ $GITHUB_REF == refs/tags/* ]]; then - IMAGE_TAG="${GITHUB_REF#refs/tags/}" - else - IMAGE_TAG="${GITHUB_REF#refs/heads/}" - fi + LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") - # first install repo dependencies - yarn - yarn build + IMAGE_TAG="${{ needs.release-versions.outputs.gh-docker-tag }}" - echo "Building Docker image for service: ${{ matrix.service }}:${IMAGE_TAG}" + GH_IMAGE_NAME="ghcr.io/${LOWERCASE_REPOSITORY}/${{ matrix.service }}-service:${IMAGE_TAG}" - if [[ "${{ matrix.service }}" == "ddp-streamer" ]]; then - DOCKERFILE_PATH="./ee/apps/ddp-streamer/Dockerfile" - else - DOCKERFILE_PATH="./apps/meteor/ee/server/services/Dockerfile" - fi + echo "GH_IMAGE_NAME: $GH_IMAGE_NAME" + + docker pull $GH_IMAGE_NAME - docker build \ - --build-arg SERVICE=${{ matrix.service }} \ - -t rocketchat/${{ matrix.service }}-service:${IMAGE_TAG} \ - -f ${DOCKERFILE_PATH} \ - . + # 'develop' or 'tag' + DOCKER_TAG=$GITHUB_REF_NAME + docker tag $GH_IMAGE_NAME rocketchat/${{ matrix.service }}-service:${IMAGE_TAG} docker push rocketchat/${{ matrix.service }}-service:${IMAGE_TAG} if [[ $GITHUB_REF == refs/tags/* ]]; then diff --git a/.github/workflows/no-new-js-files.yml b/.github/workflows/no-new-js-files.yml index 6a7473643de3..77045946b52c 100644 --- a/.github/workflows/no-new-js-files.yml +++ b/.github/workflows/no-new-js-files.yml @@ -1,4 +1,4 @@ -name: "JS file preventer" +name: 'JS file preventer' on: pull_request: types: [opened, synchronize] diff --git a/.github/workflows/pr-title-checker.yml b/.github/workflows/pr-title-checker.yml index c31a3aef54d2..7c3ec70779c2 100644 --- a/.github/workflows/pr-title-checker.yml +++ b/.github/workflows/pr-title-checker.yml @@ -1,4 +1,4 @@ -name: "PR Title Checker" +name: 'PR Title Checker' on: pull_request: types: [opened, edited] @@ -9,4 +9,4 @@ jobs: steps: - uses: thehanimo/pr-title-checker@v1.3.4 with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.RC_TITLE_CHECKER }} 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/.kodiak.toml b/.kodiak.toml new file mode 100644 index 000000000000..b2f9e45da47e --- /dev/null +++ b/.kodiak.toml @@ -0,0 +1,19 @@ +# .kodiak.toml +version = 1 + +[merge] +method = "squash" +automerge_label = ["stat: ready to merge", "automerge"] +block_on_neutral_required_check_runs = true +blocking_labels = ["stat: needs QA", "Invalid PR Title"] +prioritize_ready_to_merge = true + +[merge.message] +title = "pull_request_title" +body = "empty" +include_coauthors=true + +[merge.automerge_dependencies] +versions = ["minor", "patch"] +usernames = ["dependabot"] + 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..18948c0be5b9 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -11,5 +11,5 @@ plugins: spec: '@yarnpkg/plugin-typescript' yarnPath: .yarn/releases/yarn-3.2.0.cjs - checksumBehavior: 'update' +enableImmutableInstalls: false diff --git a/HISTORY.md b/HISTORY.md index cca3fcd54ce5..6155150e03f0 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,87 +1,127 @@ -# 4.8.2 -`2022-07-20 · 4 🐛 · 1 🔍 · 2 👩‍💻👨‍💻` +# 5.0.0 +`2022-07-22 · 14 ️️️⚠️ · 33 🎉 · 20 🚀 · 110 🐛 · 389 🔍 · 63 👩‍💻👨‍💻` ### Engine versions -- Node: `14.18.3` -- NPM: `6.14.15` -- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- Node: `14.19.3` +- NPM: `6.14.17` +- MongoDB: `4.2, 4.4, 5.0` -### 🐛 Bug fixes +### ⚠️ BREAKING CHANGES -- Error "numRequestsAllowed" property in rateLimiter for REST API endpoint when upgrading ([#26058](https://github.com/RocketChat/Rocket.Chat/pull/26058)) +- Chore: Remove unused tokenpass integration code ([#25831](https://github.com/RocketChat/Rocket.Chat/pull/25831)) -- Not showing edit message button when blocking edit after N minutes ([#25724](https://github.com/RocketChat/Rocket.Chat/pull/25724) by [@matthias4217](https://github.com/matthias4217)) +- Deactivated team members are added to auto-join rooms ([#25016](https://github.com/RocketChat/Rocket.Chat/pull/25016)) - Previously, in Rocketchat 4.7.0 and later, as mentioned in https://github.com/RocketChat/Rocket.Chat/issues/25478, the edit button was not displayed on the interface in the minute after having sent a message. This is now fixed : messages can be edited right after sending them. + - Do not add deactivated users to auto-join rooms. -- Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) +- Remove Blockstack authentication ([#25649](https://github.com/RocketChat/Rocket.Chat/pull/25649)) -- Settings not being overwritten to their default values ([#25891](https://github.com/RocketChat/Rocket.Chat/pull/25891)) + Blockstack authentication is broken and is preventing some dependencies to be up to date. As a migration to Stacks authentication is not trivial, we've opted for removing the authentication service. -
-🔍 Minor changes +- Remove RDStation integration ([#25774](https://github.com/RocketChat/Rocket.Chat/pull/25774)) +- Remove show message in main thread preference ([#26002](https://github.com/RocketChat/Rocket.Chat/pull/26002)) -- Chore: Avoid unneeded permission updates when EE license is applied ([#26253](https://github.com/RocketChat/Rocket.Chat/pull/26253)) + This PR removes the confusion between the `show message in main thread` and the function `also to send to channel`. In the past, we used the `show message in main thread` as a solution to help users to understand the thread feature, as this feature is now mature enough there's no reason to maintain this preference. + + Send the thread message to the main channel or just inside of the thread, should be a decision from the user where the function `also send to channel` appears. Because of that, and because of a bunch of requests and issues we received, we're introducing a new preference `also send thread to channel` where users will be able to decide the behavior of the checkbox. + + ![image](https://user-images.githubusercontent.com/27704687/175655594-023c5907-adc8-4924-ba7d-467608d06fec.png) + + Now there are three behavior options + - `Default`: when it unchecks after sending the first message + + + - `Always`: stay checked for all messages + + + - `Never`: stay unchecked for all messages + -
+- Remove support to old MongoDB versions ([#26098](https://github.com/RocketChat/Rocket.Chat/pull/26098)) -### 👩‍💻👨‍💻 Contributors 😍 + As per MongoDB Lifecycle Schedules (https://www.mongodb.com/support-policy/lifecycles) we're removing official support to MongoDB versions **3.6 and 4.0** that have already reached end-of-life. + + As MongoDB 4.2 was a "supported" version before Rocket.Chat 5.0, we'll continue supporting it, but will be flagged as deprecated. We recommend upgrading to MongoDB 4.4+. + + Here are official docs on how to upgrade to some of the supported versions: + + - https://www.mongodb.com/docs/manual/release-notes/4.2-upgrade-replica-set/ + - https://www.mongodb.com/docs/v4.4/release-notes/4.4-upgrade-replica-set/ + - https://www.mongodb.com/docs/manual/release-notes/5.0-upgrade-replica-set/ -- [@matthias4217](https://github.com/matthias4217) +- remove unused endpoints and restify others ([#25889](https://github.com/RocketChat/Rocket.Chat/pull/25889)) -### 👩‍💻👨‍💻 Core Team 🤓 +- Remove webRTC for channels/dm/groups ([#26225](https://github.com/RocketChat/Rocket.Chat/pull/26225)) -- [@sampaiodiego](https://github.com/sampaiodiego) +- Suspend push notifications when login token is invalidated ([#20913](https://github.com/RocketChat/Rocket.Chat/pull/20913) by [@g-thome](https://github.com/g-thome)) -# 4.8.1 -`2022-06-08 · 4 🐛 · 5 👩‍💻👨‍💻` + link the auth token to the push token -### Engine versions -- Node: `14.18.3` -- NPM: `6.14.15` -- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- Upgrade to version 5.0 can be done only from version 4.x ([#26100](https://github.com/RocketChat/Rocket.Chat/pull/26100)) -### 🐛 Bug fixes +- use urlParams on omnichannel/agent/extension/ ([#25982](https://github.com/RocketChat/Rocket.Chat/pull/25982)) +- use urlParams on omnichannel/agent/extension/ ([#25874](https://github.com/RocketChat/Rocket.Chat/pull/25874)) -- AccountBox checks for condition ([#25708](https://github.com/RocketChat/Rocket.Chat/pull/25708)) +- use urlParams on omnichannel/agent/extension/" ([#25980](https://github.com/RocketChat/Rocket.Chat/pull/25980)) -- Bump meteor-node-stubs to version 1.2.3 ([#25669](https://github.com/RocketChat/Rocket.Chat/pull/25669) by [@Sh0uld](https://github.com/Sh0uld)) +- VideoConference ([#25570](https://github.com/RocketChat/Rocket.Chat/pull/25570)) - With meteor-node-stubs version 1.2.3 a bug was fixed, which occured in issue #25460 and probably #25513 (last one not tested). - For the issue in meteor see: https://github.com/meteor/meteor/issues/11974 + In this PR we're deprecating the Video Conference functionality from the core of the application and introducing a **new video conference flow**: + + + + Now the video conference feature will be agnostic so you'll be able to set the provider such as **Jisti** and **BBB** as apps from our marketplace: + + + + Video conferences settings are now global, allowing you to set the default provider + + + + ### [Enterprise Features] + - Video Conferences List + + + - Ringing function for direct messages + + + + -- Fix prom-client new promise usage ([#25781](https://github.com/RocketChat/Rocket.Chat/pull/25781)) +### 🎉 New features -- Wrong argument name preventing Omnichannel Chat Forward to User ([#25723](https://github.com/RocketChat/Rocket.Chat/pull/25723)) -### 👩‍💻👨‍💻 Contributors 😍 +- **APPS:** Allow apps to modify a subset of global settings ([#25913](https://github.com/RocketChat/Rocket.Chat/pull/25913)) -- [@Sh0uld](https://github.com/Sh0uld) +- **APPS:** Allow dispatchment of actions from input elements ([#25949](https://github.com/RocketChat/Rocket.Chat/pull/25949)) -### 👩‍💻👨‍💻 Core Team 🤓 + This allows for apps receiving block actions when a user types on a plain text input field or selects an item from the static. A debounce of 700 ms is done when listening for typing action so the app is not flooded with actions. + + + https://user-images.githubusercontent.com/733282/174858175-5ea53046-c791-493e-859b-b80431e94ffa.mp4 -- [@KevLehman](https://github.com/KevLehman) -- [@dudanogueira](https://github.com/dudanogueira) -- [@ggazzo](https://github.com/ggazzo) -- [@tiagoevanp](https://github.com/tiagoevanp) +- **APPS:** Allowing apps to register authenticated routes ([#25937](https://github.com/RocketChat/Rocket.Chat/pull/25937)) -# 4.8.0 -`2022-05-31 · 16 🎉 · 13 🚀 · 55 🐛 · 151 🔍 · 52 👩‍💻👨‍💻` + Adds adaptations that allow apps to declare an API endpoint that requires authorization from Rocket.Chat prior to executing -### Engine versions -- Node: `14.18.3` -- NPM: `6.14.15` -- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- **ENTERPRISE:** Device Management ([#25791](https://github.com/RocketChat/Rocket.Chat/pull/25791)) -### 🎉 New features +- **ENTERPRISE:** Introducing dial pad component into sidebar, calls table, contextual bar ([#26081](https://github.com/RocketChat/Rocket.Chat/pull/26081)) + This PR adds a new call button that can be used from Sidebar & Contact Center. This also enables Omnichannel agents to make outbound calls from within Rocket.Chat. + + Depending on your server and call server configuration, you can do international calling, national and domestic calling. + + The buttons on Contact Center allows an agent to call an existing number without having to type the number again. - Ability for RC server to check the business hour for a specific department ([#25436](https://github.com/RocketChat/Rocket.Chat/pull/25436)) +- Accept quoted slash command arguments ([#11744](https://github.com/RocketChat/Rocket.Chat/pull/11744) by [@Hudell](https://github.com/Hudell)) + - Add expire index to integration history ([#25087](https://github.com/RocketChat/Rocket.Chat/pull/25087)) - Add new app events for pin, react and follow message ([#25337](https://github.com/RocketChat/Rocket.Chat/pull/25337)) @@ -100,10 +140,45 @@ https://user-images.githubusercontent.com/51996/164530391-e8b17ecd-a4d0-4ef8-a8b7-81230c1773d3.mp4 +- Colors Palette - Buttons ([#25626](https://github.com/RocketChat/Rocket.Chat/pull/25626)) + +- Community Edition Watermark ([#25844](https://github.com/RocketChat/Rocket.Chat/pull/25844)) + +- Create releases tab in the marketplace app info page ([#25965](https://github.com/RocketChat/Rocket.Chat/pull/25965)) + + Added a Releases tab to the app info page of installed marketplace apps. This tab will show all the released versions of a given app with its version number, release date in humanized form, and the changelog of this given release with the information provided by the publisher, this changelog accepts and renders markdown. Also refactored some component names and logic for maintainability reasons. + Demo gif: + ![app-releases-tab-final](https://user-images.githubusercontent.com/43561537/176228928-651074ce-1f8b-4531-95be-1dd107938bf3.gif) + +- Create Team with a member list of usernames ([#25868](https://github.com/RocketChat/Rocket.Chat/pull/25868)) + +- Enable outbound calling for EE (#25843) ([#25960](https://github.com/RocketChat/Rocket.Chat/pull/25960)) + +- Engagement Metrics - Phase 2 ([#25505](https://github.com/RocketChat/Rocket.Chat/pull/25505)) + + Add the following new statistics (metrics): + - Total Broadcast rooms + - Total rooms with an active Livestream; + - Total triggered emails; + - Total subscription roles; + - Total User Roles; + - Total uncaught exceptions; + - `homeTitleChanged`: boolean value to indicate whether the `Layout_Home_Title` setting has been changed; + - `homeBodyChanged`: boolean value to indicate whether the `Layout_Home_Body` setting has been changed; + - `customCSSChanged`: boolean value to indicate whether the `theme-custom-css` setting has been changed; + - `onLogoutCustomScriptChanged`: boolean value to indicate whether the `Custom_Script_On_Logout` setting has been changed; + - `loggedOutCustomScriptChanged`: boolean value to indicate whether the `Custom_Script_Logged_Out` setting has been changed; + - `loggedInCustomScriptChanged`: boolean value to indicate whether the `Custom_Script_Logged_In` setting has been changed; + - `matrixBridgeEnabled`: boolean value to indicate whether the Matrix bridge has been enabled; + - Expand Apps Engine's environment variable allowed list ([#23870](https://github.com/RocketChat/Rocket.Chat/pull/23870) by [@cuonghuunguyen](https://github.com/cuonghuunguyen)) - Federation (Alpha Stabilization) ([#25457](https://github.com/RocketChat/Rocket.Chat/pull/25457)) +- Fuselage ToastBar ([#25583](https://github.com/RocketChat/Rocket.Chat/pull/25583)) + + ![Kapture 2022-05-20 at 14 50 19](https://user-images.githubusercontent.com/27704687/169584462-270e73aa-6dbe-4045-9847-d429125f15a6.gif) + - Get user's preferred language via apps ([#25514](https://github.com/RocketChat/Rocket.Chat/pull/25514)) - Marketplace new app details page ([#24711](https://github.com/RocketChat/Rocket.Chat/pull/24711)) @@ -159,6 +234,14 @@ Demo gif: ![new_carousel_component](https://user-images.githubusercontent.com/43561537/167415212-9d8359c7-4132-4afa-a698-8be4ab1e1393.gif) +- Marketplace security tab app info page ([#25739](https://github.com/RocketChat/Rocket.Chat/pull/25739)) + + Created a new security tab for installed apps that displays information related to the given app security policies, terms of services, and necessary permissions for the use of the app. + Demo gif: + ![privacy-tab](https://user-images.githubusercontent.com/43561537/173878394-333057d4-3c7e-434e-a3ca-d3e08f33c7bc.gif) + +- Matrix Federation UX improvements ([#25847](https://github.com/RocketChat/Rocket.Chat/pull/25847)) + - Message Template React Component ([#23971](https://github.com/RocketChat/Rocket.Chat/pull/23971)) Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template). @@ -190,8 +273,6 @@ - Total link invitations; - Total email invitations; - Logo change; - - Number of custom script lines; - - Number of custom CSS lines; - Number of rooms inside teams; - Number of default (auto-join) rooms inside teams; - Number of users created through link invitation; @@ -200,6 +281,12 @@ - Star message, report and delete message events ([#25383](https://github.com/RocketChat/Rocket.Chat/pull/25383)) +- Use setting to determine if initial general channel is needed ([#25441](https://github.com/RocketChat/Rocket.Chat/pull/25441) by [@felipe-menelau](https://github.com/felipe-menelau)) + + - Adds flag responsible for overwriting #general channel creation + +- VoIP Input/Output Device Selection ([#25966](https://github.com/RocketChat/Rocket.Chat/pull/25966)) + ### 🚀 Improvements @@ -229,10 +316,33 @@ - Added tooltip options for message menu ([#24431](https://github.com/RocketChat/Rocket.Chat/pull/24431) by [@Himanshu664](https://github.com/Himanshu664)) +- Avoid using omnichannel-queue collection ([#25491](https://github.com/RocketChat/Rocket.Chat/pull/25491)) + +- Differ Voip calls from Incoming and Outgoing ([#25643](https://github.com/RocketChat/Rocket.Chat/pull/25643)) + + Updated this column and its respective endpoints to support inbound/outfound call definitions + ![image](https://user-images.githubusercontent.com/34130764/170512008-34202ed8-3ed4-4c28-baa5-25efc17543d5.png) + +- Expand the feature set of the new message rendering ([#25970](https://github.com/RocketChat/Rocket.Chat/pull/25970)) + + - Everything inside a new package (`@rocket.chat/gazzodown`); + - KaTeX support; + - Highlighted Words support; + - Emoji rendering expanded; + - Code rendering fixed + - Fix multiple bugs with Matrix bridge ([#25318](https://github.com/RocketChat/Rocket.Chat/pull/25318)) - Improve active/hover colors in account sidebar ([#25024](https://github.com/RocketChat/Rocket.Chat/pull/25024) by [@Himanshu664](https://github.com/Himanshu664)) +- Moved call hold/unhold to EE ([#26007](https://github.com/RocketChat/Rocket.Chat/pull/26007)) + + This PR adds a restriction, enabling the feature to hold/unhold calls only for Enterprise Edition users. + +- Moved call wrap up modal to EE ([#25875](https://github.com/RocketChat/Rocket.Chat/pull/25875)) + + This PR adds a restriction, enabling the feature to display the call wrap up modal only for Enterprise Edition users. + - New admin settings Page ([#25439](https://github.com/RocketChat/Rocket.Chat/pull/25439)) ![Screen Shot 2022-05-09 at 11 31 58](https://user-images.githubusercontent.com/27704687/167432811-f4970f23-5dae-48a0-a427-92269d08a859.png) @@ -241,15 +351,34 @@ - Performance for some Omnichannel features ([#25217](https://github.com/RocketChat/Rocket.Chat/pull/25217)) +- Refactor + unit tests for federation-v2 ([#25680](https://github.com/RocketChat/Rocket.Chat/pull/25680)) + + The main goal for this PR is to add the ability to add tests in our current federation-v2 implementation. + In this PR, I've added only unit tests (80%), but the goal is to add other kinds of tests in the near future. + + Also, I've created a diagram to show how this refactor was done, and how is the structure of the code + + ![image](https://user-images.githubusercontent.com/15324204/171039619-22168000-3626-424e-b408-18dea540f786.png) + - Rename upgrade tab routes ([#25097](https://github.com/RocketChat/Rocket.Chat/pull/25097)) Change 'upgrade tab' routes names from camelCase ('goFullyFeatured') to kebab-case ('go-fully-featured') due to URL naming consistency. Changed types, main function and test. - Unify voip streams into single stream ([#25108](https://github.com/RocketChat/Rocket.Chat/pull/25108)) +- VoIP admin page cleanup: remove unused settings ([#25993](https://github.com/RocketChat/Rocket.Chat/pull/25993)) + + https://app.clickup.com/t/2n4m61m + ### 🐛 Bug fixes +- `You and @yourUsername reacted with`title on reactions ([#25733](https://github.com/RocketChat/Rocket.Chat/pull/25733)) + +- Access issue on chat.getThreadsList ([#25750](https://github.com/RocketChat/Rocket.Chat/pull/25750)) + +- AccountBox checks for condition ([#25708](https://github.com/RocketChat/Rocket.Chat/pull/25708)) + - Add katex render to new message react template ([#25239](https://github.com/RocketChat/Rocket.Chat/pull/25239)) - Add open user card to user avatar ([#25445](https://github.com/RocketChat/Rocket.Chat/pull/25445)) @@ -279,6 +408,21 @@ - After: ![image](https://user-images.githubusercontent.com/30026625/161831092-9ee77b51-b083-4f45-9c48-ab2e0511c4d6.png) +- AgentsPage pagination ([#25820](https://github.com/RocketChat/Rocket.Chat/pull/25820)) + +- allow only livechat-agents to be contact manager for any omnichannel contact ([#25451](https://github.com/RocketChat/Rocket.Chat/pull/25451)) + +- Append path To Route For Custom Emoji ([#24379](https://github.com/RocketChat/Rocket.Chat/pull/24379)) + +- Attachments and OEmbed margins ([#25713](https://github.com/RocketChat/Rocket.Chat/pull/25713)) + +- Broken Omnichannel>Agents page ([#25731](https://github.com/RocketChat/Rocket.Chat/pull/25731)) + +- Bump meteor-node-stubs to version 1.2.3 ([#25669](https://github.com/RocketChat/Rocket.Chat/pull/25669) by [@Sh0uld](https://github.com/Sh0uld)) + + With meteor-node-stubs version 1.2.3 a bug was fixed, which occured in issue #25460 and probably #25513 (last one not tested). + For the issue in meteor see: https://github.com/meteor/meteor/issues/11974 + - Change form body parameter charset to UTF-8 to fix issue #25456 ([#25673](https://github.com/RocketChat/Rocket.Chat/pull/25673) by [@divinespear](https://github.com/divinespear)) since [mscdex/busboy](https://github.com/mscdex/busboy) 1.5.0, new option named `defParamCharset` for form body parameter encoding is added with default value `latin1`, so unicode filenames are broken since 4.7.0. @@ -312,8 +456,12 @@ When the server is disconnected, it should be indicated on the phone button. +- Client-generated sort parameters in channel directory ([#25768](https://github.com/RocketChat/Rocket.Chat/pull/25768) by [@BenWiederhake](https://github.com/BenWiederhake)) + - Close room when dismiss wrap up call modal ([#25056](https://github.com/RocketChat/Rocket.Chat/pull/25056)) +- Custom emoji reaction size ([#25393](https://github.com/RocketChat/Rocket.Chat/pull/25393)) + - Custom sound error toast messages ([#24515](https://github.com/RocketChat/Rocket.Chat/pull/24515) by [@Himanshu664](https://github.com/Himanshu664)) - Deactivating user breaks if user is the only room owner ([#24933](https://github.com/RocketChat/Rocket.Chat/pull/24933) by [@sidmohanty11](https://github.com/sidmohanty11)) @@ -329,12 +477,28 @@ - Desktop notification on multi-instance environments ([#25220](https://github.com/RocketChat/Rocket.Chat/pull/25220)) +- Direct Reply ([#22588](https://github.com/RocketChat/Rocket.Chat/pull/22588)) + +- Discussion alphabetical ordering ([#25788](https://github.com/RocketChat/Rocket.Chat/pull/25788)) + + Added a validation in the prop used for sorting (loweCaseName) checking for a prop that only exists in discussions (prid) + +- Dynamic load matrix is enabled and handle failure ([#25495](https://github.com/RocketChat/Rocket.Chat/pull/25495)) + - End call button disappearing when on-hold ([#24936](https://github.com/RocketChat/Rocket.Chat/pull/24936)) +- Error "numRequestsAllowed" property in rateLimiter for REST API endpoint when upgrading ([#26058](https://github.com/RocketChat/Rocket.Chat/pull/26058)) + - Failure to update Integration History index ([#25473](https://github.com/RocketChat/Rocket.Chat/pull/25473)) - Fix max-width message block ([#25686](https://github.com/RocketChat/Rocket.Chat/pull/25686)) +- Fix prom-client new promise usage ([#25781](https://github.com/RocketChat/Rocket.Chat/pull/25781)) + +- fixes HTML sanitizing error. ([#25410](https://github.com/RocketChat/Rocket.Chat/pull/25410)) + + If the user sent a HTML message over our product to a livechat user the HTML would get rendered on the message box, this prevents it from happening. + - Fixing app contextual bar functionality ([#25615](https://github.com/RocketChat/Rocket.Chat/pull/25615)) - Fixing Network connectivity issues with SIP client. ([#25391](https://github.com/RocketChat/Rocket.Chat/pull/25391)) @@ -351,20 +515,60 @@ - Full error message is visible ([#24856](https://github.com/RocketChat/Rocket.Chat/pull/24856) by [@Himanshu664](https://github.com/Himanshu664)) +- getUserMentionsByChannel method room permission ([#25748](https://github.com/RocketChat/Rocket.Chat/pull/25748)) + +- Importer fails to download files from URLs with query string params ([#25934](https://github.com/RocketChat/Rocket.Chat/pull/25934)) + +- Importer files are unnecessarily transferred over the network. ([#25919](https://github.com/RocketChat/Rocket.Chat/pull/25919)) + - Incorrect websocket url in livechat widget ([#25261](https://github.com/RocketChat/Rocket.Chat/pull/25261)) +- Initial members value on Create Channel Modal ([#26000](https://github.com/RocketChat/Rocket.Chat/pull/26000)) + + #### before + ![Screen Shot 2022-06-24 at 11 58 22](https://user-images.githubusercontent.com/27704687/175562315-221dbc9a-5695-4259-a8f7-644e2ff0ab36.png) + + #### after + ![Screen Shot 2022-06-24 at 11 59 38](https://user-images.githubusercontent.com/27704687/175562510-a4a6be49-bbd2-4aeb-aedb-a5a7a6f1159d.png) + +- Initial User not added to default channel ([#25544](https://github.com/RocketChat/Rocket.Chat/pull/25544)) + + If injecting initial user. The user wasn’t added to the default General channel + - Integrations avatar attribute misuse ([#25283](https://github.com/RocketChat/Rocket.Chat/pull/25283)) - Invitation links don't redirect to the registration form ([#25082](https://github.com/RocketChat/Rocket.Chat/pull/25082)) +- Kebab menu clicking issue ([#25869](https://github.com/RocketChat/Rocket.Chat/pull/25869)) + +- LDAP sync removing users from channels when multiple groups are mapped to it ([#25434](https://github.com/RocketChat/Rocket.Chat/pull/25434)) + +- Members selection field on creating team modal ([#25871](https://github.com/RocketChat/Rocket.Chat/pull/25871)) + + - Fix: add members breaking when searching users + + ![image](https://user-images.githubusercontent.com/27704687/121788070-b792f700-cba0-11eb-92b9-5833e1213c74.png) + - Message menu action not working on legacy messages. ([#25148](https://github.com/RocketChat/Rocket.Chat/pull/25148)) - Message menu dropdown not working on Mobile Web ([#25616](https://github.com/RocketChat/Rocket.Chat/pull/25616)) - Message preview not available for queued chats ([#25092](https://github.com/RocketChat/Rocket.Chat/pull/25092)) +- Messages spacing ([#25631](https://github.com/RocketChat/Rocket.Chat/pull/25631)) + + Adding `sequential` prop to Message component from Fuselage + +- Misaligned username on Room Info card for omnichannel chats ([#25331](https://github.com/RocketChat/Rocket.Chat/pull/25331)) + +- Not showing edit message button when blocking edit after N minutes ([#25724](https://github.com/RocketChat/Rocket.Chat/pull/25724) by [@matthias4217](https://github.com/matthias4217)) + + Previously, in Rocketchat 4.7.0 and later, as mentioned in https://github.com/RocketChat/Rocket.Chat/issues/25478, the edit button was not displayed on the interface in the minute after having sent a message. This is now fixed : messages can be edited right after sending them. + - NPS never finishing sending results ([#25067](https://github.com/RocketChat/Rocket.Chat/pull/25067)) +- One of the triggers was not working correctly ([#25409](https://github.com/RocketChat/Rocket.Chat/pull/25409)) + - Ordered and unordered list styles, Line breaks. ([#25494](https://github.com/RocketChat/Rocket.Chat/pull/25494)) Also removed the message.md cache from server, since changes in the parser might break messages in the future (and will in this specific case). @@ -396,6 +600,12 @@ - Remove initial 'total' text from rooms and messages groups in the admin info page - Add 'total' before 'rooms' and 'messages' title on the same section. To use the new 'Total Rooms', was created a new key in the en.i18n.json file. +- Remove duplicated icon bell when is thread main message ([#26051](https://github.com/RocketChat/Rocket.Chat/pull/26051)) + +- Remove duplicated property _USERNAMES from createDirectRoom.ts ([#26087](https://github.com/RocketChat/Rocket.Chat/pull/26087)) + + This pull request removes the duplicated property `_USERNAMES` from `apps/meteor/app/lib/server/functions/createDirectRoom.ts`, using only the existing property `roomInfo.usernames`. + - Removing user also removes them from Omni collections ([#25444](https://github.com/RocketChat/Rocket.Chat/pull/25444)) - Replace encrypted text to Encrypted Message Placeholder ([#24166](https://github.com/RocketChat/Rocket.Chat/pull/24166)) @@ -430,16 +640,60 @@ [LOOP ERROR ATTACHMENT] ![Screen Shot 2022-05-09 at 19 42 53](https://user-images.githubusercontent.com/27704687/167510439-1980461c-a885-46d2-9a49-79da432c7521.png) +- Sanitize styles in message ([#25744](https://github.com/RocketChat/Rocket.Chat/pull/25744)) + - Settings listeners not receiving overwritten values from env vars ([#25448](https://github.com/RocketChat/Rocket.Chat/pull/25448)) +- Settings not being overwritten to their default values ([#25891](https://github.com/RocketChat/Rocket.Chat/pull/25891)) + - Showing Blank Message Inside Report ([#25007](https://github.com/RocketChat/Rocket.Chat/pull/25007)) https://user-images.githubusercontent.com/53515714/161038085-4a86c7ae-6751-4996-9767-b1c9e0331a6c.mp4 +- sidebar colors ([#25987](https://github.com/RocketChat/Rocket.Chat/pull/25987)) + +- Sort by scope or creation date not working on canned responses list ([#25475](https://github.com/RocketChat/Rocket.Chat/pull/25475)) + +- Spotlight results showing usernames instead of real names ([#25471](https://github.com/RocketChat/Rocket.Chat/pull/25471)) + +- Thread Message Preview ([#25709](https://github.com/RocketChat/Rocket.Chat/pull/25709)) + +- Too many watchers in dev environment. ([#25930](https://github.com/RocketChat/Rocket.Chat/pull/25930)) + - Toolbox hiding under contextual bar ([#25237](https://github.com/RocketChat/Rocket.Chat/pull/25237)) +- toolbox menu behind thread component ([#25925](https://github.com/RocketChat/Rocket.Chat/pull/25925)) + +- UI/UX issues on Live Chat widget ([#25407](https://github.com/RocketChat/Rocket.Chat/pull/25407)) + +- Unable to close chats when comments is disabled ([#26057](https://github.com/RocketChat/Rocket.Chat/pull/26057)) + + Fixes https://github.com/RocketChat/Rocket.Chat/issues/25954 + - Unable to see channel member list by authorized channel roles ([#25412](https://github.com/RocketChat/Rocket.Chat/pull/25412)) +- Undefined headers on API Client ([#26083](https://github.com/RocketChat/Rocket.Chat/pull/26083)) + +- Unnecessary padding on teams channels footer ([#25712](https://github.com/RocketChat/Rocket.Chat/pull/25712)) + + #### before + + + ### after + + +- Update chartjs usage to v3 ([#25873](https://github.com/RocketChat/Rocket.Chat/pull/25873)) + +- Update import from `csv-parse` ([#25872](https://github.com/RocketChat/Rocket.Chat/pull/25872)) + + This PR updates the importing of `csv-parse` because the used method wasn't working anymore, we were receiving the following error: + + `error: "this.csvParser is not a function"` + +- Update subscription on update team member ([#25855](https://github.com/RocketChat/Rocket.Chat/pull/25855)) + + Added update to subscription when a team member is updated on `teams.updateMember` + - Upgrade tab loader in incorrect position ([#25398](https://github.com/RocketChat/Rocket.Chat/pull/25398)) - Add invisible prop to iframe when loading state is active. @@ -450,6 +704,19 @@ - useCurrentChatTags is not a function ([#25604](https://github.com/RocketChat/Rocket.Chat/pull/25604)) +- User abandonment setting was not working doe to failing event hook ([#25520](https://github.com/RocketChat/Rocket.Chat/pull/25520)) + + A setting watcher and the query for grabbing abandoned chats were broken, now they're not. + +- User avatar reseting and getting random image ([#25603](https://github.com/RocketChat/Rocket.Chat/pull/25603)) + + - fixes user avatar not being saved after editing the user profile issue + - fixes user avatar not getting another user picture due to database deletion error + +- user status Offline misnamed as Invisible in Custom Status edit dropdown menu ([#24796](https://github.com/RocketChat/Rocket.Chat/pull/24796) by [@Kunalvrm555](https://github.com/Kunalvrm555)) + +- User's with non-agent role shown on voip agent association model ([#25682](https://github.com/RocketChat/Rocket.Chat/pull/25682)) + - UserAutoComplete not rendering UserAvatar correctly ([#25055](https://github.com/RocketChat/Rocket.Chat/pull/25055)) ### before @@ -469,8 +736,25 @@ ### after ![Screen Shot 2022-04-07 at 00 07 13](https://user-images.githubusercontent.com/27704687/162112353-afd6aac6-b27c-4470-a642-631b8080d59e.png) +- Users without the `view-other-user-info` permission can't use the `users.list` endpoint ([#26050](https://github.com/RocketChat/Rocket.Chat/pull/26050)) + + This PR fix the query when a normal users access `users.list` + +- Validate room access ([#24534](https://github.com/RocketChat/Rocket.Chat/pull/24534)) + + The request must be blocked If the user has no permission to view rooms. + - Video and Audio not skipping forward ([#19866](https://github.com/RocketChat/Rocket.Chat/pull/19866)) +- VOIP CallContext snapshot infinite loop ([#25947](https://github.com/RocketChat/Rocket.Chat/pull/25947)) + + The application was crashing due to an error on the `useCallerInfo()` hook. + The error was: + ![image](https://user-images.githubusercontent.com/20212776/174823914-4832e5dd-c91a-4ae4-9d1f-1b960bcd372c.png) + ![image](https://user-images.githubusercontent.com/20212776/174823982-cb543fe0-663f-4530-bb94-0720653ca897.png) + + To prevent this issue to happen it was added a cached and out-of-scope snapshot variable to the hook using `useSyncExternalStore` + - VoIP disabled/enabled sequence puts voip agent in error state ([#25230](https://github.com/RocketChat/Rocket.Chat/pull/25230)) Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side) @@ -483,6 +767,10 @@ 2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance. 3. in apps/meteor/client/providers/CallProvider/hooks/useVoipClient.ts add the event handler that receives this event. Change voipEnabled from constant to state. Change this state based on the 'value' that is received by the handler. +- Voip endpoint permissions ([#25783](https://github.com/RocketChat/Rocket.Chat/pull/25783)) + +- Wrong argument name preventing Omnichannel Chat Forward to User ([#25723](https://github.com/RocketChat/Rocket.Chat/pull/25723)) +
🔍 Minor changes @@ -499,8 +787,26 @@ - Bump template-file from 6.0.0 to 6.0.1 ([#25002](https://github.com/RocketChat/Rocket.Chat/pull/25002) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: `@rocket.chat/favicon` ([#25920](https://github.com/RocketChat/Rocket.Chat/pull/25920)) + +- Chore: `refactor/tsc-perf` ([#26040](https://github.com/RocketChat/Rocket.Chat/pull/26040)) + +- Chore: Account/Profile to TS ([#25929](https://github.com/RocketChat/Rocket.Chat/pull/25929)) + +- Chore: add _id and name options to JSON Schemas ([#25813](https://github.com/RocketChat/Rocket.Chat/pull/25813)) + + This pull request adds the `roomId` and `roomName` options for the Ajv JSON Schemas on the `packages/rest-typings/src/v1/channels/` and `packages/rest-typings/src/v1/dm/` folders. + - Chore: Add /v1/video-conference endpoint types ([#25278](https://github.com/RocketChat/Rocket.Chat/pull/25278)) +- Chore: Add Agenda fork to the monorepo ([#25681](https://github.com/RocketChat/Rocket.Chat/pull/25681)) + +- Chore: add Ajv JSON Schema to api/v1 ([#25601](https://github.com/RocketChat/Rocket.Chat/pull/25601)) + + This pull request adds Ajv JSON Schema validation to `apps/meteor/app/api/server/v1/` and `packages/rest-typings/src/v1/`, where needed. + +- Chore: Add auto label and improve Kodiak configuration ([#25829](https://github.com/RocketChat/Rocket.Chat/pull/25829)) + - Chore: Add channel endpoints (rest-typings) ([#25279](https://github.com/RocketChat/Rocket.Chat/pull/25279)) - Chore: Add client folder to CODEOWNERS ([#25397](https://github.com/RocketChat/Rocket.Chat/pull/25397)) @@ -511,28 +817,78 @@ ![image](https://user-images.githubusercontent.com/40830821/162269915-931c5c3c-c979-4234-b74c-371f67467ce0.png) +- Chore: Add Livechat repo into Monorepo packages ([#25312](https://github.com/RocketChat/Rocket.Chat/pull/25312)) + +- Chore: Add missing Swedish livechat translations ([#26048](https://github.com/RocketChat/Rocket.Chat/pull/26048) by [@joakimaho](https://github.com/joakimaho)) + + Added missing Swedish translations. + - Chore: Add options to debug stdout and rate limiter ([#25336](https://github.com/RocketChat/Rocket.Chat/pull/25336)) - Chore: Add root package.json to houston files ([#25286](https://github.com/RocketChat/Rocket.Chat/pull/25286)) See title +- Chore: Add tests for agents screens ([#25637](https://github.com/RocketChat/Rocket.Chat/pull/25637)) + - Chore: Add typings for /v1/webdav.getMyAccounts ([#25276](https://github.com/RocketChat/Rocket.Chat/pull/25276)) - Chore: Add yarn plugin to check node and yarn version ([#25224](https://github.com/RocketChat/Rocket.Chat/pull/25224)) +- Chore: Adding default message parser template ([#26064](https://github.com/RocketChat/Rocket.Chat/pull/26064)) + +- Chore: adjust in some configurations ([#25612](https://github.com/RocketChat/Rocket.Chat/pull/25612)) + +- Chore: Allow endpoints to optionally require authentication ([#26084](https://github.com/RocketChat/Rocket.Chat/pull/26084)) + +- Chore: API test on method GET with params as a number. ([#25769](https://github.com/RocketChat/Rocket.Chat/pull/25769)) + +- Chore: AutoTranslate contextualBar rewrite ([#25751](https://github.com/RocketChat/Rocket.Chat/pull/25751)) + +- Chore: Avoid set useless set UTC Offset ([#26270](https://github.com/RocketChat/Rocket.Chat/pull/26270)) + +- Chore: Avoid unneeded permission updates when EE license is applied ([#26253](https://github.com/RocketChat/Rocket.Chat/pull/26253)) + +- Chore: Broken Storybook ([#25714](https://github.com/RocketChat/Rocket.Chat/pull/25714)) + + There is another small improvement on the way we got storybook files. + +- Chore: Bump deps ([#25624](https://github.com/RocketChat/Rocket.Chat/pull/25624)) + - Chore: bump fuselage ([#25605](https://github.com/RocketChat/Rocket.Chat/pull/25605)) - Chore: Bump fuselage ([#25371](https://github.com/RocketChat/Rocket.Chat/pull/25371)) +- Chore: Bump fuselage and update icon ([#26036](https://github.com/RocketChat/Rocket.Chat/pull/26036)) + +- Chore: bump fuselage packages ([#26325](https://github.com/RocketChat/Rocket.Chat/pull/26325)) + - Chore: Bump Fuselage packages ([#25259](https://github.com/RocketChat/Rocket.Chat/pull/25259)) - Chore: Cancel running jobs if PR is updated ([#24708](https://github.com/RocketChat/Rocket.Chat/pull/24708)) +- Chore: Change Apps-Engine version source for info ([#26205](https://github.com/RocketChat/Rocket.Chat/pull/26205)) + + Now that we're using `yarn`, the version stored in the `package.json` is no longer the resolved one, but it matches the input. This means that when we ran `yarn add @rocket.chat/apps-engine@alpha`, yarn saves `"alpha"` as the version of the package, while NPM added the resolved version for the tag, e.g. `"1.33.0-alpha.6507"`. This ends up breaking a few places where we need the Apps-Engine version for communication with the Marketplace. + + With this PR we change the source of that info so the problem doesn't happen anymore. + +- Chore: Change stats to daily ([#26113](https://github.com/RocketChat/Rocket.Chat/pull/26113)) + +- Chore: Check for env var values and not just if they are set ([#26219](https://github.com/RocketChat/Rocket.Chat/pull/26219)) + - Chore: Chore add validation option to rest endpoints ([#25443](https://github.com/RocketChat/Rocket.Chat/pull/25443)) +- Chore: Close tooltip on click ([#26070](https://github.com/RocketChat/Rocket.Chat/pull/26070)) + - Chore: Code Improvements for #25391 ([#25606](https://github.com/RocketChat/Rocket.Chat/pull/25606)) +- Chore: Collect e2e coverage ([#25743](https://github.com/RocketChat/Rocket.Chat/pull/25743)) + +- Chore: Colors ([#25969](https://github.com/RocketChat/Rocket.Chat/pull/25969)) + +- Chore: command's endpoints ([#25630](https://github.com/RocketChat/Rocket.Chat/pull/25630)) + - Chore: Convert `UserStatusMenu` to TS ([#25265](https://github.com/RocketChat/Rocket.Chat/pull/25265)) - Chore: Convert additionalForms ([#25586](https://github.com/RocketChat/Rocket.Chat/pull/25586)) @@ -548,30 +904,60 @@ - Chore: Convert AdminSideBar to ts ([#25372](https://github.com/RocketChat/Rocket.Chat/pull/25372)) +- Chore: convert apps/meteor/app/api/server/lib/ files to TS ([#25840](https://github.com/RocketChat/Rocket.Chat/pull/25840)) + + This pull request converts files on `apps/meteor/app/api/server/lib/` to Typescript. + - Chore: Convert apps/meteor/client/components/UserAutoComplete ([#25554](https://github.com/RocketChat/Rocket.Chat/pull/25554)) +- Chore: Convert apps/meteor/client/sidebar/header/index ([#25671](https://github.com/RocketChat/Rocket.Chat/pull/25671)) + +- Chore: Convert apps/meteor/client/sidebar/search ([#25754](https://github.com/RocketChat/Rocket.Chat/pull/25754)) + - Chore: Convert apps/meteor/client/views/admin/settings ([#25565](https://github.com/RocketChat/Rocket.Chat/pull/25565)) - Chore: Convert apps/meteor/client/views/admin/settings/inputs folder ([#25427](https://github.com/RocketChat/Rocket.Chat/pull/25427)) +- Chore: Convert assets endpoint to Typescript ([#25358](https://github.com/RocketChat/Rocket.Chat/pull/25358)) + - Chore: Convert AutoTranslate ([#25591](https://github.com/RocketChat/Rocket.Chat/pull/25591)) - Chore: Convert client/views/admin/settings/groups folder to ts ([#25345](https://github.com/RocketChat/Rocket.Chat/pull/25345)) +- Chore: convert communication methods to Typescript ([#25503](https://github.com/RocketChat/Rocket.Chat/pull/25503)) + + Convert files from `apps/meteor/app/apps/server/communication/` to ts. + +- Chore: Convert components/sidebar to TS ([#25429](https://github.com/RocketChat/Rocket.Chat/pull/25429)) + - Chore: Convert Create Channel ([#25589](https://github.com/RocketChat/Rocket.Chat/pull/25589)) +- Chore: Convert CreateChannelWithData ([#25667](https://github.com/RocketChat/Rocket.Chat/pull/25667)) + - Chore: Convert customSounds folder to ts ([#25274](https://github.com/RocketChat/Rocket.Chat/pull/25274)) - Chore: Convert customUserStatus folder to ts ([#25288](https://github.com/RocketChat/Rocket.Chat/pull/25288)) +- Chore: convert e2e to ts ([#25958](https://github.com/RocketChat/Rocket.Chat/pull/25958)) + + Converted the `apps/meteor/app/api/server/v1/e2e.js` to ts and created endpoint typings on the `packages/rest-typings/src/v1/e2e` folder. + - Chore: Convert email inbox feature to TypeScript ([#25298](https://github.com/RocketChat/Rocket.Chat/pull/25298) by [@ujorgeleite](https://github.com/ujorgeleite)) - Chore: Convert federationDashboard folder to ts ([#25343](https://github.com/RocketChat/Rocket.Chat/pull/25343)) - Chore: Convert getStatistics ([#25342](https://github.com/RocketChat/Rocket.Chat/pull/25342)) +- Chore: convert import.js endpoints to TS ([#25956](https://github.com/RocketChat/Rocket.Chat/pull/25956)) + + Converted the `apps/meteor/app/api/server/v1/import.js` to ts and created endpoint typings on the `packages/rest-typings/src/v1/import` folder. + - Chore: convert info to typescript ([#25420](https://github.com/RocketChat/Rocket.Chat/pull/25420)) +- Chore: convert invites, misc and subscriptions to TS and create definitions ([#25350](https://github.com/RocketChat/Rocket.Chat/pull/25350)) + + Converted `apps/meteor/app/api/server/v1/invites.js`, `misc.js` and `subscriptions.js` to Typescript and created their endpoint definitions on the rest-typings folder. + - Chore: Convert LivechatAgentActivity to raw model and TS ([#25123](https://github.com/RocketChat/Rocket.Chat/pull/25123)) - Chore: Convert Mailer to TS ([#25121](https://github.com/RocketChat/Rocket.Chat/pull/25121)) @@ -584,14 +970,28 @@ **Apps detail page** ![Screen Shot 2022-05-13 at 12 58 56](https://user-images.githubusercontent.com/4161171/168322241-505ee5bb-d3d8-4b0e-8757-873a1a65a6a6.png) -- Chore: Convert NotificationStatus to TS ([#25125](https://github.com/RocketChat/Rocket.Chat/pull/25125)) +- Chore: Convert MemoizedSetting, Setting, Section ([#25572](https://github.com/RocketChat/Rocket.Chat/pull/25572)) + +- Chore: Convert normalizeMessagesForUser ([#26059](https://github.com/RocketChat/Rocket.Chat/pull/26059)) + +- Chore: Convert NotificationStatus to TS ([#25125](https://github.com/RocketChat/Rocket.Chat/pull/25125)) - Chore: Convert push endpoints to TS ([#25347](https://github.com/RocketChat/Rocket.Chat/pull/25347)) - Chore: Convert RoomForeword, TextCopy and RoomAvatarEditor to TS ([#25424](https://github.com/RocketChat/Rocket.Chat/pull/25424)) +- Chore: Convert RoomMenu ([#25914](https://github.com/RocketChat/Rocket.Chat/pull/25914)) + +- Chore: Convert sidebar/header/actions ([#25581](https://github.com/RocketChat/Rocket.Chat/pull/25581)) + +- Chore: Convert sidebar/item ([#25634](https://github.com/RocketChat/Rocket.Chat/pull/25634)) + - Chore: Convert slashCommands to typescript ([#25592](https://github.com/RocketChat/Rocket.Chat/pull/25592) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) +- Chore: Convert to TS omnichannel/agent ([#25511](https://github.com/RocketChat/Rocket.Chat/pull/25511)) + +- Chore: Convert to TS RoomAutoComplete ([#25536](https://github.com/RocketChat/Rocket.Chat/pull/25536)) + - Chore: Convert to typescript some functions from app/lib/server/functions ([#24519](https://github.com/RocketChat/Rocket.Chat/pull/24519)) Convert to typescript some functions from app/lib/server/functions and transfered theses files to server/lib @@ -602,18 +1002,44 @@ - Chore: Convert useFileInput to TS ([#25426](https://github.com/RocketChat/Rocket.Chat/pull/25426)) +- Chore: Convert usePreventDefault, useQueryOptions, useShortcutOpenMenu ([#26035](https://github.com/RocketChat/Rocket.Chat/pull/26035)) + +- Chore: Convert UserAutoCompleteMultiple ([#25587](https://github.com/RocketChat/Rocket.Chat/pull/25587)) + +- Chore: Convert users endpoints ([#25635](https://github.com/RocketChat/Rocket.Chat/pull/25635)) + +- Chore: Convert useSidebarPaletteColor ([#26065](https://github.com/RocketChat/Rocket.Chat/pull/26065)) + - Chore: Convert useUpdateAvatar to TS and type avatar endpoints ([#25430](https://github.com/RocketChat/Rocket.Chat/pull/25430)) +- Chore: Converting files from app/livechat folder from JS to TS ([#25658](https://github.com/RocketChat/Rocket.Chat/pull/25658)) + + Converting files from apps/meteor/app/livechat/lib/ from JS to TS + +- Chore: Converting omnichannel installation files to ts ([#25665](https://github.com/RocketChat/Rocket.Chat/pull/25665)) + + This PR converts the omnichannel/installation folder from js to ts + - Chore: Converting orchestrator.js to ts ([#25367](https://github.com/RocketChat/Rocket.Chat/pull/25367)) +- Chore: create a e2e test guideline ([#25884](https://github.com/RocketChat/Rocket.Chat/pull/25884)) + +- Chore: Create a token for each action ([#26023](https://github.com/RocketChat/Rocket.Chat/pull/26023)) + - Chore: Create README.md for Rest Typings ([#25335](https://github.com/RocketChat/Rocket.Chat/pull/25335)) +- Chore: Custom Sounds Endpoints ([#25633](https://github.com/RocketChat/Rocket.Chat/pull/25633)) + - Chore: Dedicated package for UI contexts ([#25432](https://github.com/RocketChat/Rocket.Chat/pull/25432)) Moving our React contexts to a different package on the monorepo enable us to deliver components from another packages, because they work as a loose connection to the core APIs. - Chore: Dependencies upgrade ([#25290](https://github.com/RocketChat/Rocket.Chat/pull/25290)) +- Chore: Disabled icon colors on sidebar ([#26257](https://github.com/RocketChat/Rocket.Chat/pull/26257)) + +- Chore: Do not log integrations using `name` key ([#26163](https://github.com/RocketChat/Rocket.Chat/pull/26163)) + - Chore: Enable marketplace screenshots endpoint ([#25395](https://github.com/RocketChat/Rocket.Chat/pull/25395)) - Chore: ensure scripts use cross-env and ignore some dirs (ROC-54) ([#25218](https://github.com/RocketChat/Rocket.Chat/pull/25218)) @@ -621,34 +1047,88 @@ - data and test-failure should be ignored - ensure scripts use cross-env +- Chore: Fix CI ([#25797](https://github.com/RocketChat/Rocket.Chat/pull/25797)) + +- Chore: Fix correct unit test to api files ([#25870](https://github.com/RocketChat/Rocket.Chat/pull/25870)) + +- Chore: Fix incorrect checksum for agenda package (cause of breaking develop builds) ([#25741](https://github.com/RocketChat/Rocket.Chat/pull/25741)) + +- Chore: Fix Omnichannel E2E tests not running ([#26092](https://github.com/RocketChat/Rocket.Chat/pull/26092)) + - Chore: Fix return type warnings ([#25275](https://github.com/RocketChat/Rocket.Chat/pull/25275)) +- Chore: Fix version on develop branch ([#25842](https://github.com/RocketChat/Rocket.Chat/pull/25842)) + +- Chore: fix watermark condition ([#26095](https://github.com/RocketChat/Rocket.Chat/pull/26095)) + +- Chore: Fixes e2e playwright intermittences ([#25984](https://github.com/RocketChat/Rocket.Chat/pull/25984)) + +- Chore: Fuselage update ([#26004](https://github.com/RocketChat/Rocket.Chat/pull/26004)) + +- Chore: Fuselage update ([#25983](https://github.com/RocketChat/Rocket.Chat/pull/25983)) + +- Chore: Handle errors on index creation ([#26094](https://github.com/RocketChat/Rocket.Chat/pull/26094)) + +- Chore: Hide deprecation query log on production ([#26188](https://github.com/RocketChat/Rocket.Chat/pull/26188)) + +- Chore: Improve CI cache ([#25907](https://github.com/RocketChat/Rocket.Chat/pull/25907)) + +- Chore: Improve footer Template ([#26085](https://github.com/RocketChat/Rocket.Chat/pull/26085)) + - Chore: Increase performance and security of integrations’ scripts ([#25641](https://github.com/RocketChat/Rocket.Chat/pull/25641)) Replace internal VM implementation with VM2 which implements many more mechanisms to ensure timeout, security and allow easier configuration for future improvements on the integrations' feature. +- Chore: Info page ([#26201](https://github.com/RocketChat/Rocket.Chat/pull/26201)) + +- Chore: Introduce Modal Region ([#25962](https://github.com/RocketChat/Rocket.Chat/pull/25962)) + +- Chore: Introduce new index to query active livechat conversations for cloud scaling ([#26047](https://github.com/RocketChat/Rocket.Chat/pull/26047)) + +- Chore: Keep the option to run only the meteor app ([#25915](https://github.com/RocketChat/Rocket.Chat/pull/25915)) + +- Chore: Keyboard shortcuts contextualBar rewrite ([#25753](https://github.com/RocketChat/Rocket.Chat/pull/25753)) + - Chore: Livechat change output level ([#25522](https://github.com/RocketChat/Rocket.Chat/pull/25522)) +- Chore: Major refactors in pageobjects ([#26015](https://github.com/RocketChat/Rocket.Chat/pull/26015)) + +- Chore: Make kodiak merge message empty ([#26069](https://github.com/RocketChat/Rocket.Chat/pull/26069)) + - Chore: Manager Page Rewrite ([#25431](https://github.com/RocketChat/Rocket.Chat/pull/25431)) +- Chore: Messages raw model rewrite to ts ([#25761](https://github.com/RocketChat/Rocket.Chat/pull/25761)) + - Chore: Migrate 15-message-popup from cypress to playwright ([#25462](https://github.com/RocketChat/Rocket.Chat/pull/25462)) - Chore: migrate from cypress to pw 14-setting-permission ([#25523](https://github.com/RocketChat/Rocket.Chat/pull/25523)) +- Chore: migrate katex to ts ([#25501](https://github.com/RocketChat/Rocket.Chat/pull/25501)) + +- Chore: Migrate LivechatVisitors model to raw ([#25756](https://github.com/RocketChat/Rocket.Chat/pull/25756)) + - Chore: Migrate NotFoundPage to TS ([#25509](https://github.com/RocketChat/Rocket.Chat/pull/25509)) - Chore: Migrate oauth2server to typescript ([#25126](https://github.com/RocketChat/Rocket.Chat/pull/25126)) +- Chore: Migrate oembed to ts ([#25622](https://github.com/RocketChat/Rocket.Chat/pull/25622)) + - Chore: Migrate retention-policy to ts ([#25582](https://github.com/RocketChat/Rocket.Chat/pull/25582)) +- Chore: Migrate some small helper functions to TypeScript ([#25666](https://github.com/RocketChat/Rocket.Chat/pull/25666)) + - Chore: Migrate spotify to ts ([#25507](https://github.com/RocketChat/Rocket.Chat/pull/25507)) +- Chore: migrate-to-pw-16-discussion ([#25567](https://github.com/RocketChat/Rocket.Chat/pull/25567)) + - Chore: migrate-to-pw-adjust-in-intermitences ([#25542](https://github.com/RocketChat/Rocket.Chat/pull/25542)) - Chore: Minor dependency updates ([#25269](https://github.com/RocketChat/Rocket.Chat/pull/25269)) - Chore: Missing keys in APIsDisplay ([#24464](https://github.com/RocketChat/Rocket.Chat/pull/24464)) +- Chore: Model Typings ([#25758](https://github.com/RocketChat/Rocket.Chat/pull/25758)) + - Chore: Monorepo ([#25074](https://github.com/RocketChat/Rocket.Chat/pull/25074)) - Chore: Move admin sidebarItems registration to the main file ([#25442](https://github.com/RocketChat/Rocket.Chat/pull/25442)) @@ -657,32 +1137,90 @@ - Chore: move definitions to packages ([#25085](https://github.com/RocketChat/Rocket.Chat/pull/25085)) +- Chore: move fork of cas module to the monorepo ([#26107](https://github.com/RocketChat/Rocket.Chat/pull/26107)) + - Chore: Move markdown message parser to a `callback` ([#25413](https://github.com/RocketChat/Rocket.Chat/pull/25413)) +- Chore: Move voip's Wrap-up and On-hold functionality to EE (Backend) ([#25160](https://github.com/RocketChat/Rocket.Chat/pull/25160)) + +- Chore: Notification Preferences to TS ([#25827](https://github.com/RocketChat/Rocket.Chat/pull/25827)) + + - Notifications Preferences to TS. + - Fix broken save action. + - Chore: organize test files and fix code coverage ([#24900](https://github.com/RocketChat/Rocket.Chat/pull/24900)) +- Chore: Plan tag ([#26224](https://github.com/RocketChat/Rocket.Chat/pull/26224)) + + Now we only have one plan tag for all plans \/ + ![image](https://user-images.githubusercontent.com/40830821/178366367-12388c4c-6822-4e41-be8d-ca306718be98.png) + +- Chore: Prune Messages contextualBar rewrite ([#25757](https://github.com/RocketChat/Rocket.Chat/pull/25757)) + +- Chore: Remove all cypress tests, configs and references ([#25564](https://github.com/RocketChat/Rocket.Chat/pull/25564)) + - Chore: Remove Alpine image deps after using them ([#25053](https://github.com/RocketChat/Rocket.Chat/pull/25053)) +- Chore: Remove compose from main repo ([#23426](https://github.com/RocketChat/Rocket.Chat/pull/23426)) + +- Chore: Remove duplicate checksumBehavior key from yarn file ([#25730](https://github.com/RocketChat/Rocket.Chat/pull/25730)) + +- Chore: remove duplicated NotFoundPage.js ([#25749](https://github.com/RocketChat/Rocket.Chat/pull/25749)) + - Chore: Remove duplicated useUserRoom ([#25180](https://github.com/RocketChat/Rocket.Chat/pull/25180)) +- Chore: Remove Imperative Modal from context ([#25911](https://github.com/RocketChat/Rocket.Chat/pull/25911)) + - Chore: Remove old files from removed Omnichannel feature ([#25129](https://github.com/RocketChat/Rocket.Chat/pull/25129)) +- Chore: Remove old rest api code ([#25863](https://github.com/RocketChat/Rocket.Chat/pull/25863)) + - Chore: Remove package-lock.json from houston files ([#25280](https://github.com/RocketChat/Rocket.Chat/pull/25280)) Houston config in the `package.json` file still mentioned `package-lock.json`, but it doesn't exist anymore +- Chore: Remove snap files from Houston config ([#25819](https://github.com/RocketChat/Rocket.Chat/pull/25819)) + +- Chore: Remove TimeSync usage ([#26294](https://github.com/RocketChat/Rocket.Chat/pull/26294)) + +- Chore: Remove toastr package ([#25787](https://github.com/RocketChat/Rocket.Chat/pull/25787)) + - Chore: Remove unused Drone CI files ([#25124](https://github.com/RocketChat/Rocket.Chat/pull/25124)) +- Chore: remove unused locators from e2e tests ([#25860](https://github.com/RocketChat/Rocket.Chat/pull/25860)) + +- Chore: Remove unused migrations ([#26102](https://github.com/RocketChat/Rocket.Chat/pull/26102)) + + After giving it some thought: + + - 234 through 240 are not going to be run anymore. Keeping them does not affect behavior of course, but this (removing) makes it easier to quickly glance at and understand what migrations are actually included in 5.x.y (especially in tag compare view or in general just checking the ref). + + - Also changed the file name of 233 to be more explicit at what it does so to not confuse with actual "migrations" without having to open the file. + + - The redirect to the documentation page (go.rocket....) is not yet set up, jfyi. + - Chore: Reorder unreleased migrations ([#25508](https://github.com/RocketChat/Rocket.Chat/pull/25508)) +- Chore: Replace `useSubscription` with `useSyncExternalStore` ([#25909](https://github.com/RocketChat/Rocket.Chat/pull/25909)) + +- Chore: Replace AnnouncementModal in favor of GenericModal ([#25752](https://github.com/RocketChat/Rocket.Chat/pull/25752)) + - Chore: Rest API query parameters handling ([#25648](https://github.com/RocketChat/Rocket.Chat/pull/25648)) - Chore: REST query and body params validation ([#25446](https://github.com/RocketChat/Rocket.Chat/pull/25446)) +- Chore: RestApiClient as Package ([#25469](https://github.com/RocketChat/Rocket.Chat/pull/25469)) + +- Chore: Revert `yarn dev` implementation ([#26075](https://github.com/RocketChat/Rocket.Chat/pull/26075)) + - Chore: Rewrite 2fa to typescript ([#25285](https://github.com/RocketChat/Rocket.Chat/pull/25285)) - Chore: Rewrite action-links to ts ([#25418](https://github.com/RocketChat/Rocket.Chat/pull/25418)) +- Chore: Rewrite AddUsers to TS ([#25830](https://github.com/RocketChat/Rocket.Chat/pull/25830) by [@kodiakhq[bot]](https://github.com/kodiakhq[bot])) + +- Chore: Rewrite Admin UsersTable to Typescript ([#25698](https://github.com/RocketChat/Rocket.Chat/pull/25698)) + - Chore: Rewrite autotranslate to ts ([#25425](https://github.com/RocketChat/Rocket.Chat/pull/25425)) - Chore: Rewrite im and dm endpoints to ts ([#25521](https://github.com/RocketChat/Rocket.Chat/pull/25521)) @@ -722,21 +1260,49 @@ - Chore: Rewrite mail-messages to ts ([#25421](https://github.com/RocketChat/Rocket.Chat/pull/25421)) +- Chore: Rewrite RoomWithData ([#25858](https://github.com/RocketChat/Rocket.Chat/pull/25858)) + - Chore: Rewrite some Omnichannel files to TypeScript ([#25359](https://github.com/RocketChat/Rocket.Chat/pull/25359)) apps/meteor/client/components/Omnichannel/modals/* apps/meteor/client/components/Omnichannel/Tags.js +- Chore: Room access validation may be called without user information ([#26086](https://github.com/RocketChat/Rocket.Chat/pull/26086)) + +- Chore: RouteGroup for My Account sidebar ([#25632](https://github.com/RocketChat/Rocket.Chat/pull/25632)) + + Refactoring My Accounts routes. Allows to add "my account" routes for EE. + +- Chore: Run tests on docker ([#25556](https://github.com/RocketChat/Rocket.Chat/pull/25556)) + +- Chore: Settings UI issue ([#26053](https://github.com/RocketChat/Rocket.Chat/pull/26053)) + +- Chore: Small fix on callProvider ([#25963](https://github.com/RocketChat/Rocket.Chat/pull/25963)) + - Chore: solve yarn issues from env var ([#25468](https://github.com/RocketChat/Rocket.Chat/pull/25468)) +- Chore: Split useUserInfoActions into small hooks ([#25747](https://github.com/RocketChat/Rocket.Chat/pull/25747)) + - Chore: Sync with master ([#25284](https://github.com/RocketChat/Rocket.Chat/pull/25284)) +- Chore: Taking out Blaze from routes with `MainLayout` ([#25697](https://github.com/RocketChat/Rocket.Chat/pull/25697)) + + While working with @guijun13 on the new homepage I saw we're still rendering a Blaze template even to just embedded components into `MainLayout`. This PR addresses it. + - Chore: Template to generate packages ([#25174](https://github.com/RocketChat/Rocket.Chat/pull/25174)) ``` npx hygen package new test ``` +- Chore: Test for department screen ([#25696](https://github.com/RocketChat/Rocket.Chat/pull/25696)) + +- Chore: test turbo params ([#26038](https://github.com/RocketChat/Rocket.Chat/pull/26038)) + +- Chore: Testing Kodiak feature ([#25794](https://github.com/RocketChat/Rocket.Chat/pull/25794)) + +- Chore: Tests refactor pageobjects ([#26245](https://github.com/RocketChat/Rocket.Chat/pull/26245)) + - Chore: Tests with Playwright (task: All works) ([#25122](https://github.com/RocketChat/Rocket.Chat/pull/25122)) - Chore: Tests with Playwright (task: ROC-25, 06-message) ([#25252](https://github.com/RocketChat/Rocket.Chat/pull/25252)) @@ -747,44 +1313,135 @@ - Chore: Tests with Playwright (task: ROC-66, Intermittent resolution in tests) ([#25416](https://github.com/RocketChat/Rocket.Chat/pull/25416)) +- Chore: Translate admin helpers to TS ([#25690](https://github.com/RocketChat/Rocket.Chat/pull/25690)) + - Chore: TS conversion folder client ([#25031](https://github.com/RocketChat/Rocket.Chat/pull/25031)) - Chore: TS migration SortList ([#25167](https://github.com/RocketChat/Rocket.Chat/pull/25167)) +- Chore: ui-client package ([#25916](https://github.com/RocketChat/Rocket.Chat/pull/25916)) + - Chore: Update Apps-Engine and Fuselage ([#25700](https://github.com/RocketChat/Rocket.Chat/pull/25700)) - Chore: Update Apps-Engine version ([#25617](https://github.com/RocketChat/Rocket.Chat/pull/25617)) +- Chore: Update Apps-Engine version ([#26258](https://github.com/RocketChat/Rocket.Chat/pull/26258)) + + Bumping Apps-Engine version + +- Chore: update avatar colors ([#26153](https://github.com/RocketChat/Rocket.Chat/pull/26153)) + - Chore: Update Livechat to the last version ([#25257](https://github.com/RocketChat/Rocket.Chat/pull/25257)) - Chore: Update Livechat version ([#25130](https://github.com/RocketChat/Rocket.Chat/pull/25130)) +- Chore: Update Meteor 2.7.3 ([#25991](https://github.com/RocketChat/Rocket.Chat/pull/25991)) + - Chore: update OTR icon ([#24521](https://github.com/RocketChat/Rocket.Chat/pull/24521) by [@kibonusp](https://github.com/kibonusp)) I changed the shredder icon in OTR contextual bar to the stopwatch icon, recently added to the fuselage. +- Chore: Update package.json update tsc memory ([#25755](https://github.com/RocketChat/Rocket.Chat/pull/25755)) + +- Chore: update pageobjects to use es6 getters and remove export default ([#25867](https://github.com/RocketChat/Rocket.Chat/pull/25867)) + +- Chore: Update poplib ([#25964](https://github.com/RocketChat/Rocket.Chat/pull/25964)) + +- Chore: Update useSidebarPalette selectors ([#26322](https://github.com/RocketChat/Rocket.Chat/pull/26322)) + - Chore: Update Volta configuration ([#25394](https://github.com/RocketChat/Rocket.Chat/pull/25394)) [Volta](https://volta.sh/) need some extra configuration to work on monorepos. +- Chore: Updating Apps-Engine ([#26001](https://github.com/RocketChat/Rocket.Chat/pull/26001)) + +- Chore: Upgrade and remove unnecessary Livechat dependencies ([#25672](https://github.com/RocketChat/Rocket.Chat/pull/25672)) + +- Chore: Upgrade Fuselage packages to `next` dist-tag ([#26274](https://github.com/RocketChat/Rocket.Chat/pull/26274)) + + Upgrade Fuselage packages to the latest development versions. + +- Chore: use params instead of URL building on livechat endpoints ([#25810](https://github.com/RocketChat/Rocket.Chat/pull/25810)) + - Chore: User set UTC offset ([#25381](https://github.com/RocketChat/Rocket.Chat/pull/25381)) +- Chore: VideoConference UX/UI Refactor 1st Interaction ([#26183](https://github.com/RocketChat/Rocket.Chat/pull/26183)) + +- Chore: VoIP Context ([#25994](https://github.com/RocketChat/Rocket.Chat/pull/25994)) + +- Chore: Watch for package changes ([#25910](https://github.com/RocketChat/Rocket.Chat/pull/25910)) + + With the current `dev` pipeline, whenever we modify a package (e.g. `api-client`), we have to kill the meteor proccess and run `yarn dev` again in order for the changes to be compiled and the new output to be used by meteor. + + This has the drawback of taking a little longer to run the dev environment, since we can't cache a watched buid. In the other hand, it reduces the friction of modifying internal packages since we don't need to rebuild the project for changes to take effect. + + This will enable us to move more things to separate packages without affecting the dev experience too much. + +- Chore(deps): Bump sharp from 0.30.4 to 0.30.6 ([#25719](https://github.com/RocketChat/Rocket.Chat/pull/25719) by [@dependabot[bot]](https://github.com/dependabot[bot])) + - i18n: Language update from LingoHub 🤖 on 2022-04-04Z ([#25043](https://github.com/RocketChat/Rocket.Chat/pull/25043)) - Merge master into develop & Set version to 4.7.0-develop ([#25028](https://github.com/RocketChat/Rocket.Chat/pull/25028)) +- Merge master into develop & Set version to 5.0.0 ([#25702](https://github.com/RocketChat/Rocket.Chat/pull/25702) by [@felipe-menelau](https://github.com/felipe-menelau)) + +- Regression: Admin Avatar Edit endpoint fix ([#26232](https://github.com/RocketChat/Rocket.Chat/pull/26232)) + +- Regression: [VideoConference] Callee client behaves improperly when accepting a call from someone who lost the connection ([#26101](https://github.com/RocketChat/Rocket.Chat/pull/26101)) + + If the caller loses connection and the callee accepts the call anyway, the client will wait for five seconds for confirmation that they can join the call. This PR improves the UI behavior during those five seconds. + +- Regression: [VideoConference] If the caller loses connection, direct calls are never canceled ([#26099](https://github.com/RocketChat/Rocket.Chat/pull/26099)) + +- Regression: `yarn dev` not working ([#26071](https://github.com/RocketChat/Rocket.Chat/pull/26071)) + - Regression: Add `isPending` status to message ([#25299](https://github.com/RocketChat/Rocket.Chat/pull/25299)) +- Regression: Add appId prop to slashcommand ([#25988](https://github.com/RocketChat/Rocket.Chat/pull/25988)) + + Pass the appId when present to the slashcommand array. This avoid problems with contextual bar and modals not opening. + +- Regression: Add better error messages when call fails ([#26134](https://github.com/RocketChat/Rocket.Chat/pull/26134)) + +- Regression: Add Error boundary to katex render component ([#26067](https://github.com/RocketChat/Rocket.Chat/pull/26067)) + - Regression: Add eslint package to micro services Dockerfile ([#25311](https://github.com/RocketChat/Rocket.Chat/pull/25311)) - Regression: Add select message to system message and thread preview and allow select on legacy template ([#25251](https://github.com/RocketChat/Rocket.Chat/pull/25251)) +- Regression: Add v1 to licenses.add endpoint ([#26311](https://github.com/RocketChat/Rocket.Chat/pull/26311)) + +- Regression: Added missing call button in the contact info contextual bar ([#26135](https://github.com/RocketChat/Rocket.Chat/pull/26135)) + +- Regression: Added missing call button to contact center calls list ([#26119](https://github.com/RocketChat/Rocket.Chat/pull/26119)) + + This PR adds a call button to the contact center calls list rows. + +- Regression: Adjusted priority to run canned responses replace before new parser ([#26298](https://github.com/RocketChat/Rocket.Chat/pull/26298)) + + Canned responses placeholders were not being replaced properly after we changed to the new md parser. + This fix changes the priority so that the canned responses replace logic runs before the parser, thus bringing back this functionality. + + Before: + Screen Shot 2022-07-18 at 19 25 07 + + After: + Screen Shot 2022-07-18 at 19 26 09 + +- Regression: Align TypeScript version across workspaces ([#26184](https://github.com/RocketChat/Rocket.Chat/pull/26184)) + + Some places were still referring to TypeScript 4.3.4 instead of 4.5.5, so this PR targets it. + +- Regression: All users in members list showing as federated ([#26129](https://github.com/RocketChat/Rocket.Chat/pull/26129)) + - Regression: App event listeners broke Slackbridge integration and importers ([#25689](https://github.com/RocketChat/Rocket.Chat/pull/25689)) Some event listeners triggered by Apps were calling `Meteor.user()` in functions that could run outside of Meteor environment - Regression: Assets & Slack Bridge Setting Page not rendering ([#25629](https://github.com/RocketChat/Rocket.Chat/pull/25629)) +- Regression: AutoTranslate on new message template ([#26049](https://github.com/RocketChat/Rocket.Chat/pull/26049)) + - Regression: Avatar not loading on first direct message ([#25211](https://github.com/RocketChat/Rocket.Chat/pull/25211)) fix avatar not loading on a first direct message @@ -795,43 +1452,201 @@ For reasons I've no clue, any client import path matching `**/data/**` will not be included in the final bundle, failing silently on transpiling/bundling. +- Regression: Broken emoji picker on Livechat ([#26128](https://github.com/RocketChat/Rocket.Chat/pull/26128)) + - Regression: bump onboarding-ui version ([#25320](https://github.com/RocketChat/Rocket.Chat/pull/25320)) - Bump to 'next' the onboarding-ui package from fuselage. - Update from 'companyEmail' to 'email' adminData usage types +- Regression: Burger menu showing arrow instead of burguer ([#26170](https://github.com/RocketChat/Rocket.Chat/pull/26170)) + +- Regression: Call toggle missing network disconnection state ([#26237](https://github.com/RocketChat/Rocket.Chat/pull/26237)) + + This PR brings back the network disconnection state to the voip call toggle button + + ![image (4)](https://user-images.githubusercontent.com/6494543/178564719-f436505e-3ae3-4d69-ba5a-27ce8e8c5fba.png) + +- Regression: Calling info on VoipFooter when performing an outbound call ([#26138](https://github.com/RocketChat/Rocket.Chat/pull/26138)) + + ![image](https://user-images.githubusercontent.com/17487063/177395438-a0b2d30a-e0e2-4a31-9b55-2c6c3216bbd7.png) + +- Regression: Cannot logout when CallProvider is unregistered and mounted ([#26158](https://github.com/RocketChat/Rocket.Chat/pull/26158)) + +- Regression: Cannot open Menu in searched message. ([#26172](https://github.com/RocketChat/Rocket.Chat/pull/26172)) + +- Regression: Change Audio settings for device settings as modal title ([#26159](https://github.com/RocketChat/Rocket.Chat/pull/26159)) + - Regression: Change logic to check if connection is online on unstable networks ([#25618](https://github.com/RocketChat/Rocket.Chat/pull/25618)) - Regression: Change preference to be default legacy messages ([#25255](https://github.com/RocketChat/Rocket.Chat/pull/25255)) +- Regression: Changing isEnterprise useQuery name to prevent conflict of queries ([#26116](https://github.com/RocketChat/Rocket.Chat/pull/26116)) + + Changed the name of useQuery hook to prevent conflict of queries with same name. + +- Regression: Channel `type` icon on Engagement Dashboard ([#26269](https://github.com/RocketChat/Rocket.Chat/pull/26269)) + + This PR fixes a bug on which the channel type is inverted. + - Regression: CI playwright ([#25168](https://github.com/RocketChat/Rocket.Chat/pull/25168)) - Regression: CI services build ([#25555](https://github.com/RocketChat/Rocket.Chat/pull/25555)) +- Regression: Clear user selection filter after selecting desired user. ([#26295](https://github.com/RocketChat/Rocket.Chat/pull/26295)) + +- Regression: Close button on modals created via apps not working ([#26127](https://github.com/RocketChat/Rocket.Chat/pull/26127)) + +- Regression: Contact manager edit/view not working ([#26155](https://github.com/RocketChat/Rocket.Chat/pull/26155)) + + Basically, the Contact Center was working, but not the right way. This PR fixes: + - Ability to select Contact Managers from dropdown + - Ability to validate Contact Edits without requesting data a ton of times + - Ability to remove Contact manager from a contact + - Ability to see Contacts and Contact Managers on Contact View + - Fix endpoints validation + - Add validators (ajv) to endpoint, thou not being used yet (since we hit a special endpoint) + +- Regression: Contact manager endpoint usage ([#26063](https://github.com/RocketChat/Rocket.Chat/pull/26063)) + +- Regression: Correct call ringtones ([#26111](https://github.com/RocketChat/Rocket.Chat/pull/26111)) + + - outbound-call-ringing ringtone: Should be played when the outbound call is initiated and not yet established(Current implementation is playing the incoming-call ringtone) + - call-ended ringtone: Should be played whenever a call ends. + +- Regression: Device management table missing device icon and ip text ellipsis ([#26255](https://github.com/RocketChat/Rocket.Chat/pull/26255)) + +- Regression: Do not show federated tooltip on non-federated rooms ([#26115](https://github.com/RocketChat/Rocket.Chat/pull/26115)) + +- Regression: Docker image publish ([#25931](https://github.com/RocketChat/Rocket.Chat/pull/25931)) + +- Regression: Don't open mdm feature modal on registration page ([#26234](https://github.com/RocketChat/Rocket.Chat/pull/26234)) + +- Regression: Emojis displaying as `:undefined:` ([#26141](https://github.com/RocketChat/Rocket.Chat/pull/26141)) + +- Regression: Empty URL previews in messages. ([#26160](https://github.com/RocketChat/Rocket.Chat/pull/26160)) + - Regression: Endpoint types with Ajv Coercing data types ([#25644](https://github.com/RocketChat/Rocket.Chat/pull/25644)) Ajv Coercing data types should be `true` to accept all kinds of data requested. - Regression: eslint not running on packages ([#25305](https://github.com/RocketChat/Rocket.Chat/pull/25305)) +- Regression: Federated users not showing as federated in Room Members ([#26249](https://github.com/RocketChat/Rocket.Chat/pull/26249)) + +- Regression: fix `directory` endpoint not listing teams ([#26310](https://github.com/RocketChat/Rocket.Chat/pull/26310)) + +- Regression: Fix app icons breaking UI ([#26278](https://github.com/RocketChat/Rocket.Chat/pull/26278)) + +- Regression: fix apps path ([#25809](https://github.com/RocketChat/Rocket.Chat/pull/25809)) + +- Regression: Fix apps wrong typing ([#25824](https://github.com/RocketChat/Rocket.Chat/pull/25824)) + +- Regression: Fix assets format ([#26140](https://github.com/RocketChat/Rocket.Chat/pull/26140)) + +- Regression: Fix blackscreen after app install ([#25950](https://github.com/RocketChat/Rocket.Chat/pull/25950)) + + Fixed an error where the client screen would go black after installing an app. This was hapenning because the handleAppAddedOrUpdated function from the AppsProvider had a wrong type for the return of the getAppFromMarketplace function. + + Demo gifs: + + Before + ![app-install-error-before](https://user-images.githubusercontent.com/43561537/174861410-024dff74-b5d9-49ba-ae67-849f1ff239a9.gif) + + After: + ![app-install-error-after](https://user-images.githubusercontent.com/43561537/174861448-58b071e6-8e1b-4391-b49a-44b68249acbf.gif) + +- Regression: Fix breaking omnichannel tests ([#26305](https://github.com/RocketChat/Rocket.Chat/pull/26305)) + +- Regression: Fix call direction ([#26055](https://github.com/RocketChat/Rocket.Chat/pull/26055)) + - Regression: Fix CI monorepo build ([#25107](https://github.com/RocketChat/Rocket.Chat/pull/25107)) - Regression: Fix clicking on visitor's chat in the sidebar does not display the chat window ([#25380](https://github.com/RocketChat/Rocket.Chat/pull/25380)) Fix: livechat room not opening. +- Regression: Fix command previews ([#26199](https://github.com/RocketChat/Rocket.Chat/pull/26199)) + + Fixes slash command previews not being showed + +- Regression: Fix e2e CI ([#25974](https://github.com/RocketChat/Rocket.Chat/pull/25974)) + - Regression: Fix English i18n react text ([#25368](https://github.com/RocketChat/Rocket.Chat/pull/25368)) Incorrect text in reaction tooltip has been fixed +- Regression: Fix extended sidebar item ([#25887](https://github.com/RocketChat/Rocket.Chat/pull/25887)) + - Regression: Fix federation Matrix bridge startup ([#25273](https://github.com/RocketChat/Rocket.Chat/pull/25273)) +- Regression: Fix files list endpoints ([#26226](https://github.com/RocketChat/Rocket.Chat/pull/26226)) + +- Regression: Fix get myself user data ([#26328](https://github.com/RocketChat/Rocket.Chat/pull/26328)) + +- Regression: Fix import endpoints ([#26074](https://github.com/RocketChat/Rocket.Chat/pull/26074)) + +- Regression: Fix job Id not returned by agenda ([#26315](https://github.com/RocketChat/Rocket.Chat/pull/26315)) + +- Regression: Fix marketplace app apis visibility problem ([#26080](https://github.com/RocketChat/Rocket.Chat/pull/26080)) + + Solved a problem that showed an unwanted zero in place of the APIs section for apps that weren't installed/did not have an APIs section. + Before: + ![image](https://user-images.githubusercontent.com/43561537/176743542-8f5d2e97-48e7-4947-a82a-43c3a15ea0ac.png) + + After(non installed app): + ![image](https://user-images.githubusercontent.com/43561537/176744082-0139e15b-b03b-4c03-9267-9a17d14b70e9.png) + + After(installed app) + ![image](https://user-images.githubusercontent.com/43561537/176772870-c5382edc-59e6-42e4-8dfa-f1e4fd0267c0.png) + +- Regression: Fix marketplace releases tab crash bug ([#26162](https://github.com/RocketChat/Rocket.Chat/pull/26162)) + + Fixed a bug where RC would crash because the marketplace releases tab was trying to display undefined data from manually installed apps. + Demo gif: + ![app-releases-tab-crash-error](https://user-images.githubusercontent.com/43561537/177656489-325790d3-49e0-46c8-8ac2-1f74c6a309ad.gif) + +- Regression: Fix micro services ([#26054](https://github.com/RocketChat/Rocket.Chat/pull/26054)) + - Regression: Fix micro services Docker build ([#25193](https://github.com/RocketChat/Rocket.Chat/pull/25193)) - Regression: Fix multi line is not showing an empty line between lines ([#25317](https://github.com/RocketChat/Rocket.Chat/pull/25317)) +- Regression: Fix Omnichannel not working after meteor update ([#26194](https://github.com/RocketChat/Rocket.Chat/pull/26194)) + + Fixed things: + - Omnichannel Directory + - Omnichannel Current Chats + - Auto Selection Algo + - Load Balance Algo + - Manual Selection Algo + - Livechat New Conversations + + Other fixed things: + - Warning on fields deprecation + - Warning on "remove" deprecation + - Remove findAndModify usage + +- Regression: Fix permissions page pagination ([#26304](https://github.com/RocketChat/Rocket.Chat/pull/26304)) + +- Regression: Fix rendered markdown styling on app info page details section ([#26093](https://github.com/RocketChat/Rocket.Chat/pull/26093)) + + Fixed two styling problems on the AppDetails markdown. The first one was a misuse of flex and the second was the fact that the withRichContent flag was missing on the box that received the markdown. + Demo images: + Before: + ![image](https://user-images.githubusercontent.com/43561537/177857346-54476879-2618-452f-8585-1922dcbfa9c1.png) + + After: + ![image](https://user-images.githubusercontent.com/43561537/177857376-e96e4ad3-3410-4847-89b7-df074ff87b2f.png) + + Clickup task: https://app.clickup.com/t/2rwq0q7 + - Regression: Fix reply button not working when hideFlexTab is enabled ([#25306](https://github.com/RocketChat/Rocket.Chat/pull/25306)) +- Regression: Fix routing for public queue and visitor abandonment fiber usage ([#26233](https://github.com/RocketChat/Rocket.Chat/pull/26233)) + +- Regression: Fix scheduler not working ([#26242](https://github.com/RocketChat/Rocket.Chat/pull/26242)) + - Regression: Fix services Docker build on CI ([#25181](https://github.com/RocketChat/Rocket.Chat/pull/25181)) - Regression: Fix services-image-build-check ([#25519](https://github.com/RocketChat/Rocket.Chat/pull/25519)) @@ -848,59 +1663,228 @@ Made changes to one of its child dependencies `matrix-rust-sdk-bindings` that adds pre-built binaries for mac and musl (for alpine). +- Regression: Fix threads list ([#26052](https://github.com/RocketChat/Rocket.Chat/pull/26052)) + +- Regression: Fix users.create call ([#25834](https://github.com/RocketChat/Rocket.Chat/pull/25834)) + +- Regression: Fix voip call wrap-up model not working ([#26024](https://github.com/RocketChat/Rocket.Chat/pull/26024)) + +- Regression: get user from `req` on `ui.interaction` endpoints ([#26256](https://github.com/RocketChat/Rocket.Chat/pull/26256)) + +- Regression: Inline code and copyonly tag styles ([#26173](https://github.com/RocketChat/Rocket.Chat/pull/26173)) + +- Regression: Invalid Voip host issue preventing agents connecting to asterisk ([#26056](https://github.com/RocketChat/Rocket.Chat/pull/26056)) + +- Regression: Last_login translation key ([#26203](https://github.com/RocketChat/Rocket.Chat/pull/26203)) + +- Regression: Link not scrolling to message ([#26073](https://github.com/RocketChat/Rocket.Chat/pull/26073)) + +- Regression: Livechat not rendering UiKit messages with action buttons ([#26327](https://github.com/RocketChat/Rocket.Chat/pull/26327)) + +- Regression: Livechat rooms not opening due to route desync ([#26209](https://github.com/RocketChat/Rocket.Chat/pull/26209)) + + Due to route information only updating on `Tracker.afterFlush` (https://github.com/RocketChat/Rocket.Chat/pull/25990), we found out that calling the `tabBar.openUserInfo()` method at this point will cause a route change to the previous route instead of the current one, preventing livechat rooms from being opened. + + As a provisory solution, we're delaying the opening of the contextual bar, which then ensures that the route info is up to date. Although this solution works, we need to find a more reliable way of ensuring consistent route changes with up-to-date information. + + ### I'm definitely open for better looking alternatives. Please leave a comment if you have a better solution to share. + +- Regression: Matrix Federation regressions ([#26283](https://github.com/RocketChat/Rocket.Chat/pull/26283)) + - Regression: Messages in new message template Crashing. ([#25327](https://github.com/RocketChat/Rocket.Chat/pull/25327)) +- Regression: Meteor uses `projection` for its observes ([#26223](https://github.com/RocketChat/Rocket.Chat/pull/26223)) + - Regression: Missing settings group descriptions ([#25639](https://github.com/RocketChat/Rocket.Chat/pull/25639)) -- Regression: Revert Bugsnag version ([#25313](https://github.com/RocketChat/Rocket.Chat/pull/25313)) +- Regression: moving Community Watermark to `ee` folder ([#26148](https://github.com/RocketChat/Rocket.Chat/pull/26148)) -- Regression: Rocket.Chat Webapp not loading. ([#25349](https://github.com/RocketChat/Rocket.Chat/pull/25349)) + Due to legal reasons, the Watermark used in community Edition was moved to Enterprise folder `ee` -- Regression: Show username and real name on the message system ([#25254](https://github.com/RocketChat/Rocket.Chat/pull/25254)) +- Regression: Non-reactive routes ([#25990](https://github.com/RocketChat/Rocket.Chat/pull/25990)) -- Regression: Shows error if micro service cannot connect to Mongo ([#25301](https://github.com/RocketChat/Rocket.Chat/pull/25301)) + When `Tracker.autorun()` calls are nested, it's possible that an invalidation at the parent render the children non-reactive due to synchronous calls. To avoid that under the callback given by `useSyncExternalStore`, we schedule an `onStoreChange` callback call to not make it reside at the same backtrace. -- Regression: Subscription menu not appearing for non installed but subscribed apps ([#25627](https://github.com/RocketChat/Rocket.Chat/pull/25627)) +- Regression: Omni-chats not getting routed automatically to bots ([#26267](https://github.com/RocketChat/Rocket.Chat/pull/26267)) - Fixed a problem on which the AppMenu component did not appear for apps that had an active subscription but weren't installed, now the rendering of the component is also based on the isSubscribed flag, and the appearance of the uninstall and enable/disable options are based on the app.installed flag so that the correct options appear on all the edge cases. +- Regression: Options overlapping input in Users Autocomplete ([#26309](https://github.com/RocketChat/Rocket.Chat/pull/26309)) + +- Regression: OTR with new React Messages ([#26179](https://github.com/RocketChat/Rocket.Chat/pull/26179)) + + This PR solves 2 OTR issues with new react message components + + - disable the server side message parser for OTR messages + - adds the stopwatch icon for otr messages + + ### Before + Screenshot 2022-07-08 at 12 58 08 AM + + ### After + Screenshot 2022-07-08 at 12 55 08 AM + +- Regression: Parse outbound phone number removing * putting + char ([#26154](https://github.com/RocketChat/Rocket.Chat/pull/26154)) + +- Regression: Re-add view logs button ([#25876](https://github.com/RocketChat/Rocket.Chat/pull/25876)) + + Re-added the view logs button to the appMenu component so that the user can go directly from the marketplace list of apps to the app info page with the logs tab already open. Demo gif: - ![subscription-manager-fix](https://user-images.githubusercontent.com/43561537/170132040-dc8535c0-8056-4fb2-b008-afaece744868.gif) + ![re-add-view-logs-button](https://user-images.githubusercontent.com/43561537/173681990-86c8a93c-bb2e-4540-824d-b7fbb3161356.gif) -- Regression: Update settings groups description ([#25663](https://github.com/RocketChat/Rocket.Chat/pull/25663)) +- Regression: Remove 4.0 version banner ([#26251](https://github.com/RocketChat/Rocket.Chat/pull/26251)) -- Regression: Use exact Node version on micro services Docker images ([#25287](https://github.com/RocketChat/Rocket.Chat/pull/25287)) + Created a migration to disable and dismiss for all users the old 4.0 version banner. + It happened when a new admin user has been added. -- Regression: Validate empty fields for Message template ([#25250](https://github.com/RocketChat/Rocket.Chat/pull/25250)) +- Regression: Remove alpha tag and fix initialization process ([#26248](https://github.com/RocketChat/Rocket.Chat/pull/26248)) -- Regression: VoIp wrap up modal not opening after call disconnect ([#25651](https://github.com/RocketChat/Rocket.Chat/pull/25651)) +- Regression: remove italic from reaction translation ([#26152](https://github.com/RocketChat/Rocket.Chat/pull/26152)) - This PR fixes a bug preventing the wrap up call modal from being displayed after caller or agent ends the call. +- Regression: Removed CE watermark from VoipFooter ([#26239](https://github.com/RocketChat/Rocket.Chat/pull/26239)) -- Regression: yarn dev triggers build dependencies ([#25208](https://github.com/RocketChat/Rocket.Chat/pull/25208)) + The objective of this change is to remove the CE watermark **only** during an active call. The CE watermark will be displayed normally in all other scenarios. Bellow you can see a demonstration of the expected behavior: + + ![ce-watermark-removed-voip](https://user-images.githubusercontent.com/6494543/178615342-8049a2a8-d331-46a9-a8f1-8461ae341b50.gif) -- Release 4.7.0 ([#25390](https://github.com/RocketChat/Rocket.Chat/pull/25390) by [@Himanshu664](https://github.com/Himanshu664) & [@dependabot[bot]](https://github.com/dependabot[bot]) & [@lingohub[bot]](https://github.com/lingohub[bot])) +- Regression: Replace contact center icon ([#26216](https://github.com/RocketChat/Rocket.Chat/pull/26216)) -- Release 4.7.1 ([#25510](https://github.com/RocketChat/Rocket.Chat/pull/25510) by [@felipe-menelau](https://github.com/felipe-menelau)) +- Regression: REST API calls at Engagement Dashboard ([#26235](https://github.com/RocketChat/Rocket.Chat/pull/26235)) -- Release 4.7.2 ([#25580](https://github.com/RocketChat/Rocket.Chat/pull/25580)) + Parameters for GET requests are *not* serialized as for other methods, therefore sending `Date` objects is not viable due to the way `Date.prototype.toString` works. This PR uses `Date.prototype.toISOString` explicitly to serialize dates. -- Test: Migrate 13-permissions from cypress to playwright ([#25558](https://github.com/RocketChat/Rocket.Chat/pull/25558)) +- Regression: Revert Bugsnag version ([#25313](https://github.com/RocketChat/Rocket.Chat/pull/25313)) -
+- Regression: Revert Livechat packages upgrades/removals that were causing issues ([#26077](https://github.com/RocketChat/Rocket.Chat/pull/26077)) -### 👩‍💻👨‍💻 Contributors 😍 +- Regression: Revert replace contact center icon ([#26238](https://github.com/RocketChat/Rocket.Chat/pull/26238)) -- [@Himanshu664](https://github.com/Himanshu664) -- [@aakash-gitdev](https://github.com/aakash-gitdev) -- [@cuonghuunguyen](https://github.com/cuonghuunguyen) +- Regression: Reverting @rocket.chat/mp3-encoder version to fix Audio Message ([#26197](https://github.com/RocketChat/Rocket.Chat/pull/26197)) + + An unknow breaking change (still investigating) happened when upgrading the [@rocket.chat/mp3-encoder](https://github.com/RocketChat/fuselage/tree/develop/packages/mp3-encoder) package to version 0.25.0, because of that we revert the version to 0.24.0 the last know working version. + +- Regression: Rocket.Chat Webapp not loading. ([#25349](https://github.com/RocketChat/Rocket.Chat/pull/25349)) + +- Regression: Room Endpoint Call Issues ([#25928](https://github.com/RocketChat/Rocket.Chat/pull/25928)) + + This PR fixes small management bugs related with channels, rooms and teams + +- Regression: Search on Member List ([#26273](https://github.com/RocketChat/Rocket.Chat/pull/26273)) + +- Regression: Send files with `enter` key ([#26136](https://github.com/RocketChat/Rocket.Chat/pull/26136)) + +- Regression: Set `offset` and `count` optional on `ChatGetThreadsListSchema` ([#25961](https://github.com/RocketChat/Rocket.Chat/pull/25961)) + +- Regression: Show username and real name on the message system ([#25254](https://github.com/RocketChat/Rocket.Chat/pull/25254)) + +- Regression: Shows error if micro service cannot connect to Mongo ([#25301](https://github.com/RocketChat/Rocket.Chat/pull/25301)) + +- Regression: Sidebar icons spacing ([#26139](https://github.com/RocketChat/Rocket.Chat/pull/26139)) + + - Fixed the sidebar icons ('display' and 'create new') spacing issue + + before: + ![image](https://user-images.githubusercontent.com/5263975/178897210-50615ea9-28d5-4b35-a93a-c5facea365e5.png) + + + + after: + + ![image](https://user-images.githubusercontent.com/5263975/178896945-1bf71112-8a01-4db6-9f9b-20ea778496f7.png) + +- Regression: Special characters on phone number ([#26241](https://github.com/RocketChat/Rocket.Chat/pull/26241)) + + PR Includes: + - Keep focus on phone input of dial pad + - Handle submit with "Enter" key + - Remove mask and mandatory "+" char + - Long press for "0"/"+" button + +- Regression: Subscription menu not appearing for non installed but subscribed apps ([#25627](https://github.com/RocketChat/Rocket.Chat/pull/25627)) + + Fixed a problem on which the AppMenu component did not appear for apps that had an active subscription but weren't installed, now the rendering of the component is also based on the isSubscribed flag, and the appearance of the uninstall and enable/disable options are based on the app.installed flag so that the correct options appear on all the edge cases. + Demo gif: + ![subscription-manager-fix](https://user-images.githubusercontent.com/43561537/170132040-dc8535c0-8056-4fb2-b008-afaece744868.gif) + +- Regression: TOTP Modal with new rest api package ([#25893](https://github.com/RocketChat/Rocket.Chat/pull/25893)) + +- Regression: UIKit buttons auth user validation ([#26171](https://github.com/RocketChat/Rocket.Chat/pull/26171)) + + Fix the validation to match the new feature that allows apps to register auth-required routes. + +- Regression: Unable to click on UiKit buttons provided by apps ([#26125](https://github.com/RocketChat/Rocket.Chat/pull/26125)) + +- Regression: Unable to edit user details via admin panel ([#25923](https://github.com/RocketChat/Rocket.Chat/pull/25923)) + +- Regression: Unavailable devices in unsupported browsers ([#26174](https://github.com/RocketChat/Rocket.Chat/pull/26174)) + +- Regression: Unhandled Exceptions metric causing a secondary exception ([#26088](https://github.com/RocketChat/Rocket.Chat/pull/26088)) + +- Regression: Update error message on `useEndpointActionExperimental` ([#26062](https://github.com/RocketChat/Rocket.Chat/pull/26062)) + + This PR changes the way we show an error message to the user on the `useEndpointActionExperimental` hook, previously for `Object` error messages it was being shown as undefined + +- Regression: Update message reaction text ([#26097](https://github.com/RocketChat/Rocket.Chat/pull/26097)) + +- Regression: Update settings groups description ([#25663](https://github.com/RocketChat/Rocket.Chat/pull/25663)) + +- Regression: Use exact Node version on micro services Docker images ([#25287](https://github.com/RocketChat/Rocket.Chat/pull/25287)) + +- Regression: Use fname instead real unique name for Voip ([#26319](https://github.com/RocketChat/Rocket.Chat/pull/26319)) + + Affect: + - Voip room header + - Contacts table + - Contact info + +- Regression: UserInfo/RoomInfo Menu ([#26252](https://github.com/RocketChat/Rocket.Chat/pull/26252)) + + **note**: next fuselage's version needed + + #### before + ![Screen Shot 2022-07-13 at 12 24 38](https://user-images.githubusercontent.com/27704687/178771262-d482b300-de80-4961-be2e-8c034480d237.png) + + #### after + ![Screen Shot 2022-07-13 at 12 25 39](https://user-images.githubusercontent.com/27704687/178771460-db10883b-aa6d-4254-82d4-8cadd6991ae8.png) + +- Regression: Users on new sessions are forced to re-configure 2fa ([#26117](https://github.com/RocketChat/Rocket.Chat/pull/26117)) + +- Regression: Users Table loading state ([#26079](https://github.com/RocketChat/Rocket.Chat/pull/26079)) + +- Regression: Validate empty fields for Message template ([#25250](https://github.com/RocketChat/Rocket.Chat/pull/25250)) + +- Regression: VoIp wrap up modal not opening after call disconnect ([#25651](https://github.com/RocketChat/Rocket.Chat/pull/25651)) + + This PR fixes a bug preventing the wrap up call modal from being displayed after caller or agent ends the call. + +- Regression: Webhook Integration Creation + string error toast msg ([#26008](https://github.com/RocketChat/Rocket.Chat/pull/26008)) + +- Regression: yarn dev triggers build dependencies ([#25208](https://github.com/RocketChat/Rocket.Chat/pull/25208)) + +- Revert: "Chore: Collect e2e coverage" ([#25936](https://github.com/RocketChat/Rocket.Chat/pull/25936)) + +- Test: Migrate 13-permissions from cypress to playwright ([#25558](https://github.com/RocketChat/Rocket.Chat/pull/25558)) + + + +### 👩‍💻👨‍💻 Contributors 😍 + +- [@BenWiederhake](https://github.com/BenWiederhake) +- [@Himanshu664](https://github.com/Himanshu664) +- [@Hudell](https://github.com/Hudell) +- [@Kunalvrm555](https://github.com/Kunalvrm555) +- [@Sh0uld](https://github.com/Sh0uld) +- [@aakash-gitdev](https://github.com/aakash-gitdev) +- [@cuonghuunguyen](https://github.com/cuonghuunguyen) - [@dependabot[bot]](https://github.com/dependabot[bot]) - [@divinespear](https://github.com/divinespear) - [@eduardofcabrera](https://github.com/eduardofcabrera) - [@felipe-menelau](https://github.com/felipe-menelau) +- [@g-thome](https://github.com/g-thome) +- [@joakimaho](https://github.com/joakimaho) - [@kibonusp](https://github.com/kibonusp) -- [@lingohub[bot]](https://github.com/lingohub[bot]) +- [@kodiakhq[bot]](https://github.com/kodiakhq[bot]) +- [@matthias4217](https://github.com/matthias4217) - [@ostjen](https://github.com/ostjen) - [@paulobernardoaf](https://github.com/paulobernardoaf) - [@sidmohanty11](https://github.com/sidmohanty11) @@ -910,6 +1894,7 @@ - [@AllanPazRibeiro](https://github.com/AllanPazRibeiro) - [@KevLehman](https://github.com/KevLehman) +- [@LucianoPierdona](https://github.com/LucianoPierdona) - [@MarcosSpessatto](https://github.com/MarcosSpessatto) - [@MartinSchoeler](https://github.com/MartinSchoeler) - [@PedroRorato](https://github.com/PedroRorato) @@ -919,9 +1904,11 @@ - [@amolghode1981](https://github.com/amolghode1981) - [@carlosrodrigues94](https://github.com/carlosrodrigues94) - [@cauefcr](https://github.com/cauefcr) +- [@csuadev](https://github.com/csuadev) - [@d-gubert](https://github.com/d-gubert) - [@debdutdeb](https://github.com/debdutdeb) - [@dougfabris](https://github.com/dougfabris) +- [@dudanogueira](https://github.com/dudanogueira) - [@felipe-rod123](https://github.com/felipe-rod123) - [@filipemarins](https://github.com/filipemarins) - [@gabriellsh](https://github.com/gabriellsh) @@ -943,13 +1930,14 @@ - [@souzaramon](https://github.com/souzaramon) - [@tapiarafael](https://github.com/tapiarafael) - [@tassoevan](https://github.com/tassoevan) +- [@thassiov](https://github.com/thassiov) - [@tiagoevanp](https://github.com/tiagoevanp) - [@tmontini](https://github.com/tmontini) - [@weslley543](https://github.com/weslley543) - [@yash-rajpal](https://github.com/yash-rajpal) -# 4.7.4 -`2022-05-30 · 1 🐛 · 1 🔍 · 2 👩‍💻👨‍💻` +# 4.8.2 +`2022-07-21 · 4 🐛 · 3 🔍 · 7 👩‍💻👨‍💻` ### Engine versions - Node: `14.18.3` @@ -959,40 +1947,43 @@ ### 🐛 Bug fixes -- Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) +- Error "numRequestsAllowed" property in rateLimiter for REST API endpoint when upgrading ([#26058](https://github.com/RocketChat/Rocket.Chat/pull/26058)) -
-🔍 Minor changes +- Not showing edit message button when blocking edit after N minutes ([#25724](https://github.com/RocketChat/Rocket.Chat/pull/25724) by [@matthias4217](https://github.com/matthias4217)) + Previously, in Rocketchat 4.7.0 and later, as mentioned in https://github.com/RocketChat/Rocket.Chat/issues/25478, the edit button was not displayed on the interface in the minute after having sent a message. This is now fixed : messages can be edited right after sending them. -- Load missed messages from opened rooms when reconnect ([#553](https://github.com/RocketChat/Rocket.Chat/pull/553)) +- Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) -
+- Settings not being overwritten to their default values ([#25891](https://github.com/RocketChat/Rocket.Chat/pull/25891)) -### 👩‍💻👨‍💻 Core Team 🤓 +
+🔍 Minor changes -- [@ggazzo](https://github.com/ggazzo) -- [@rodrigok](https://github.com/rodrigok) -# 4.7.3 -`2022-05-20 · 1 🐛 · 1 👩‍💻👨‍💻` +- Chore: Avoid unneeded permission updates when EE license is applied ([#26253](https://github.com/RocketChat/Rocket.Chat/pull/26253)) -### Engine versions -- Node: `14.18.3` -- NPM: `6.14.15` -- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- Release 4.8.1 ([#25814](https://github.com/RocketChat/Rocket.Chat/pull/25814) by [@Sh0uld](https://github.com/Sh0uld)) -### 🐛 Bug fixes +- Release 4.8.2 ([#26326](https://github.com/RocketChat/Rocket.Chat/pull/26326) by [@matthias4217](https://github.com/matthias4217)) +
-- Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) +### 👩‍💻👨‍💻 Contributors 😍 + +- [@Sh0uld](https://github.com/Sh0uld) +- [@matthias4217](https://github.com/matthias4217) ### 👩‍💻👨‍💻 Core Team 🤓 +- [@KevLehman](https://github.com/KevLehman) +- [@dudanogueira](https://github.com/dudanogueira) - [@ggazzo](https://github.com/ggazzo) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tiagoevanp](https://github.com/tiagoevanp) -# 4.7.2 -`2022-05-20 · 5 🐛 · 2 🔍 · 7 👩‍💻👨‍💻` +# 4.8.1 +`2022-06-08 · 4 🐛 · 5 👩‍💻👨‍💻` ### Engine versions - Node: `14.18.3` @@ -1002,42 +1993,30 @@ ### 🐛 Bug fixes -- Dynamic load matrix is enabled and handle failure ([#25495](https://github.com/RocketChat/Rocket.Chat/pull/25495)) - -- Initial User not added to default channel ([#25544](https://github.com/RocketChat/Rocket.Chat/pull/25544)) - - If injecting initial user. The user wasn’t added to the default General channel - -- One of the triggers was not working correctly ([#25409](https://github.com/RocketChat/Rocket.Chat/pull/25409)) - -- UI/UX issues on Live Chat widget ([#25407](https://github.com/RocketChat/Rocket.Chat/pull/25407)) - -- User abandonment setting was not working doe to failing event hook ([#25520](https://github.com/RocketChat/Rocket.Chat/pull/25520)) +- AccountBox checks for condition ([#25708](https://github.com/RocketChat/Rocket.Chat/pull/25708)) - A setting watcher and the query for grabbing abandoned chats were broken, now they're not. +- Bump meteor-node-stubs to version 1.2.3 ([#25669](https://github.com/RocketChat/Rocket.Chat/pull/25669) by [@Sh0uld](https://github.com/Sh0uld)) -
-🔍 Minor changes + With meteor-node-stubs version 1.2.3 a bug was fixed, which occured in issue #25460 and probably #25513 (last one not tested). + For the issue in meteor see: https://github.com/meteor/meteor/issues/11974 +- Fix prom-client new promise usage ([#25781](https://github.com/RocketChat/Rocket.Chat/pull/25781)) -- Chore: Add Livechat repo into Monorepo packages ([#25312](https://github.com/RocketChat/Rocket.Chat/pull/25312)) +- Wrong argument name preventing Omnichannel Chat Forward to User ([#25723](https://github.com/RocketChat/Rocket.Chat/pull/25723)) -- Release 4.7.2 ([#25580](https://github.com/RocketChat/Rocket.Chat/pull/25580)) +### 👩‍💻👨‍💻 Contributors 😍 -
+- [@Sh0uld](https://github.com/Sh0uld) ### 👩‍💻👨‍💻 Core Team 🤓 -- [@MartinSchoeler](https://github.com/MartinSchoeler) -- [@cauefcr](https://github.com/cauefcr) -- [@d-gubert](https://github.com/d-gubert) -- [@dougfabris](https://github.com/dougfabris) -- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@KevLehman](https://github.com/KevLehman) +- [@dudanogueira](https://github.com/dudanogueira) - [@ggazzo](https://github.com/ggazzo) - [@tiagoevanp](https://github.com/tiagoevanp) -# 4.7.1 -`2022-05-13 · 1 🎉 · 2 🐛 · 1 🔍 · 4 👩‍💻👨‍💻` +# 4.8.0 +`2022-05-31 · 16 🎉 · 13 🚀 · 55 🐛 · 151 🔍 · 52 👩‍💻👨‍💻` ### Engine versions - Node: `14.18.3` @@ -1047,47 +2026,19 @@ ### 🎉 New features -- Use setting to determine if initial general channel is needed ([#25441](https://github.com/RocketChat/Rocket.Chat/pull/25441) by [@felipe-menelau](https://github.com/felipe-menelau)) - - - Adds flag responsible for overwriting #general channel creation - -### 🐛 Bug fixes - - -- LDAP sync removing users from channels when multiple groups are mapped to it ([#25434](https://github.com/RocketChat/Rocket.Chat/pull/25434)) - -- Spotlight results showing usernames instead of real names ([#25471](https://github.com/RocketChat/Rocket.Chat/pull/25471)) - -
-🔍 Minor changes - - -- Release 4.7.1 ([#25510](https://github.com/RocketChat/Rocket.Chat/pull/25510) by [@felipe-menelau](https://github.com/felipe-menelau)) - -
- -### 👩‍💻👨‍💻 Contributors 😍 - -- [@felipe-menelau](https://github.com/felipe-menelau) - -### 👩‍💻👨‍💻 Core Team 🤓 +- Ability for RC server to check the business hour for a specific department ([#25436](https://github.com/RocketChat/Rocket.Chat/pull/25436)) -- [@d-gubert](https://github.com/d-gubert) -- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) -- [@sampaiodiego](https://github.com/sampaiodiego) +- Add expire index to integration history ([#25087](https://github.com/RocketChat/Rocket.Chat/pull/25087)) -# 4.7.0 -`2022-05-04 · 4 🎉 · 7 🚀 · 33 🐛 · 69 🔍 · 35 👩‍💻👨‍💻` +- Add new app events for pin, react and follow message ([#25337](https://github.com/RocketChat/Rocket.Chat/pull/25337)) -### Engine versions -- Node: `14.18.3` -- NPM: `6.14.15` -- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- Add new events after user login, logout and change his status ([#25234](https://github.com/RocketChat/Rocket.Chat/pull/25234)) -### 🎉 New features +- Add option to show mentions badge when show counter is disabled ([#25329](https://github.com/RocketChat/Rocket.Chat/pull/25329)) +- Add user events for apps ([#25165](https://github.com/RocketChat/Rocket.Chat/pull/25165)) -- Add expire index to integration history ([#25087](https://github.com/RocketChat/Rocket.Chat/pull/25087)) +- Adding app button on user dropdown ([#25326](https://github.com/RocketChat/Rocket.Chat/pull/25326)) - Alpha Matrix Federation ([#23688](https://github.com/RocketChat/Rocket.Chat/pull/23688)) @@ -1097,6 +2048,63 @@ - Expand Apps Engine's environment variable allowed list ([#23870](https://github.com/RocketChat/Rocket.Chat/pull/23870) by [@cuonghuunguyen](https://github.com/cuonghuunguyen)) +- Federation (Alpha Stabilization) ([#25457](https://github.com/RocketChat/Rocket.Chat/pull/25457)) + +- Get user's preferred language via apps ([#25514](https://github.com/RocketChat/Rocket.Chat/pull/25514)) + +- Marketplace new app details page ([#24711](https://github.com/RocketChat/Rocket.Chat/pull/24711)) + + Change the app details page layout for the new marketplace UI. General Task: [MKP12 - New UI - App Detail Page](https://app.clickup.com/t/1na769h) + + ## [MKP12 - Tab Navigation](https://app.clickup.com/t/2452f5u) + New tab navigation layout for the app details page. Now the app details page is divided into three sections, details, logs, and settings, that can each be accessed through a Tabs fuselage component. + + Demo gif: + ![tab_navigation_demo_gif](https://user-images.githubusercontent.com/43561537/157276436-3dab34c5-20da-4f5d-99d0-54c1c718ac1f.gif) + + ## [MKP12 - Header](https://app.clickup.com/t/25rhm0x) + Implemented a new header for the marketplaces app details page. + -Changed the size of the app name; + -Implemented the app description field on the header; + -Changed the "metadata" section of the header(The part with the version and author information) now it also shows the last time the app was updated; + -Created a chip that will show when an app is part of one or more bundles and inform which are the bundles; + -Implemented a tooltip for the bundle chips; + -Created a new button + data badge component to substitute the current App Status; + -Changed the title of the "purchase button". Now it shows different text based on the "purchase type" of the app; + -Created a new Pricing & Status display which shows the price when the app is not bought/installed and shows the app status(Enabled/Disabled) when it is bought/installed; + -Changed the way the tabs are rendered, now if the app is not installed(and consequently doesn't have logs and settings tab) it will not render these tabs; + + Demo gif: + ![new-header-gif](https://user-images.githubusercontent.com/43561537/159064599-fd64dfe2-86a3-47da-81ba-1e83f1b87432.gif) + + ## [MKP12 - Configuration Tab](https://app.clickup.com/t/2452gh4) + Delivered together with the tab-navigation task. Changed the app settings from the details of the app to the new settings tab. + Demo image: + ![New configuration tab](https://user-images.githubusercontent.com/43561537/160211324-95db0566-85bf-4dde-a814-3c6f23dcee4d.png) + + ## [MKP12 - Log Tab](https://app.clickup.com/t/2452gg1) + Changed the place of the app logs from the page to the new logs tab. Also changed some styles of the logs accordions to fit better with the new container. + + Before: + ![Before](https://user-images.githubusercontent.com/43561537/160210302-148ce584-604f-40ff-8209-141667016163.png) + + After + ![After](https://user-images.githubusercontent.com/43561537/160210984-d4060c5a-f912-4ef9-87e3-fa459080e2d4.png) + + ## [MKP12 - Page Header](https://app.clickup.com/t/29b0b12) + Changed the design for the page header of the app details page from a title on the left with a save and back button on the right to a back arrow icon on the left side of the title with the save button still on the right. Also changed the title of the page from App details to Back. + Edit: After some design reconsideration, the page title was changed to App Info. + Demo gif: + ![new_page_header_app_details](https://user-images.githubusercontent.com/43561537/160937741-f5514f70-f43b-4400-8b2f-a5a26f95de9d.gif) + + ## [MKP12 - Detail Tab](https://app.clickup.com/t/2452gf7) + Implemented markdown on the description section of the app details page, now the description will show the detailedDescription.rendered (as rendered JSX) information in case it exists and show the description (a.k.a. short description) information in case it doesn't. Unfortunately, as of right now no app has a visual example of a markdown description and because of that, I will not be able to provide a demo image/gif for this PR. + + ## [MKP12 - Slider Component](https://app.clickup.com/t/2452h26) + Created an image carousel component on the app details page. This component receives images from the apps/appId/screenshots endpoint and shows them on the content section of the app details of any apps that have screenshots registered, if the app has no screenshots it simply shows nothing where the carousel should be. This component is complete with keyboard arrow navigation on the "open" carousel, hover highlight on the carousel preview and close on esc press. + Demo gif: + ![new_carousel_component](https://user-images.githubusercontent.com/43561537/167415212-9d8359c7-4132-4afa-a698-8be4ab1e1393.gif) + - Message Template React Component ([#23971](https://github.com/RocketChat/Rocket.Chat/pull/23971)) Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template). @@ -1108,8 +2116,42 @@ image -### 🚀 Improvements - +- New button for network outage ([#25499](https://github.com/RocketChat/Rocket.Chat/pull/25499)) + + When network outage happens it should be conveyed to the user with special icon. This icon should not be clickable. + Network outage handling is handled in https://app.clickup.com/t/245c0d8 task. + +- New stats rewrite ([#25078](https://github.com/RocketChat/Rocket.Chat/pull/25078) by [@ostjen](https://github.com/ostjen)) + + Add the following new statistics (**metrics**): + + - Total users with TOTP enabled; + - Total users with 2FA enabled; + - Total pinned messages; + - Total starred messages; + - Total email messages; + - Total rooms with at least one starred message; + - Total rooms with at least one pinned message; + - Total encrypted rooms; + - Total link invitations; + - Total email invitations; + - Logo change; + - Number of custom script lines; + - Number of custom CSS lines; + - Number of rooms inside teams; + - Number of default (auto-join) rooms inside teams; + - Number of users created through link invitation; + - Number of users created through manual entry; + - Number of imported users (by import type); + +- Star message, report and delete message events ([#25383](https://github.com/RocketChat/Rocket.Chat/pull/25383)) + +### 🚀 Improvements + + +- **ENTERPRISE:** Allow mapping LDAP groups to multiple RC roles ([#23849](https://github.com/RocketChat/Rocket.Chat/pull/23849)) + + - Add support to mapping LDAP groups to multiple roles (by specifying arrays in the "User Data Group Map" enterprise setting. - Add OTR Room States ([#24565](https://github.com/RocketChat/Rocket.Chat/pull/24565)) @@ -1127,23 +2169,37 @@ - Add tooltip to sidebar room menu ([#24405](https://github.com/RocketChat/Rocket.Chat/pull/24405) by [@Himanshu664](https://github.com/Himanshu664)) +- add warnings for federation setup ([#25684](https://github.com/RocketChat/Rocket.Chat/pull/25684)) + - Added MaxNickNameLength and MaxBioLength constants ([#25231](https://github.com/RocketChat/Rocket.Chat/pull/25231) by [@aakash-gitdev](https://github.com/aakash-gitdev)) - Added tooltip options for message menu ([#24431](https://github.com/RocketChat/Rocket.Chat/pull/24431) by [@Himanshu664](https://github.com/Himanshu664)) +- Fix multiple bugs with Matrix bridge ([#25318](https://github.com/RocketChat/Rocket.Chat/pull/25318)) + - Improve active/hover colors in account sidebar ([#25024](https://github.com/RocketChat/Rocket.Chat/pull/25024) by [@Himanshu664](https://github.com/Himanshu664)) +- New admin settings Page ([#25439](https://github.com/RocketChat/Rocket.Chat/pull/25439)) + + ![Screen Shot 2022-05-09 at 11 31 58](https://user-images.githubusercontent.com/27704687/167432811-f4970f23-5dae-48a0-a427-92269d08a859.png) + +- Pass allowDiskUse to channel aggregations on engagement dashboard ([#22374](https://github.com/RocketChat/Rocket.Chat/pull/22374)) + - Performance for some Omnichannel features ([#25217](https://github.com/RocketChat/Rocket.Chat/pull/25217)) - Rename upgrade tab routes ([#25097](https://github.com/RocketChat/Rocket.Chat/pull/25097)) Change 'upgrade tab' routes names from camelCase ('goFullyFeatured') to kebab-case ('go-fully-featured') due to URL naming consistency. Changed types, main function and test. +- Unify voip streams into single stream ([#25108](https://github.com/RocketChat/Rocket.Chat/pull/25108)) + ### 🐛 Bug fixes - Add katex render to new message react template ([#25239](https://github.com/RocketChat/Rocket.Chat/pull/25239)) +- Add open user card to user avatar ([#25445](https://github.com/RocketChat/Rocket.Chat/pull/25445)) + - Add reaction not working in legacy messages ([#25222](https://github.com/RocketChat/Rocket.Chat/pull/25222)) - Added invalid password error message ([#24714](https://github.com/RocketChat/Rocket.Chat/pull/24714) by [@Himanshu664](https://github.com/Himanshu664)) @@ -1169,6 +2225,18 @@ - After: ![image](https://user-images.githubusercontent.com/30026625/161831092-9ee77b51-b083-4f45-9c48-ab2e0511c4d6.png) +- Change form body parameter charset to UTF-8 to fix issue #25456 ([#25673](https://github.com/RocketChat/Rocket.Chat/pull/25673) by [@divinespear](https://github.com/divinespear)) + + since [mscdex/busboy](https://github.com/mscdex/busboy) 1.5.0, new option named `defParamCharset` for form body parameter encoding is added with default value `latin1`, so unicode filenames are broken since 4.7.0. + + ![Screenshot from 2022-05-28 16-26-06](https://user-images.githubusercontent.com/126630/170815447-1f3bd579-243a-42d3-86f6-814aeaa30ce9.png) + +- Change NPS Vote identifier + nps index to unique ([#25423](https://github.com/RocketChat/Rocket.Chat/pull/25423)) + +- Click to join button Jitsi Call ([#25569](https://github.com/RocketChat/Rocket.Chat/pull/25569)) + + Added `ToolboxProvider` to `MessageListProvider` and fixed actionLink.js open function exec + - Client disconnection on network loss ([#25170](https://github.com/RocketChat/Rocket.Chat/pull/25170)) Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online @@ -1209,20 +2277,48 @@ - End call button disappearing when on-hold ([#24936](https://github.com/RocketChat/Rocket.Chat/pull/24936)) +- Failure to update Integration History index ([#25473](https://github.com/RocketChat/Rocket.Chat/pull/25473)) + +- Fix max-width message block ([#25686](https://github.com/RocketChat/Rocket.Chat/pull/25686)) + +- Fixing app contextual bar functionality ([#25615](https://github.com/RocketChat/Rocket.Chat/pull/25615)) + +- Fixing Network connectivity issues with SIP client. ([#25391](https://github.com/RocketChat/Rocket.Chat/pull/25391)) + + The previous PR https://github.com/RocketChat/Rocket.Chat/pull/25170 did not handle the issues completely. + This PR is expected to handle + 1. Clearing call related UI when the network is disconnected or switched. + 2. Do clean connectivity. There were few issues discovered in earlier implementation. e.g endpoint would randomly + get disconnected after a while. This was due to the fact that the earlier socket disconnection caused the + removal of contact on asterisk. This should be fixed in this PR. + 3. This PR contains a lot of logs. This will be removed before the final merge. + - FormData uploads not working ([#25069](https://github.com/RocketChat/Rocket.Chat/pull/25069)) - Full error message is visible ([#24856](https://github.com/RocketChat/Rocket.Chat/pull/24856) by [@Himanshu664](https://github.com/Himanshu664)) - Incorrect websocket url in livechat widget ([#25261](https://github.com/RocketChat/Rocket.Chat/pull/25261)) +- Integrations avatar attribute misuse ([#25283](https://github.com/RocketChat/Rocket.Chat/pull/25283)) + - Invitation links don't redirect to the registration form ([#25082](https://github.com/RocketChat/Rocket.Chat/pull/25082)) - Message menu action not working on legacy messages. ([#25148](https://github.com/RocketChat/Rocket.Chat/pull/25148)) +- Message menu dropdown not working on Mobile Web ([#25616](https://github.com/RocketChat/Rocket.Chat/pull/25616)) + - Message preview not available for queued chats ([#25092](https://github.com/RocketChat/Rocket.Chat/pull/25092)) - NPS never finishing sending results ([#25067](https://github.com/RocketChat/Rocket.Chat/pull/25067)) +- Ordered and unordered list styles, Line breaks. ([#25494](https://github.com/RocketChat/Rocket.Chat/pull/25494)) + + Also removed the message.md cache from server, since changes in the parser might break messages in the future (and will in this specific case). + +- Pinned Message display cutting off information ([#25535](https://github.com/RocketChat/Rocket.Chat/pull/25535)) + +- Prevent federation crash on invite users as a non-owner user ([#25683](https://github.com/RocketChat/Rocket.Chat/pull/25683)) + - Prevent sequential messages edited icon to hide on hover ([#24984](https://github.com/RocketChat/Rocket.Chat/pull/24984)) ### before @@ -1235,10 +2331,19 @@ Modify Meteor's `HTTP.call` to add back proxy support +- Quote message spacing ([#25613](https://github.com/RocketChat/Rocket.Chat/pull/25613)) + - Read receipts show with color gray when not read yet ([#25244](https://github.com/RocketChat/Rocket.Chat/pull/25244)) - Read receipts showing before message read ([#25216](https://github.com/RocketChat/Rocket.Chat/pull/25216)) +- Remove 'total' text in admin info page ([#25638](https://github.com/RocketChat/Rocket.Chat/pull/25638)) + + - Remove initial 'total' text from rooms and messages groups in the admin info page + - Add 'total' before 'rooms' and 'messages' title on the same section. To use the new 'Total Rooms', was created a new key in the en.i18n.json file. + +- Removing user also removes them from Omni collections ([#25444](https://github.com/RocketChat/Rocket.Chat/pull/25444)) + - Replace encrypted text to Encrypted Message Placeholder ([#24166](https://github.com/RocketChat/Rocket.Chat/pull/24166)) ### before @@ -1253,16 +2358,44 @@ - room creation fails if app framework is disabled ([#25200](https://github.com/RocketChat/Rocket.Chat/pull/25200)) +- Rooms' names turn lower case on CSV import ([#24612](https://github.com/RocketChat/Rocket.Chat/pull/24612)) + + * Change 'Settings' import to not get cached configs + * Remove update `UI_Allow_room_names_with_special_chars` value + +- Sanitize customUserStatus and fix infinite loop ([#25449](https://github.com/RocketChat/Rocket.Chat/pull/25449)) + + ### Additional improves: + - usage of RHF to avoid unnecessary Add and Edit components separately and form validation + - usage of `GenericTableV2` and some hooks to avoid unnecessary code + - fix `IUserStatus` type + - improves in UI design + - improves **empty** and **loading** state + - improves files structure + + [LOOP ERROR ATTACHMENT] + ![Screen Shot 2022-05-09 at 19 42 53](https://user-images.githubusercontent.com/27704687/167510439-1980461c-a885-46d2-9a49-79da432c7521.png) + +- Settings listeners not receiving overwritten values from env vars ([#25448](https://github.com/RocketChat/Rocket.Chat/pull/25448)) + - Showing Blank Message Inside Report ([#25007](https://github.com/RocketChat/Rocket.Chat/pull/25007)) https://user-images.githubusercontent.com/53515714/161038085-4a86c7ae-6751-4996-9767-b1c9e0331a6c.mp4 - Toolbox hiding under contextual bar ([#25237](https://github.com/RocketChat/Rocket.Chat/pull/25237)) +- Unable to see channel member list by authorized channel roles ([#25412](https://github.com/RocketChat/Rocket.Chat/pull/25412)) + +- Upgrade tab loader in incorrect position ([#25398](https://github.com/RocketChat/Rocket.Chat/pull/25398)) + + - Add invisible prop to iframe when loading state is active. + - Upgrade Tab showing for a split second ([#25050](https://github.com/RocketChat/Rocket.Chat/pull/25050)) - Use correct room property for call ended at ([#24932](https://github.com/RocketChat/Rocket.Chat/pull/24932)) +- useCurrentChatTags is not a function ([#25604](https://github.com/RocketChat/Rocket.Chat/pull/25604)) + - UserAutoComplete not rendering UserAvatar correctly ([#25055](https://github.com/RocketChat/Rocket.Chat/pull/25055)) ### before @@ -1312,6 +2445,12 @@ - Bump template-file from 6.0.0 to 6.0.1 ([#25002](https://github.com/RocketChat/Rocket.Chat/pull/25002) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Add /v1/video-conference endpoint types ([#25278](https://github.com/RocketChat/Rocket.Chat/pull/25278)) + +- Chore: Add channel endpoints (rest-typings) ([#25279](https://github.com/RocketChat/Rocket.Chat/pull/25279)) + +- Chore: Add client folder to CODEOWNERS ([#25397](https://github.com/RocketChat/Rocket.Chat/pull/25397)) + - Chore: Add error boundary to message component ([#25223](https://github.com/RocketChat/Rocket.Chat/pull/25223)) Not crash the whole application if something goes wrong in the MessageList component. @@ -1324,127 +2463,328 @@ See title +- Chore: Add typings for /v1/webdav.getMyAccounts ([#25276](https://github.com/RocketChat/Rocket.Chat/pull/25276)) + - Chore: Add yarn plugin to check node and yarn version ([#25224](https://github.com/RocketChat/Rocket.Chat/pull/25224)) +- Chore: bump fuselage ([#25605](https://github.com/RocketChat/Rocket.Chat/pull/25605)) + - Chore: Bump fuselage ([#25371](https://github.com/RocketChat/Rocket.Chat/pull/25371)) - Chore: Bump Fuselage packages ([#25259](https://github.com/RocketChat/Rocket.Chat/pull/25259)) - Chore: Cancel running jobs if PR is updated ([#24708](https://github.com/RocketChat/Rocket.Chat/pull/24708)) -- Chore: Convert admin custom sound to tsx ([#25128](https://github.com/RocketChat/Rocket.Chat/pull/25128)) +- Chore: Chore add validation option to rest endpoints ([#25443](https://github.com/RocketChat/Rocket.Chat/pull/25443)) -- Chore: Convert LivechatAgentActivity to raw model and TS ([#25123](https://github.com/RocketChat/Rocket.Chat/pull/25123)) +- Chore: Code Improvements for #25391 ([#25606](https://github.com/RocketChat/Rocket.Chat/pull/25606)) -- Chore: Convert Mailer to TS ([#25121](https://github.com/RocketChat/Rocket.Chat/pull/25121)) +- Chore: Convert `UserStatusMenu` to TS ([#25265](https://github.com/RocketChat/Rocket.Chat/pull/25265)) -- Chore: Convert NotificationStatus to TS ([#25125](https://github.com/RocketChat/Rocket.Chat/pull/25125)) +- Chore: Convert additionalForms ([#25586](https://github.com/RocketChat/Rocket.Chat/pull/25586)) -- Chore: Create README.md for Rest Typings ([#25335](https://github.com/RocketChat/Rocket.Chat/pull/25335)) +- Chore: Convert Admin -> Rooms to TS ([#25348](https://github.com/RocketChat/Rocket.Chat/pull/25348)) -- Chore: ensure scripts use cross-env and ignore some dirs (ROC-54) ([#25218](https://github.com/RocketChat/Rocket.Chat/pull/25218)) +- Chore: Convert admin custom sound to tsx ([#25128](https://github.com/RocketChat/Rocket.Chat/pull/25128)) - - data and test-failure should be ignored - - ensure scripts use cross-env +- Chore: Convert Admin/OAuthApps to TS ([#25277](https://github.com/RocketChat/Rocket.Chat/pull/25277)) -- Chore: Fix return type warnings ([#25275](https://github.com/RocketChat/Rocket.Chat/pull/25275)) + - Converts Admin/OAuthApps to TS. + - migrated forms to react-hook-form -- Chore: Migrate oauth2server to typescript ([#25126](https://github.com/RocketChat/Rocket.Chat/pull/25126)) +- Chore: Convert AdminSideBar to ts ([#25372](https://github.com/RocketChat/Rocket.Chat/pull/25372)) -- Chore: Minor dependency updates ([#25269](https://github.com/RocketChat/Rocket.Chat/pull/25269)) +- Chore: Convert apps/meteor/client/components/UserAutoComplete ([#25554](https://github.com/RocketChat/Rocket.Chat/pull/25554)) -- Chore: Missing keys in APIsDisplay ([#24464](https://github.com/RocketChat/Rocket.Chat/pull/24464)) +- Chore: Convert apps/meteor/client/views/admin/settings ([#25565](https://github.com/RocketChat/Rocket.Chat/pull/25565)) -- Chore: Monorepo ([#25074](https://github.com/RocketChat/Rocket.Chat/pull/25074)) +- Chore: Convert apps/meteor/client/views/admin/settings/inputs folder ([#25427](https://github.com/RocketChat/Rocket.Chat/pull/25427)) -- Chore: move definitions to packages ([#25085](https://github.com/RocketChat/Rocket.Chat/pull/25085)) +- Chore: Convert AutoTranslate ([#25591](https://github.com/RocketChat/Rocket.Chat/pull/25591)) -- Chore: organize test files and fix code coverage ([#24900](https://github.com/RocketChat/Rocket.Chat/pull/24900)) +- Chore: Convert client/views/admin/settings/groups folder to ts ([#25345](https://github.com/RocketChat/Rocket.Chat/pull/25345)) -- Chore: Remove Alpine image deps after using them ([#25053](https://github.com/RocketChat/Rocket.Chat/pull/25053)) +- Chore: Convert Create Channel ([#25589](https://github.com/RocketChat/Rocket.Chat/pull/25589)) -- Chore: Remove duplicated useUserRoom ([#25180](https://github.com/RocketChat/Rocket.Chat/pull/25180)) +- Chore: Convert customSounds folder to ts ([#25274](https://github.com/RocketChat/Rocket.Chat/pull/25274)) -- Chore: Remove old files from removed Omnichannel feature ([#25129](https://github.com/RocketChat/Rocket.Chat/pull/25129)) +- Chore: Convert customUserStatus folder to ts ([#25288](https://github.com/RocketChat/Rocket.Chat/pull/25288)) -- Chore: Remove package-lock.json from houston files ([#25280](https://github.com/RocketChat/Rocket.Chat/pull/25280)) +- Chore: Convert email inbox feature to TypeScript ([#25298](https://github.com/RocketChat/Rocket.Chat/pull/25298) by [@ujorgeleite](https://github.com/ujorgeleite)) - Houston config in the `package.json` file still mentioned `package-lock.json`, but it doesn't exist anymore +- Chore: Convert federationDashboard folder to ts ([#25343](https://github.com/RocketChat/Rocket.Chat/pull/25343)) -- Chore: Remove unused Drone CI files ([#25124](https://github.com/RocketChat/Rocket.Chat/pull/25124)) +- Chore: Convert getStatistics ([#25342](https://github.com/RocketChat/Rocket.Chat/pull/25342)) -- Chore: Sync with master ([#25284](https://github.com/RocketChat/Rocket.Chat/pull/25284)) +- Chore: convert info to typescript ([#25420](https://github.com/RocketChat/Rocket.Chat/pull/25420)) -- Chore: Template to generate packages ([#25174](https://github.com/RocketChat/Rocket.Chat/pull/25174)) +- Chore: Convert LivechatAgentActivity to raw model and TS ([#25123](https://github.com/RocketChat/Rocket.Chat/pull/25123)) - ``` - npx hygen package new test - ``` +- Chore: Convert Mailer to TS ([#25121](https://github.com/RocketChat/Rocket.Chat/pull/25121)) -- Chore: Tests with Playwright (task: All works) ([#25122](https://github.com/RocketChat/Rocket.Chat/pull/25122)) +- Chore: convert marketplace price display component to use typescript ([#25504](https://github.com/RocketChat/Rocket.Chat/pull/25504)) -- Chore: Tests with Playwright (task: ROC-28, 09-channels) ([#25196](https://github.com/RocketChat/Rocket.Chat/pull/25196)) + **Marketplace apps listing page** + ![Screen Shot 2022-05-13 at 12 57 43](https://user-images.githubusercontent.com/4161171/168322189-67990fdf-a447-46dc-8f88-08b16c2a5416.png) + + **Apps detail page** + ![Screen Shot 2022-05-13 at 12 58 56](https://user-images.githubusercontent.com/4161171/168322241-505ee5bb-d3d8-4b0e-8757-873a1a65a6a6.png) -- Chore: TS conversion folder client ([#25031](https://github.com/RocketChat/Rocket.Chat/pull/25031)) +- Chore: Convert NotificationStatus to TS ([#25125](https://github.com/RocketChat/Rocket.Chat/pull/25125)) -- Chore: TS migration SortList ([#25167](https://github.com/RocketChat/Rocket.Chat/pull/25167)) +- Chore: Convert push endpoints to TS ([#25347](https://github.com/RocketChat/Rocket.Chat/pull/25347)) -- Chore: Update Livechat to the last version ([#25257](https://github.com/RocketChat/Rocket.Chat/pull/25257)) +- Chore: Convert RoomForeword, TextCopy and RoomAvatarEditor to TS ([#25424](https://github.com/RocketChat/Rocket.Chat/pull/25424)) -- Chore: Update Livechat version ([#25130](https://github.com/RocketChat/Rocket.Chat/pull/25130)) +- Chore: Convert slashCommands to typescript ([#25592](https://github.com/RocketChat/Rocket.Chat/pull/25592) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) -- Chore: update OTR icon ([#24521](https://github.com/RocketChat/Rocket.Chat/pull/24521) by [@kibonusp](https://github.com/kibonusp)) +- Chore: Convert to typescript some functions from app/lib/server/functions ([#24519](https://github.com/RocketChat/Rocket.Chat/pull/24519)) - I changed the shredder icon in OTR contextual bar to the stopwatch icon, recently added to the fuselage. + Convert to typescript some functions from app/lib/server/functions and transfered theses files to server/lib -- i18n: Language update from LingoHub 🤖 on 2022-04-04Z ([#25043](https://github.com/RocketChat/Rocket.Chat/pull/25043)) +- Chore: Convert to typescript the slash commands help files ([#24307](https://github.com/RocketChat/Rocket.Chat/pull/24307) by [@eduardofcabrera](https://github.com/eduardofcabrera)) -- Merge master into develop & Set version to 4.7.0-develop ([#25028](https://github.com/RocketChat/Rocket.Chat/pull/25028)) + Convert to typescript the slash commands help files -- Regression: Add `isPending` status to message ([#25299](https://github.com/RocketChat/Rocket.Chat/pull/25299)) +- Chore: Convert useFileInput to TS ([#25426](https://github.com/RocketChat/Rocket.Chat/pull/25426)) -- Regression: Add eslint package to micro services Dockerfile ([#25311](https://github.com/RocketChat/Rocket.Chat/pull/25311)) +- Chore: Convert useUpdateAvatar to TS and type avatar endpoints ([#25430](https://github.com/RocketChat/Rocket.Chat/pull/25430)) -- Regression: Add select message to system message and thread preview and allow select on legacy template ([#25251](https://github.com/RocketChat/Rocket.Chat/pull/25251)) +- Chore: Converting orchestrator.js to ts ([#25367](https://github.com/RocketChat/Rocket.Chat/pull/25367)) -- Regression: Avatar not loading on first direct message ([#25211](https://github.com/RocketChat/Rocket.Chat/pull/25211)) +- Chore: Create README.md for Rest Typings ([#25335](https://github.com/RocketChat/Rocket.Chat/pull/25335)) - fix avatar not loading on a first direct message +- Chore: Dedicated package for UI contexts ([#25432](https://github.com/RocketChat/Rocket.Chat/pull/25432)) -- Regression: Better MongoDB connection management for micro services ([#25323](https://github.com/RocketChat/Rocket.Chat/pull/25323)) + Moving our React contexts to a different package on the monorepo enable us to deliver components from another packages, because they work as a loose connection to the core APIs. -- Regression: bump onboarding-ui version ([#25320](https://github.com/RocketChat/Rocket.Chat/pull/25320)) +- Chore: Dependencies upgrade ([#25290](https://github.com/RocketChat/Rocket.Chat/pull/25290)) - - Bump to 'next' the onboarding-ui package from fuselage. - - Update from 'companyEmail' to 'email' adminData usage types +- Chore: Enable marketplace screenshots endpoint ([#25395](https://github.com/RocketChat/Rocket.Chat/pull/25395)) -- Regression: Change preference to be default legacy messages ([#25255](https://github.com/RocketChat/Rocket.Chat/pull/25255)) +- Chore: ensure scripts use cross-env and ignore some dirs (ROC-54) ([#25218](https://github.com/RocketChat/Rocket.Chat/pull/25218)) -- Regression: CI playwright ([#25168](https://github.com/RocketChat/Rocket.Chat/pull/25168)) + - data and test-failure should be ignored + - ensure scripts use cross-env -- Regression: eslint not running on packages ([#25305](https://github.com/RocketChat/Rocket.Chat/pull/25305)) +- Chore: Fix return type warnings ([#25275](https://github.com/RocketChat/Rocket.Chat/pull/25275)) -- Regression: Fix CI monorepo build ([#25107](https://github.com/RocketChat/Rocket.Chat/pull/25107)) +- Chore: Increase performance and security of integrations’ scripts ([#25641](https://github.com/RocketChat/Rocket.Chat/pull/25641)) -- Regression: Fix clicking on visitor's chat in the sidebar does not display the chat window ([#25380](https://github.com/RocketChat/Rocket.Chat/pull/25380)) + Replace internal VM implementation with VM2 which implements many more mechanisms to ensure timeout, security and allow easier configuration for future improvements on the integrations' feature. - Fix: livechat room not opening. +- Chore: Livechat change output level ([#25522](https://github.com/RocketChat/Rocket.Chat/pull/25522)) -- Regression: Fix English i18n react text ([#25368](https://github.com/RocketChat/Rocket.Chat/pull/25368)) +- Chore: Manager Page Rewrite ([#25431](https://github.com/RocketChat/Rocket.Chat/pull/25431)) - Incorrect text in reaction tooltip has been fixed +- Chore: Migrate 15-message-popup from cypress to playwright ([#25462](https://github.com/RocketChat/Rocket.Chat/pull/25462)) -- Regression: Fix federation Matrix bridge startup ([#25273](https://github.com/RocketChat/Rocket.Chat/pull/25273)) +- Chore: migrate from cypress to pw 14-setting-permission ([#25523](https://github.com/RocketChat/Rocket.Chat/pull/25523)) -- Regression: Fix micro services Docker build ([#25193](https://github.com/RocketChat/Rocket.Chat/pull/25193)) +- Chore: Migrate NotFoundPage to TS ([#25509](https://github.com/RocketChat/Rocket.Chat/pull/25509)) -- Regression: Fix multi line is not showing an empty line between lines ([#25317](https://github.com/RocketChat/Rocket.Chat/pull/25317)) +- Chore: Migrate oauth2server to typescript ([#25126](https://github.com/RocketChat/Rocket.Chat/pull/25126)) -- Regression: Fix reply button not working when hideFlexTab is enabled ([#25306](https://github.com/RocketChat/Rocket.Chat/pull/25306)) +- Chore: Migrate retention-policy to ts ([#25582](https://github.com/RocketChat/Rocket.Chat/pull/25582)) -- Regression: Fix services Docker build on CI ([#25181](https://github.com/RocketChat/Rocket.Chat/pull/25181)) +- Chore: Migrate spotify to ts ([#25507](https://github.com/RocketChat/Rocket.Chat/pull/25507)) -- Regression: Fix size of custom emoji and render emoji on thread message preview ([#25314](https://github.com/RocketChat/Rocket.Chat/pull/25314)) +- Chore: migrate-to-pw-adjust-in-intermitences ([#25542](https://github.com/RocketChat/Rocket.Chat/pull/25542)) + +- Chore: Minor dependency updates ([#25269](https://github.com/RocketChat/Rocket.Chat/pull/25269)) + +- Chore: Missing keys in APIsDisplay ([#24464](https://github.com/RocketChat/Rocket.Chat/pull/24464)) + +- Chore: Monorepo ([#25074](https://github.com/RocketChat/Rocket.Chat/pull/25074)) + +- Chore: Move admin sidebarItems registration to the main file ([#25442](https://github.com/RocketChat/Rocket.Chat/pull/25442)) + +- Chore: Move ddp-streamer micro service to its own sub-repo ([#25246](https://github.com/RocketChat/Rocket.Chat/pull/25246)) + +- Chore: move definitions to packages ([#25085](https://github.com/RocketChat/Rocket.Chat/pull/25085)) + +- Chore: Move markdown message parser to a `callback` ([#25413](https://github.com/RocketChat/Rocket.Chat/pull/25413)) + +- Chore: organize test files and fix code coverage ([#24900](https://github.com/RocketChat/Rocket.Chat/pull/24900)) + +- Chore: Remove Alpine image deps after using them ([#25053](https://github.com/RocketChat/Rocket.Chat/pull/25053)) + +- Chore: Remove duplicated useUserRoom ([#25180](https://github.com/RocketChat/Rocket.Chat/pull/25180)) + +- Chore: Remove old files from removed Omnichannel feature ([#25129](https://github.com/RocketChat/Rocket.Chat/pull/25129)) + +- Chore: Remove package-lock.json from houston files ([#25280](https://github.com/RocketChat/Rocket.Chat/pull/25280)) + + Houston config in the `package.json` file still mentioned `package-lock.json`, but it doesn't exist anymore + +- Chore: Remove unused Drone CI files ([#25124](https://github.com/RocketChat/Rocket.Chat/pull/25124)) + +- Chore: Reorder unreleased migrations ([#25508](https://github.com/RocketChat/Rocket.Chat/pull/25508)) + +- Chore: Rest API query parameters handling ([#25648](https://github.com/RocketChat/Rocket.Chat/pull/25648)) + +- Chore: REST query and body params validation ([#25446](https://github.com/RocketChat/Rocket.Chat/pull/25446)) + +- Chore: Rewrite 2fa to typescript ([#25285](https://github.com/RocketChat/Rocket.Chat/pull/25285)) + +- Chore: Rewrite action-links to ts ([#25418](https://github.com/RocketChat/Rocket.Chat/pull/25418)) + +- Chore: Rewrite autotranslate to ts ([#25425](https://github.com/RocketChat/Rocket.Chat/pull/25425)) + +- Chore: Rewrite im and dm endpoints to ts ([#25521](https://github.com/RocketChat/Rocket.Chat/pull/25521)) + + - Endpoints rewritten to TS + - dm.create + - dm.delete + - dm.close + - dm.counters + - dm.files + - dm.history + - dm.members + - dm.messages + - dm.messages.others + - dm.list + - dm.list.everyone + - dm.open + - dm.setTopic + - im.create + - im.delete + - im.close + - im.counters + - im.files + - im.history + - im.members + - im.messages + - im.messages.others + - im.list + - im.list.everyone + - im.open + - im.setTopic + - Some lines of code was refactored on `apps/meteor/app/api/server/v1/im.ts` + - Unnecessary functions were deleted on `apps/meteor/app/lib/server/functions/getDirectMessageByNameOrIdWithOptionToJoin.ts` + - New types was added on `apps/meteor/app/api/server/api.d.ts` + +- Chore: Rewrite Jitsi Contextualbar to TS ([#25303](https://github.com/RocketChat/Rocket.Chat/pull/25303)) + +- Chore: Rewrite mail-messages to ts ([#25421](https://github.com/RocketChat/Rocket.Chat/pull/25421)) + +- Chore: Rewrite some Omnichannel files to TypeScript ([#25359](https://github.com/RocketChat/Rocket.Chat/pull/25359)) + + apps/meteor/client/components/Omnichannel/modals/* + apps/meteor/client/components/Omnichannel/Tags.js + +- Chore: solve yarn issues from env var ([#25468](https://github.com/RocketChat/Rocket.Chat/pull/25468)) + +- Chore: Sync with master ([#25284](https://github.com/RocketChat/Rocket.Chat/pull/25284)) + +- Chore: Template to generate packages ([#25174](https://github.com/RocketChat/Rocket.Chat/pull/25174)) + + ``` + npx hygen package new test + ``` + +- Chore: Tests with Playwright (task: All works) ([#25122](https://github.com/RocketChat/Rocket.Chat/pull/25122)) + +- Chore: Tests with Playwright (task: ROC-25, 06-message) ([#25252](https://github.com/RocketChat/Rocket.Chat/pull/25252)) + +- Chore: Tests with Playwright (task: ROC-28, 09-channels) ([#25196](https://github.com/RocketChat/Rocket.Chat/pull/25196)) + +- Chore: Tests with Playwright (task: ROC-31, 12-settings) ([#25253](https://github.com/RocketChat/Rocket.Chat/pull/25253)) + +- Chore: Tests with Playwright (task: ROC-66, Intermittent resolution in tests) ([#25416](https://github.com/RocketChat/Rocket.Chat/pull/25416)) + +- Chore: TS conversion folder client ([#25031](https://github.com/RocketChat/Rocket.Chat/pull/25031)) + +- Chore: TS migration SortList ([#25167](https://github.com/RocketChat/Rocket.Chat/pull/25167)) + +- Chore: Update Apps-Engine and Fuselage ([#25700](https://github.com/RocketChat/Rocket.Chat/pull/25700)) + +- Chore: Update Apps-Engine version ([#25617](https://github.com/RocketChat/Rocket.Chat/pull/25617)) + +- Chore: Update Livechat to the last version ([#25257](https://github.com/RocketChat/Rocket.Chat/pull/25257)) + +- Chore: Update Livechat version ([#25130](https://github.com/RocketChat/Rocket.Chat/pull/25130)) + +- Chore: update OTR icon ([#24521](https://github.com/RocketChat/Rocket.Chat/pull/24521) by [@kibonusp](https://github.com/kibonusp)) + + I changed the shredder icon in OTR contextual bar to the stopwatch icon, recently added to the fuselage. + +- Chore: Update Volta configuration ([#25394](https://github.com/RocketChat/Rocket.Chat/pull/25394)) + + [Volta](https://volta.sh/) need some extra configuration to work on monorepos. + +- Chore: User set UTC offset ([#25381](https://github.com/RocketChat/Rocket.Chat/pull/25381)) + +- i18n: Language update from LingoHub 🤖 on 2022-04-04Z ([#25043](https://github.com/RocketChat/Rocket.Chat/pull/25043)) + +- Merge master into develop & Set version to 4.7.0-develop ([#25028](https://github.com/RocketChat/Rocket.Chat/pull/25028)) + +- Regression: Add `isPending` status to message ([#25299](https://github.com/RocketChat/Rocket.Chat/pull/25299)) + +- Regression: Add eslint package to micro services Dockerfile ([#25311](https://github.com/RocketChat/Rocket.Chat/pull/25311)) + +- Regression: Add select message to system message and thread preview and allow select on legacy template ([#25251](https://github.com/RocketChat/Rocket.Chat/pull/25251)) + +- Regression: App event listeners broke Slackbridge integration and importers ([#25689](https://github.com/RocketChat/Rocket.Chat/pull/25689)) + + Some event listeners triggered by Apps were calling `Meteor.user()` in functions that could run outside of Meteor environment + +- Regression: Assets & Slack Bridge Setting Page not rendering ([#25629](https://github.com/RocketChat/Rocket.Chat/pull/25629)) + +- Regression: Avatar not loading on first direct message ([#25211](https://github.com/RocketChat/Rocket.Chat/pull/25211)) + + fix avatar not loading on a first direct message + +- Regression: Better MongoDB connection management for micro services ([#25323](https://github.com/RocketChat/Rocket.Chat/pull/25323)) + +- Regression: Broken components on Federation and Engagement dashboards ([#25653](https://github.com/RocketChat/Rocket.Chat/pull/25653)) + + For reasons I've no clue, any client import path matching `**/data/**` will not be included in the final bundle, failing silently on transpiling/bundling. + +- Regression: bump onboarding-ui version ([#25320](https://github.com/RocketChat/Rocket.Chat/pull/25320)) + + - Bump to 'next' the onboarding-ui package from fuselage. + - Update from 'companyEmail' to 'email' adminData usage types + +- Regression: Change logic to check if connection is online on unstable networks ([#25618](https://github.com/RocketChat/Rocket.Chat/pull/25618)) + +- Regression: Change preference to be default legacy messages ([#25255](https://github.com/RocketChat/Rocket.Chat/pull/25255)) + +- Regression: CI playwright ([#25168](https://github.com/RocketChat/Rocket.Chat/pull/25168)) + +- Regression: CI services build ([#25555](https://github.com/RocketChat/Rocket.Chat/pull/25555)) + +- Regression: Endpoint types with Ajv Coercing data types ([#25644](https://github.com/RocketChat/Rocket.Chat/pull/25644)) + + Ajv Coercing data types should be `true` to accept all kinds of data requested. + +- Regression: eslint not running on packages ([#25305](https://github.com/RocketChat/Rocket.Chat/pull/25305)) + +- Regression: Fix CI monorepo build ([#25107](https://github.com/RocketChat/Rocket.Chat/pull/25107)) + +- Regression: Fix clicking on visitor's chat in the sidebar does not display the chat window ([#25380](https://github.com/RocketChat/Rocket.Chat/pull/25380)) + + Fix: livechat room not opening. + +- Regression: Fix English i18n react text ([#25368](https://github.com/RocketChat/Rocket.Chat/pull/25368)) + + Incorrect text in reaction tooltip has been fixed + +- Regression: Fix federation Matrix bridge startup ([#25273](https://github.com/RocketChat/Rocket.Chat/pull/25273)) + +- Regression: Fix micro services Docker build ([#25193](https://github.com/RocketChat/Rocket.Chat/pull/25193)) + +- Regression: Fix multi line is not showing an empty line between lines ([#25317](https://github.com/RocketChat/Rocket.Chat/pull/25317)) + +- Regression: Fix reply button not working when hideFlexTab is enabled ([#25306](https://github.com/RocketChat/Rocket.Chat/pull/25306)) + +- Regression: Fix services Docker build on CI ([#25181](https://github.com/RocketChat/Rocket.Chat/pull/25181)) + +- Regression: Fix services-image-build-check ([#25519](https://github.com/RocketChat/Rocket.Chat/pull/25519)) + +- Regression: Fix size of custom emoji and render emoji on thread message preview ([#25314](https://github.com/RocketChat/Rocket.Chat/pull/25314)) + +- Regression: Fix sort field files.list ([#25687](https://github.com/RocketChat/Rocket.Chat/pull/25687)) - Regression: Fix the alpine image and dev UX installing matrix-rust-sdk-bindings ([#25319](https://github.com/RocketChat/Rocket.Chat/pull/25319)) @@ -1456,6 +2796,10 @@ - Regression: Messages in new message template Crashing. ([#25327](https://github.com/RocketChat/Rocket.Chat/pull/25327)) +- Regression: Missing settings group descriptions ([#25639](https://github.com/RocketChat/Rocket.Chat/pull/25639)) + + + - Regression: Revert Bugsnag version ([#25313](https://github.com/RocketChat/Rocket.Chat/pull/25313)) - Regression: Rocket.Chat Webapp not loading. ([#25349](https://github.com/RocketChat/Rocket.Chat/pull/25349)) @@ -1464,12 +2808,32 @@ - Regression: Shows error if micro service cannot connect to Mongo ([#25301](https://github.com/RocketChat/Rocket.Chat/pull/25301)) +- Regression: Subscription menu not appearing for non installed but subscribed apps ([#25627](https://github.com/RocketChat/Rocket.Chat/pull/25627)) + + Fixed a problem on which the AppMenu component did not appear for apps that had an active subscription but weren't installed, now the rendering of the component is also based on the isSubscribed flag, and the appearance of the uninstall and enable/disable options are based on the app.installed flag so that the correct options appear on all the edge cases. + Demo gif: + ![subscription-manager-fix](https://user-images.githubusercontent.com/43561537/170132040-dc8535c0-8056-4fb2-b008-afaece744868.gif) + +- Regression: Update settings groups description ([#25663](https://github.com/RocketChat/Rocket.Chat/pull/25663)) + - Regression: Use exact Node version on micro services Docker images ([#25287](https://github.com/RocketChat/Rocket.Chat/pull/25287)) - Regression: Validate empty fields for Message template ([#25250](https://github.com/RocketChat/Rocket.Chat/pull/25250)) +- Regression: VoIp wrap up modal not opening after call disconnect ([#25651](https://github.com/RocketChat/Rocket.Chat/pull/25651)) + + This PR fixes a bug preventing the wrap up call modal from being displayed after caller or agent ends the call. + - Regression: yarn dev triggers build dependencies ([#25208](https://github.com/RocketChat/Rocket.Chat/pull/25208)) +- Release 4.7.0 ([#25390](https://github.com/RocketChat/Rocket.Chat/pull/25390) by [@Himanshu664](https://github.com/Himanshu664) & [@dependabot[bot]](https://github.com/dependabot[bot]) & [@lingohub[bot]](https://github.com/lingohub[bot])) + +- Release 4.7.1 ([#25510](https://github.com/RocketChat/Rocket.Chat/pull/25510) by [@felipe-menelau](https://github.com/felipe-menelau)) + +- Release 4.7.2 ([#25580](https://github.com/RocketChat/Rocket.Chat/pull/25580)) + +- Test: Migrate 13-permissions from cypress to playwright ([#25558](https://github.com/RocketChat/Rocket.Chat/pull/25558)) + ### 👩‍💻👨‍💻 Contributors 😍 @@ -1478,9 +2842,15 @@ - [@aakash-gitdev](https://github.com/aakash-gitdev) - [@cuonghuunguyen](https://github.com/cuonghuunguyen) - [@dependabot[bot]](https://github.com/dependabot[bot]) +- [@divinespear](https://github.com/divinespear) +- [@eduardofcabrera](https://github.com/eduardofcabrera) +- [@felipe-menelau](https://github.com/felipe-menelau) - [@kibonusp](https://github.com/kibonusp) +- [@lingohub[bot]](https://github.com/lingohub[bot]) +- [@ostjen](https://github.com/ostjen) - [@paulobernardoaf](https://github.com/paulobernardoaf) - [@sidmohanty11](https://github.com/sidmohanty11) +- [@ujorgeleite](https://github.com/ujorgeleite) ### 👩‍💻👨‍💻 Core Team 🤓 @@ -1488,544 +2858,660 @@ - [@KevLehman](https://github.com/KevLehman) - [@MarcosSpessatto](https://github.com/MarcosSpessatto) - [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@PedroRorato](https://github.com/PedroRorato) - [@alansikora](https://github.com/alansikora) - [@albuquerquefabio](https://github.com/albuquerquefabio) +- [@aleksandernsilva](https://github.com/aleksandernsilva) - [@amolghode1981](https://github.com/amolghode1981) +- [@carlosrodrigues94](https://github.com/carlosrodrigues94) +- [@cauefcr](https://github.com/cauefcr) - [@d-gubert](https://github.com/d-gubert) - [@debdutdeb](https://github.com/debdutdeb) - [@dougfabris](https://github.com/dougfabris) +- [@felipe-rod123](https://github.com/felipe-rod123) - [@filipemarins](https://github.com/filipemarins) - [@gabriellsh](https://github.com/gabriellsh) - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) - [@guijun13](https://github.com/guijun13) +- [@hugocostadev](https://github.com/hugocostadev) - [@jeanfbrito](https://github.com/jeanfbrito) - [@juliajforesti](https://github.com/juliajforesti) +- [@marceloschmidt](https://github.com/marceloschmidt) +- [@matheusbsilva137](https://github.com/matheusbsilva137) +- [@matheuslc](https://github.com/matheuslc) - [@murtaza98](https://github.com/murtaza98) - [@nishant23122000](https://github.com/nishant23122000) - [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@rique223](https://github.com/rique223) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) - [@souzaramon](https://github.com/souzaramon) +- [@tapiarafael](https://github.com/tapiarafael) - [@tassoevan](https://github.com/tassoevan) - [@tiagoevanp](https://github.com/tiagoevanp) - [@tmontini](https://github.com/tmontini) - [@weslley543](https://github.com/weslley543) - [@yash-rajpal](https://github.com/yash-rajpal) -# 4.6.3 -`2022-04-19 · 1 🐛 · 1 👩‍💻👨‍💻` +# 4.7.4 +`2022-05-30 · 1 🐛 · 1 🔍 · 2 👩‍💻👨‍💻` ### Engine versions - Node: `14.18.3` - NPM: `6.14.15` - MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` -- Apps-Engine: `1.31.0` ### 🐛 Bug fixes -- Desktop notification on multi-instance environments ([#25220](https://github.com/RocketChat/Rocket.Chat/pull/25220)) +- Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) -### 👩‍💻👨‍💻 Core Team 🤓 +
+🔍 Minor changes -- [@sampaiodiego](https://github.com/sampaiodiego) -# 4.6.2 -`2022-04-14 · 2 🐛 · 2 👩‍💻👨‍💻` +- Load missed messages from opened rooms when reconnect ([#553](https://github.com/RocketChat/Rocket.Chat/pull/553)) -### Engine versions -- Node: `14.18.3` -- NPM: `6.14.15` -- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` -- Apps-Engine: `1.31.0` +
-### 🐛 Bug fixes +### 👩‍💻👨‍💻 Core Team 🤓 +- [@ggazzo](https://github.com/ggazzo) +- [@rodrigok](https://github.com/rodrigok) -- Database indexes not being created ([#25101](https://github.com/RocketChat/Rocket.Chat/pull/25101)) +# 4.7.3 +`2022-05-20 · 1 🐛 · 1 👩‍💻👨‍💻` -- Deactivating user breaks if user is the only room owner ([#24933](https://github.com/RocketChat/Rocket.Chat/pull/24933) by [@sidmohanty11](https://github.com/sidmohanty11)) +### Engine versions +- Node: `14.18.3` +- NPM: `6.14.15` +- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` - ## Before - - https://user-images.githubusercontent.com/73601258/160000871-cfc2f2a5-2a59-4d27-8049-7754d003dd48.mp4 - - - - ## After - https://user-images.githubusercontent.com/73601258/159998287-681ab475-ff33-4282-82ff-db751c59a392.mp4 +### 🐛 Bug fixes -### 👩‍💻👨‍💻 Contributors 😍 -- [@sidmohanty11](https://github.com/sidmohanty11) +- Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) ### 👩‍💻👨‍💻 Core Team 🤓 -- [@sampaiodiego](https://github.com/sampaiodiego) +- [@ggazzo](https://github.com/ggazzo) -# 4.6.1 -`2022-04-07 · 6 🐛 · 5 👩‍💻👨‍💻` +# 4.7.2 +`2022-05-20 · 5 🐛 · 2 🔍 · 7 👩‍💻👨‍💻` ### Engine versions - Node: `14.18.3` - NPM: `6.14.15` - MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` -- Apps-Engine: `1.31.0` ### 🐛 Bug fixes -- FormData uploads not working ([#25069](https://github.com/RocketChat/Rocket.Chat/pull/25069)) +- Dynamic load matrix is enabled and handle failure ([#25495](https://github.com/RocketChat/Rocket.Chat/pull/25495)) -- Invitation links don't redirect to the registration form ([#25082](https://github.com/RocketChat/Rocket.Chat/pull/25082)) +- Initial User not added to default channel ([#25544](https://github.com/RocketChat/Rocket.Chat/pull/25544)) -- NPS never finishing sending results ([#25067](https://github.com/RocketChat/Rocket.Chat/pull/25067)) + If injecting initial user. The user wasn’t added to the default General channel -- Proxy settings being ignored ([#25022](https://github.com/RocketChat/Rocket.Chat/pull/25022)) +- One of the triggers was not working correctly ([#25409](https://github.com/RocketChat/Rocket.Chat/pull/25409)) - Modify Meteor's `HTTP.call` to add back proxy support +- UI/UX issues on Live Chat widget ([#25407](https://github.com/RocketChat/Rocket.Chat/pull/25407)) -- Upgrade Tab showing for a split second ([#25050](https://github.com/RocketChat/Rocket.Chat/pull/25050)) +- User abandonment setting was not working doe to failing event hook ([#25520](https://github.com/RocketChat/Rocket.Chat/pull/25520)) -- UserAutoComplete not rendering UserAvatar correctly ([#25055](https://github.com/RocketChat/Rocket.Chat/pull/25055)) + A setting watcher and the query for grabbing abandoned chats were broken, now they're not. - ### before - ![Screen Shot 2022-04-04 at 16 50 21](https://user-images.githubusercontent.com/27704687/161620921-800bf66a-806d-4f83-b2e1-073c34215001.png) - - ### after - ![Screen Shot 2022-04-04 at 16 49 00](https://user-images.githubusercontent.com/27704687/161620720-3e27774d-c241-46ca-b764-932a9295d709.png) +
+🔍 Minor changes + + +- Chore: Add Livechat repo into Monorepo packages ([#25312](https://github.com/RocketChat/Rocket.Chat/pull/25312)) + +- Release 4.7.2 ([#25580](https://github.com/RocketChat/Rocket.Chat/pull/25580)) + +
### 👩‍💻👨‍💻 Core Team 🤓 +- [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@cauefcr](https://github.com/cauefcr) +- [@d-gubert](https://github.com/d-gubert) - [@dougfabris](https://github.com/dougfabris) -- [@gabriellsh](https://github.com/gabriellsh) -- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) -- [@sampaiodiego](https://github.com/sampaiodiego) -- [@yash-rajpal](https://github.com/yash-rajpal) +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@ggazzo](https://github.com/ggazzo) +- [@tiagoevanp](https://github.com/tiagoevanp) -# 4.6.0 -`2022-04-01 · 2 🎉 · 7 🚀 · 57 🐛 · 62 🔍 · 34 👩‍💻👨‍💻` +# 4.7.1 +`2022-05-13 · 1 🎉 · 2 🐛 · 1 🔍 · 4 👩‍💻👨‍💻` ### Engine versions - Node: `14.18.3` - NPM: `6.14.15` - MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` -- Apps-Engine: `1.31.0` ### 🎉 New features -- Telemetry Events ([#24781](https://github.com/RocketChat/Rocket.Chat/pull/24781) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) - -- Upgrade Tab ([#24835](https://github.com/RocketChat/Rocket.Chat/pull/24835)) +- Use setting to determine if initial general channel is needed ([#25441](https://github.com/RocketChat/Rocket.Chat/pull/25441) by [@felipe-menelau](https://github.com/felipe-menelau)) - ![image](https://user-images.githubusercontent.com/27704687/160172260-c656282e-a487-4092-948d-d11c9bacb598.png) + - Adds flag responsible for overwriting #general channel creation -### 🚀 Improvements +### 🐛 Bug fixes -- **ENTERPRISE:** Don't start presence monitor when running micro services ([#24739](https://github.com/RocketChat/Rocket.Chat/pull/24739)) +- LDAP sync removing users from channels when multiple groups are mapped to it ([#25434](https://github.com/RocketChat/Rocket.Chat/pull/25434)) -- Adding new statistics related to voip and omnichannel ([#24887](https://github.com/RocketChat/Rocket.Chat/pull/24887)) +- Spotlight results showing usernames instead of real names ([#25471](https://github.com/RocketChat/Rocket.Chat/pull/25471)) - - Total of Canned response messages sent - - Total of tags used - - Last-Chatted Agent Preferred (enabled/disabled) - - Assign new conversations to the contact manager (enabled/disabled) - - How to handle Visitor Abandonment setting - - Amount of chats placed on hold - - VoIP Enabled - - Amount of VoIP Calls - - Amount of VoIP Extensions connected - - Amount of Calls placed on hold (1x per call) - - Fixed Session Aggregation type definitions +
+🔍 Minor changes -- New omnichannel statistics and async statistics processing. ([#24749](https://github.com/RocketChat/Rocket.Chat/pull/24749)) - https://app.clickup.com/t/1z4zg4e +- Release 4.7.1 ([#25510](https://github.com/RocketChat/Rocket.Chat/pull/25510) by [@felipe-menelau](https://github.com/felipe-menelau)) -- Standarize queue behavior for managers and agents when subscribing ([#24837](https://github.com/RocketChat/Rocket.Chat/pull/24837)) +
-- Updated links in readme ([#24028](https://github.com/RocketChat/Rocket.Chat/pull/24028) by [@aswinidev](https://github.com/aswinidev)) +### 👩‍💻👨‍💻 Contributors 😍 -- UX - VoIP Call Component ([#24748](https://github.com/RocketChat/Rocket.Chat/pull/24748)) +- [@felipe-menelau](https://github.com/felipe-menelau) -- Voip Extensions disabled state ([#24750](https://github.com/RocketChat/Rocket.Chat/pull/24750)) +### 👩‍💻👨‍💻 Core Team 🤓 -### 🐛 Bug fixes +- [@d-gubert](https://github.com/d-gubert) +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@sampaiodiego](https://github.com/sampaiodiego) +# 4.7.0 +`2022-05-04 · 4 🎉 · 7 🚀 · 33 🐛 · 69 🔍 · 35 👩‍💻👨‍💻` -- "livechat/webrtc.call" endpoint not working ([#24804](https://github.com/RocketChat/Rocket.Chat/pull/24804)) +### Engine versions +- Node: `14.18.3` +- NPM: `6.14.15` +- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` -- "Match error" when converting a team to a channel ([#24629](https://github.com/RocketChat/Rocket.Chat/pull/24629)) +### 🎉 New features - - Fix "Match error" when trying to convert a channel to a team; -- **ENTERPRISE:** Auto reload feature of ddp-streamer micro service ([#24793](https://github.com/RocketChat/Rocket.Chat/pull/24793)) +- Add expire index to integration history ([#25087](https://github.com/RocketChat/Rocket.Chat/pull/25087)) -- **ENTERPRISE:** DDP streamer not sending data to all clients ([#24738](https://github.com/RocketChat/Rocket.Chat/pull/24738)) +- Alpha Matrix Federation ([#23688](https://github.com/RocketChat/Rocket.Chat/pull/23688)) -- **ENTERPRISE:** Notifications not being sent by ddp-streamer ([#24831](https://github.com/RocketChat/Rocket.Chat/pull/24831)) + Experimental support for Matrix Federation with a Bridge + + https://user-images.githubusercontent.com/51996/164530391-e8b17ecd-a4d0-4ef8-a8b7-81230c1773d3.mp4 -- **ENTERPRISE:** Presence micro service logic ([#24724](https://github.com/RocketChat/Rocket.Chat/pull/24724)) +- Expand Apps Engine's environment variable allowed list ([#23870](https://github.com/RocketChat/Rocket.Chat/pull/23870) by [@cuonghuunguyen](https://github.com/cuonghuunguyen)) -- **VOIP:** SidebarFooter component ([#24838](https://github.com/RocketChat/Rocket.Chat/pull/24838)) +- Message Template React Component ([#23971](https://github.com/RocketChat/Rocket.Chat/pull/23971)) - - Improve the CallProvider code; - - Adjust the text case of the VoIP component on the FooterSidebar; - - Fix the bad behavior with the changes in queue's name. + Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template). + + + + ![Screen Shot 2022-04-05 at 11 14 18](https://user-images.githubusercontent.com/27704687/161774027-38dd9c7b-eeeb-45e2-b9d8-ea2a9be8486d.png) + In case you encounter any problems, or want to compare, temporarily it is possible to use the old version + + image -- `PaginatedSelectFiltered` not handling changes ([#24732](https://github.com/RocketChat/Rocket.Chat/pull/24732)) +### 🚀 Improvements -- API Error preventing adding an email to users without one (like bot/app users) ([#24709](https://github.com/RocketChat/Rocket.Chat/pull/24709)) -- Apple login script being loaded even when Apple Login is disabled. ([#24760](https://github.com/RocketChat/Rocket.Chat/pull/24760)) +- Add OTR Room States ([#24565](https://github.com/RocketChat/Rocket.Chat/pull/24565)) -- Apple OAuth ([#24879](https://github.com/RocketChat/Rocket.Chat/pull/24879)) + Earlier OTR room uses only 2 states, we need more states to support future features. + This adds more states for the OTR contextualBar. + + - Expired + Screen Shot 2022-04-20 at 13 55 52 + + - Declined + Screen Shot 2022-04-20 at 13 49 28 + + - Error + Screen Shot 2022-04-20 at 13 55 26 -- auto-join team channels not honoring user preferences ([#24779](https://github.com/RocketChat/Rocket.Chat/pull/24779) by [@ostjen](https://github.com/ostjen)) +- Add tooltip to sidebar room menu ([#24405](https://github.com/RocketChat/Rocket.Chat/pull/24405) by [@Himanshu664](https://github.com/Himanshu664)) -- Broken build caused by PRs modifying same file differently ([#24863](https://github.com/RocketChat/Rocket.Chat/pull/24863)) +- Added MaxNickNameLength and MaxBioLength constants ([#25231](https://github.com/RocketChat/Rocket.Chat/pull/25231) by [@aakash-gitdev](https://github.com/aakash-gitdev)) -- Broken multiple OAuth integrations ([#24705](https://github.com/RocketChat/Rocket.Chat/pull/24705)) +- Added tooltip options for message menu ([#24431](https://github.com/RocketChat/Rocket.Chat/pull/24431) by [@Himanshu664](https://github.com/Himanshu664)) -- Components for user search ([#24677](https://github.com/RocketChat/Rocket.Chat/pull/24677)) +- Improve active/hover colors in account sidebar ([#25024](https://github.com/RocketChat/Rocket.Chat/pull/25024) by [@Himanshu664](https://github.com/Himanshu664)) -- Critical: Incorrect visitor getting assigned to a chat from apps ([#24805](https://github.com/RocketChat/Rocket.Chat/pull/24805)) +- Performance for some Omnichannel features ([#25217](https://github.com/RocketChat/Rocket.Chat/pull/25217)) -- Custom script not being fired ([#24901](https://github.com/RocketChat/Rocket.Chat/pull/24901)) +- Rename upgrade tab routes ([#25097](https://github.com/RocketChat/Rocket.Chat/pull/25097)) -- Date Message Export Filter Fix ([#24542](https://github.com/RocketChat/Rocket.Chat/pull/24542) by [@eduardofcabrera](https://github.com/eduardofcabrera)) + Change 'upgrade tab' routes names from camelCase ('goFullyFeatured') to kebab-case ('go-fully-featured') due to URL naming consistency. Changed types, main function and test. - Fix message export filter to get all messages between "from date" and "to date", including "to date". +### 🐛 Bug fixes -- DDP Rate Limiter Translation key ([#24898](https://github.com/RocketChat/Rocket.Chat/pull/24898)) - Before: - image - - - Now: - image +- Add katex render to new message react template ([#25239](https://github.com/RocketChat/Rocket.Chat/pull/25239)) -- DDP streamer errors ([#24710](https://github.com/RocketChat/Rocket.Chat/pull/24710)) +- Add reaction not working in legacy messages ([#25222](https://github.com/RocketChat/Rocket.Chat/pull/25222)) -- Disable voip button when call is in progress ([#24864](https://github.com/RocketChat/Rocket.Chat/pull/24864)) +- Added invalid password error message ([#24714](https://github.com/RocketChat/Rocket.Chat/pull/24714) by [@Himanshu664](https://github.com/Himanshu664)) -- Duplicated 'name' log key ([#24590](https://github.com/RocketChat/Rocket.Chat/pull/24590)) +- Adjust email label in Setup Wizard i18n files ([#25260](https://github.com/RocketChat/Rocket.Chat/pull/25260)) -- Duplicated "jump to message" button on starred messages ([#24867](https://github.com/RocketChat/Rocket.Chat/pull/24867) by [@Himanshu664](https://github.com/Himanshu664)) + - remove 'Company' label on onboarding email keys in certain languages -- External search providers not working ([#24860](https://github.com/RocketChat/Rocket.Chat/pull/24860) by [@tkurz](https://github.com/tkurz)) +- AgentOverview analytics wrong departmentId parameter ([#25073](https://github.com/RocketChat/Rocket.Chat/pull/25073) by [@paulobernardoaf](https://github.com/paulobernardoaf)) -- German translation for Monitore ([#24785](https://github.com/RocketChat/Rocket.Chat/pull/24785) by [@JMoVS](https://github.com/JMoVS)) + When filtering the analytics charts by department, data would not appear because the object: + ```js + { + value: "department-id", + label: "department-name" + } + ``` + was being used in the `departmentId` parameter. + + - Before: + ![image](https://user-images.githubusercontent.com/30026625/161832057-d96ffd21-a7dd-421e-bfaa-3b9f4a9127b2.png) + + - After: + ![image](https://user-images.githubusercontent.com/30026625/161831092-9ee77b51-b083-4f45-9c48-ab2e0511c4d6.png) -- Handle Other Formats inside Upload Avatar ([#24226](https://github.com/RocketChat/Rocket.Chat/pull/24226)) +- Client disconnection on network loss ([#25170](https://github.com/RocketChat/Rocket.Chat/pull/25170)) - After resolving issue #24213 : + Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online + unless agent explicitly logs off. + Agent can stop receiving calls in multiple ways due to network loss. Network loss can happen in following ways. + 1. User tries to switch the network. User experiences a glitch of disconnectivity. This can be simulated by turning the network off + in the network tab of chrome's dev tool. This can disconnect the UA if the disconnection happens just before the registration refresh. + 2. Second reason is when computer goes in sleep mode. + 3. Third reason is that when asterisk is crashed/in maintenance mode/explicitly stopped. + Solution: + The idea is to detect the network disconnection and start the start the attempts to reconnect. + The detection of the disconnection does not happen in case#1. The SIPUA's UserAgent transport does not + call onDisconnected when network loss of such kind happens. To tackle this problem, window's online and offline event handlers are + used. - https://user-images.githubusercontent.com/53515714/150325012-91413025-786e-4ce0-ae75-629f6b05b024.mp4 - -- High CPU usage caused by CallProvider ([#24994](https://github.com/RocketChat/Rocket.Chat/pull/24994)) - - Remove infinity loop inside useVoipClient hook. + The number of retries is configurable but ideally it is to be kept at -1. Whenever disconnection happens, it should keep on trying to + reconnect with increasing backoff time. This behaviour is useful when the asterisk is stopped. - #closes #24970 + When the server is disconnected, it should be indicated on the phone button. -- Ignore customClass on messages ([#24845](https://github.com/RocketChat/Rocket.Chat/pull/24845)) +- Close room when dismiss wrap up call modal ([#25056](https://github.com/RocketChat/Rocket.Chat/pull/25056)) -- LDAP avatars being rotated according to metadata even if the setting to rotate uploads is off ([#24320](https://github.com/RocketChat/Rocket.Chat/pull/24320)) +- Custom sound error toast messages ([#24515](https://github.com/RocketChat/Rocket.Chat/pull/24515) by [@Himanshu664](https://github.com/Himanshu664)) - - Use the `FileUpload_RotateImages` setting (**Administration > File Upload > Rotate images on upload**) to control whether avatars should be rotated automatically based on their data (XEIF); - - Display the avatar image preview (orientation) according to the `FileUpload_RotateImages` setting. +- Deactivating user breaks if user is the only room owner ([#24933](https://github.com/RocketChat/Rocket.Chat/pull/24933) by [@sidmohanty11](https://github.com/sidmohanty11)) -- Missing dependency on useEffect at CallProvider ([#24882](https://github.com/RocketChat/Rocket.Chat/pull/24882)) + ## Before + + https://user-images.githubusercontent.com/73601258/160000871-cfc2f2a5-2a59-4d27-8049-7754d003dd48.mp4 + + + + ## After + https://user-images.githubusercontent.com/73601258/159998287-681ab475-ff33-4282-82ff-db751c59a392.mp4 -- Missing username on messages imported from Slack ([#24674](https://github.com/RocketChat/Rocket.Chat/pull/24674)) +- Desktop notification on multi-instance environments ([#25220](https://github.com/RocketChat/Rocket.Chat/pull/25220)) - - Fix missing sender's username on messages imported from Slack. - -- Nextcloud OAuth for incomplete token URL ([#24476](https://github.com/RocketChat/Rocket.Chat/pull/24476)) +- End call button disappearing when on-hold ([#24936](https://github.com/RocketChat/Rocket.Chat/pull/24936)) -- no id of room closer in livechat-close message ([#24683](https://github.com/RocketChat/Rocket.Chat/pull/24683)) +- FormData uploads not working ([#25069](https://github.com/RocketChat/Rocket.Chat/pull/25069)) -- Opening a new DM from user card ([#24623](https://github.com/RocketChat/Rocket.Chat/pull/24623)) +- Full error message is visible ([#24856](https://github.com/RocketChat/Rocket.Chat/pull/24856) by [@Himanshu664](https://github.com/Himanshu664)) - A race condition on `useRoomIcon` -- delayed merge of rooms and subscriptions -- was causing a UI crash whenever someone tried to open a DM from the user card component. +- Incorrect websocket url in livechat widget ([#25261](https://github.com/RocketChat/Rocket.Chat/pull/25261)) -- Prevent call button toggle when user is on call ([#24758](https://github.com/RocketChat/Rocket.Chat/pull/24758)) +- Invitation links don't redirect to the registration form ([#25082](https://github.com/RocketChat/Rocket.Chat/pull/25082)) -- Prune Message issue ([#24424](https://github.com/RocketChat/Rocket.Chat/pull/24424)) +- Message menu action not working on legacy messages. ([#25148](https://github.com/RocketChat/Rocket.Chat/pull/25148)) -- Push privacy config to not show username not being respected ([#24606](https://github.com/RocketChat/Rocket.Chat/pull/24606)) +- Message preview not available for queued chats ([#25092](https://github.com/RocketChat/Rocket.Chat/pull/25092)) -- Register with Secret URL ([#24921](https://github.com/RocketChat/Rocket.Chat/pull/24921)) +- NPS never finishing sending results ([#25067](https://github.com/RocketChat/Rocket.Chat/pull/25067)) -- Reload roomslist after successful deletion of a room from admin panel. ([#23795](https://github.com/RocketChat/Rocket.Chat/pull/23795) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari)) +- Prevent sequential messages edited icon to hide on hover ([#24984](https://github.com/RocketChat/Rocket.Chat/pull/24984)) - Removed the logic for calling the `rooms.adminRooms` endPoint from the `RoomsTable` Component and moved it to its parent component `RoomsPage`. - This allows to call the endPoint `rooms.adminRooms` from `EditRoomContextBar` Component which is also has `RoomPage` Component as its parent. + ### before + Screen Shot 2022-03-29 at 13 35 56 - Also added a succes toast message after the successful deletion of room. - -- Revert AutoComplete ([#24812](https://github.com/RocketChat/Rocket.Chat/pull/24812)) - -- Room archived/unarchived system messages aren't sent when editing room settings ([#24897](https://github.com/RocketChat/Rocket.Chat/pull/24897)) + ### after + Screen Shot 2022-03-29 at 11 48 05 - - Send the "Room archived" and "Room unarchived" system messages when editing room settings (and not only when rooms are archived/unarchived with the slash-command); - - Fix the "Hide System Messages" option for the "Room archived" and "Room unarchived" system messages; +- Proxy settings being ignored ([#25022](https://github.com/RocketChat/Rocket.Chat/pull/25022)) -- room message not load when is a new message ([#24955](https://github.com/RocketChat/Rocket.Chat/pull/24955)) + Modify Meteor's `HTTP.call` to add back proxy support - When the room object is searched for the first time, it does not exist on the front object yet (subscription), adding a fallback search for room list will guarantee to search the room details. - - before: - https://user-images.githubusercontent.com/9275105/160223241-d2319f3e-82c5-47d6-867f-695ab2361a17.mp4 - - after: - https://user-images.githubusercontent.com/9275105/160223244-84d0d2a1-3d95-464d-8b8a-e264b0d4d690.mp4 +- Read receipts show with color gray when not read yet ([#25244](https://github.com/RocketChat/Rocket.Chat/pull/25244)) -- Room's message count not being incremented on import ([#24696](https://github.com/RocketChat/Rocket.Chat/pull/24696)) +- Read receipts showing before message read ([#25216](https://github.com/RocketChat/Rocket.Chat/pull/25216)) - - Fix rooms' message counter not being incremented on message import. +- Replace encrypted text to Encrypted Message Placeholder ([#24166](https://github.com/RocketChat/Rocket.Chat/pull/24166)) -- SAML Force name to string ([#24930](https://github.com/RocketChat/Rocket.Chat/pull/24930)) + ### before + ![image](https://user-images.githubusercontent.com/27704687/150807900-154a9cdb-ee13-4333-8628-f287ab914b40.png) + + ### after + Screenshot 2022-01-13 at 8 57 47 PM -- Several issues related to custom roles ([#24052](https://github.com/RocketChat/Rocket.Chat/pull/24052)) +- Reply button behavior on broadcast channel ([#25175](https://github.com/RocketChat/Rocket.Chat/pull/25175)) - - Throw an error when trying to delete a role (User or Subscription role) that are still being used; - - Fix "Invalid Role" error for custom roles in Role Editing sidebar; - - Fix "Users in Role" screen for custom roles. + Hide reply button for the user that sent the message -- Show call icon only when user has extension associated ([#24752](https://github.com/RocketChat/Rocket.Chat/pull/24752)) +- room creation fails if app framework is disabled ([#25200](https://github.com/RocketChat/Rocket.Chat/pull/25200)) -- Show only available agents on extension association modal ([#24680](https://github.com/RocketChat/Rocket.Chat/pull/24680)) +- Showing Blank Message Inside Report ([#25007](https://github.com/RocketChat/Rocket.Chat/pull/25007)) -- Show only enabled departments on forward ([#24829](https://github.com/RocketChat/Rocket.Chat/pull/24829)) + https://user-images.githubusercontent.com/53515714/161038085-4a86c7ae-6751-4996-9767-b1c9e0331a6c.mp4 -- System messages are sent when adding or removing a group from a team ([#24743](https://github.com/RocketChat/Rocket.Chat/pull/24743)) +- Toolbox hiding under contextual bar ([#25237](https://github.com/RocketChat/Rocket.Chat/pull/25237)) - - Do not send system messages when adding or removing a new or existing _group_ from a team. +- Upgrade Tab showing for a split second ([#25050](https://github.com/RocketChat/Rocket.Chat/pull/25050)) -- Typo and placeholder on wrap up call modal ([#24737](https://github.com/RocketChat/Rocket.Chat/pull/24737)) +- Use correct room property for call ended at ([#24932](https://github.com/RocketChat/Rocket.Chat/pull/24932)) -- Typo in wrap-up term ([#24661](https://github.com/RocketChat/Rocket.Chat/pull/24661)) +- UserAutoComplete not rendering UserAvatar correctly ([#25055](https://github.com/RocketChat/Rocket.Chat/pull/25055)) -- VoIP button gets disabled whenever user status changes ([#24789](https://github.com/RocketChat/Rocket.Chat/pull/24789)) + ### before + ![Screen Shot 2022-04-04 at 16 50 21](https://user-images.githubusercontent.com/27704687/161620921-800bf66a-806d-4f83-b2e1-073c34215001.png) + + ### after + ![Screen Shot 2022-04-04 at 16 49 00](https://user-images.githubusercontent.com/27704687/161620720-3e27774d-c241-46ca-b764-932a9295d709.png) -- VoIP Enable/Disable setting on CallContext/CallProvider Notifications ([#24607](https://github.com/RocketChat/Rocket.Chat/pull/24607)) +- UserCard sanitization ([#25089](https://github.com/RocketChat/Rocket.Chat/pull/25089)) -- Voip Stream Reinitialization Error ([#24657](https://github.com/RocketChat/Rocket.Chat/pull/24657)) + - Rewrites the component to TS + - Fixes some visual issues + + ### before + ![Screen Shot 2022-04-07 at 00 23 11](https://user-images.githubusercontent.com/27704687/162113925-5c9484d1-23e9-4623-8b86-3fbc71b461a1.png) + + ### after + ![Screen Shot 2022-04-07 at 00 07 13](https://user-images.githubusercontent.com/27704687/162112353-afd6aac6-b27c-4470-a642-631b8080d59e.png) -- VoipExtensionsPage component call ([#24792](https://github.com/RocketChat/Rocket.Chat/pull/24792)) +- Video and Audio not skipping forward ([#19866](https://github.com/RocketChat/Rocket.Chat/pull/19866)) -- Wrong business hour behavior ([#24896](https://github.com/RocketChat/Rocket.Chat/pull/24896)) +- VoIP disabled/enabled sequence puts voip agent in error state ([#25230](https://github.com/RocketChat/Rocket.Chat/pull/25230)) -- Wrong param usage on queue summary call ([#24799](https://github.com/RocketChat/Rocket.Chat/pull/24799)) + Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side) + + It was realised that the race does not happen because of the database or server noticing the changes late. But because of the time taken to establish the AMI connection with Asterisk. + + Solution: + + 1. Change apps/meteor/app/voip/server/startup.ts. When VoIP_Enabled is changed, await for Voip.init() to complete and then broadcast connector.statuschanged with changed value. + 2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance. + 3. in apps/meteor/client/providers/CallProvider/hooks/useVoipClient.ts add the event handler that receives this event. Change voipEnabled from constant to state. Change this state based on the 'value' that is received by the handler.
🔍 Minor changes -- Bump @rocket.chat/emitter from 0.31.4 to 0.31.9 in /ee/server/services ([#25021](https://github.com/RocketChat/Rocket.Chat/pull/25021) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Bump body-parser from 1.19.2 to 1.20.0 in /ee/server/services ([#25042](https://github.com/RocketChat/Rocket.Chat/pull/25042) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Bump @rocket.chat/message-parser from 0.31.4 to 0.31.9 in /ee/server/services ([#25019](https://github.com/RocketChat/Rocket.Chat/pull/25019) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Bump ejson from 2.2.1 to 2.2.2 ([#25057](https://github.com/RocketChat/Rocket.Chat/pull/25057) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Bump @rocket.chat/string-helpers from 0.31.4 to 0.31.9 in /ee/server/services ([#25018](https://github.com/RocketChat/Rocket.Chat/pull/25018) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Bump eslint-plugin-anti-trojan-source from 1.0.6 to 1.1.0 ([#25076](https://github.com/RocketChat/Rocket.Chat/pull/25076) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Bump @rocket.chat/ui-kit from 0.31.4 to 0.31.9 in /ee/server/services ([#25020](https://github.com/RocketChat/Rocket.Chat/pull/25020) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Bump minimist from 1.2.5 to 1.2.6 in /ee/server/services ([#24991](https://github.com/RocketChat/Rocket.Chat/pull/24991) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Bump @types/clipboard from 2.0.1 to 2.0.7 ([#24832](https://github.com/RocketChat/Rocket.Chat/pull/24832) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Bump pino and pino-pretty ([#25052](https://github.com/RocketChat/Rocket.Chat/pull/25052)) -- Bump @types/mailparser from 3.0.2 to 3.4.0 ([#24833](https://github.com/RocketChat/Rocket.Chat/pull/24833) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Bump template-file from 6.0.0 to 6.0.1 ([#25002](https://github.com/RocketChat/Rocket.Chat/pull/25002) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Bump @types/nodemailer from 6.4.2 to 6.4.4 ([#24822](https://github.com/RocketChat/Rocket.Chat/pull/24822) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Add error boundary to message component ([#25223](https://github.com/RocketChat/Rocket.Chat/pull/25223)) -- Bump @types/ws from 8.2.3 to 8.5.2 in /ee/server/services ([#24666](https://github.com/RocketChat/Rocket.Chat/pull/24666) by [@dependabot[bot]](https://github.com/dependabot[bot])) + Not crash the whole application if something goes wrong in the MessageList component. + + ![image](https://user-images.githubusercontent.com/40830821/162269915-931c5c3c-c979-4234-b74c-371f67467ce0.png) -- Bump @types/ws from 8.5.2 to 8.5.3 in /ee/server/services ([#24820](https://github.com/RocketChat/Rocket.Chat/pull/24820) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Add options to debug stdout and rate limiter ([#25336](https://github.com/RocketChat/Rocket.Chat/pull/25336)) -- Bump actions/checkout from 2 to 3 ([#24668](https://github.com/RocketChat/Rocket.Chat/pull/24668) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Add root package.json to houston files ([#25286](https://github.com/RocketChat/Rocket.Chat/pull/25286)) -- Bump actions/setup-node from 2 to 3 ([#24642](https://github.com/RocketChat/Rocket.Chat/pull/24642) by [@dependabot[bot]](https://github.com/dependabot[bot])) + See title -- Bump body-parser from 1.19.0 to 1.19.2 ([#24821](https://github.com/RocketChat/Rocket.Chat/pull/24821) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Add yarn plugin to check node and yarn version ([#25224](https://github.com/RocketChat/Rocket.Chat/pull/25224)) -- Bump is-svg from 4.3.1 to 4.3.2 ([#24801](https://github.com/RocketChat/Rocket.Chat/pull/24801) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Bump fuselage ([#25371](https://github.com/RocketChat/Rocket.Chat/pull/25371)) -- Bump jschardet from 1.6.0 to 3.0.0 ([#23121](https://github.com/RocketChat/Rocket.Chat/pull/23121) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Bump Fuselage packages ([#25259](https://github.com/RocketChat/Rocket.Chat/pull/25259)) -- Bump pino from 7.8.0 to 7.8.1 in /ee/server/services ([#24783](https://github.com/RocketChat/Rocket.Chat/pull/24783) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Cancel running jobs if PR is updated ([#24708](https://github.com/RocketChat/Rocket.Chat/pull/24708)) -- Bump pino from 7.8.1 to 7.9.1 in /ee/server/services ([#24869](https://github.com/RocketChat/Rocket.Chat/pull/24869) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Convert admin custom sound to tsx ([#25128](https://github.com/RocketChat/Rocket.Chat/pull/25128)) -- Bump pino-pretty from 7.5.1 to 7.5.2 in /ee/server/services ([#24689](https://github.com/RocketChat/Rocket.Chat/pull/24689) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Convert LivechatAgentActivity to raw model and TS ([#25123](https://github.com/RocketChat/Rocket.Chat/pull/25123)) -- Bump pino-pretty from 7.5.2 to 7.5.3 in /ee/server/services ([#24698](https://github.com/RocketChat/Rocket.Chat/pull/24698) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Convert Mailer to TS ([#25121](https://github.com/RocketChat/Rocket.Chat/pull/25121)) -- Bump pino-pretty from 7.5.3 to 7.5.4 in /ee/server/services ([#24870](https://github.com/RocketChat/Rocket.Chat/pull/24870) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Convert NotificationStatus to TS ([#25125](https://github.com/RocketChat/Rocket.Chat/pull/25125)) -- Bump prometheus-gc-stats from 0.6.2 to 0.6.3 ([#24803](https://github.com/RocketChat/Rocket.Chat/pull/24803) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Create README.md for Rest Typings ([#25335](https://github.com/RocketChat/Rocket.Chat/pull/25335)) -- Bump ts-node from 10.5.0 to 10.6.0 in /ee/server/services ([#24667](https://github.com/RocketChat/Rocket.Chat/pull/24667) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: ensure scripts use cross-env and ignore some dirs (ROC-54) ([#25218](https://github.com/RocketChat/Rocket.Chat/pull/25218)) -- Bump ts-node from 10.6.0 to 10.7.0 in /ee/server/services ([#24716](https://github.com/RocketChat/Rocket.Chat/pull/24716) by [@dependabot[bot]](https://github.com/dependabot[bot])) + - data and test-failure should be ignored + - ensure scripts use cross-env -- Bump url-parse from 1.5.7 to 1.5.10 ([#24640](https://github.com/RocketChat/Rocket.Chat/pull/24640) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Fix return type warnings ([#25275](https://github.com/RocketChat/Rocket.Chat/pull/25275)) -- Chore: Add E2E tests for livechat/room.close ([#24729](https://github.com/RocketChat/Rocket.Chat/pull/24729) by [@Muramatsu2602](https://github.com/Muramatsu2602)) +- Chore: Migrate oauth2server to typescript ([#25126](https://github.com/RocketChat/Rocket.Chat/pull/25126)) - * Create a new test suite file under tests/end-to-end/api/livechat - * Create tests for the following endpoint: - + ivechat/room.close +- Chore: Minor dependency updates ([#25269](https://github.com/RocketChat/Rocket.Chat/pull/25269)) -- Chore: Add E2E tests for livechat/visitor ([#24764](https://github.com/RocketChat/Rocket.Chat/pull/24764) by [@Muramatsu2602](https://github.com/Muramatsu2602)) +- Chore: Missing keys in APIsDisplay ([#24464](https://github.com/RocketChat/Rocket.Chat/pull/24464)) - - Create a new test suite file under tests/end-to-end/api/livechat - - Create tests for the following endpoints: - + livechat/visitor (create visitor, update visitor, add custom fields to visitors) +- Chore: Monorepo ([#25074](https://github.com/RocketChat/Rocket.Chat/pull/25074)) -- Chore: add some missing REST definitions ([#24925](https://github.com/RocketChat/Rocket.Chat/pull/24925) by [@gerzonc](https://github.com/gerzonc)) +- Chore: move definitions to packages ([#25085](https://github.com/RocketChat/Rocket.Chat/pull/25085)) - On the [mobile client](https://github.com/RocketChat/Rocket.Chat.ReactNative), we made an effort to collect more `REST API` definitions that are missing on the server side during our migration to TypeScript. Since we're both migrating to TypeScript, we thought it would be a good idea to share those so you guys can benefit from our initiative. +- Chore: organize test files and fix code coverage ([#24900](https://github.com/RocketChat/Rocket.Chat/pull/24900)) -- Chore: added Server Instances endpoint types ([#24507](https://github.com/RocketChat/Rocket.Chat/pull/24507)) +- Chore: Remove Alpine image deps after using them ([#25053](https://github.com/RocketChat/Rocket.Chat/pull/25053)) - Created typing for endpoint definitions on `instances.ts`. +- Chore: Remove duplicated useUserRoom ([#25180](https://github.com/RocketChat/Rocket.Chat/pull/25180)) -- Chore: added settings endpoint types ([#24506](https://github.com/RocketChat/Rocket.Chat/pull/24506)) +- Chore: Remove old files from removed Omnichannel feature ([#25129](https://github.com/RocketChat/Rocket.Chat/pull/25129)) - Created typing for endpoint definitions on `settings.ts`. +- Chore: Remove package-lock.json from houston files ([#25280](https://github.com/RocketChat/Rocket.Chat/pull/25280)) -- Chore: APIClass types ([#24747](https://github.com/RocketChat/Rocket.Chat/pull/24747)) + Houston config in the `package.json` file still mentioned `package-lock.json`, but it doesn't exist anymore - This pull request creates a new `restivus` module (.d.ts) for the `api.js` file. +- Chore: Remove unused Drone CI files ([#25124](https://github.com/RocketChat/Rocket.Chat/pull/25124)) -- Chore: Bump Fuselage packages ([#25015](https://github.com/RocketChat/Rocket.Chat/pull/25015)) +- Chore: Sync with master ([#25284](https://github.com/RocketChat/Rocket.Chat/pull/25284)) - It uses the last stable version of Fuselage packages. +- Chore: Template to generate packages ([#25174](https://github.com/RocketChat/Rocket.Chat/pull/25174)) -- Chore: Convert server functions from javascript to typescript ([#24384](https://github.com/RocketChat/Rocket.Chat/pull/24384)) + ``` + npx hygen package new test + ``` - This pull request will be used to rewrite some functions on the Chat Engine to Typescript, in order to increase security and specify variable types on the code. +- Chore: Tests with Playwright (task: All works) ([#25122](https://github.com/RocketChat/Rocket.Chat/pull/25122)) -- Chore: converted more hooks to typescript ([#24628](https://github.com/RocketChat/Rocket.Chat/pull/24628)) +- Chore: Tests with Playwright (task: ROC-28, 09-channels) ([#25196](https://github.com/RocketChat/Rocket.Chat/pull/25196)) - Converted some functions on `client/hooks/` from JavaScript to Typescript. +- Chore: TS conversion folder client ([#25031](https://github.com/RocketChat/Rocket.Chat/pull/25031)) -- Chore: Fix Cypress tests ([#24544](https://github.com/RocketChat/Rocket.Chat/pull/24544)) +- Chore: TS migration SortList ([#25167](https://github.com/RocketChat/Rocket.Chat/pull/25167)) -- Chore: Fix grammatical errors in Code of Conduct ([#24759](https://github.com/RocketChat/Rocket.Chat/pull/24759) by [@aadishJ01](https://github.com/aadishJ01)) +- Chore: Update Livechat to the last version ([#25257](https://github.com/RocketChat/Rocket.Chat/pull/25257)) -- Chore: fix grammatical errors in Features ([#24771](https://github.com/RocketChat/Rocket.Chat/pull/24771) by [@aadishJ01](https://github.com/aadishJ01)) +- Chore: Update Livechat version ([#25130](https://github.com/RocketChat/Rocket.Chat/pull/25130)) -- Chore: Fix MongoDB versions on release notes ([#24877](https://github.com/RocketChat/Rocket.Chat/pull/24877)) +- Chore: update OTR icon ([#24521](https://github.com/RocketChat/Rocket.Chat/pull/24521) by [@kibonusp](https://github.com/kibonusp)) -- Chore: Get Settings Statistics ([#24397](https://github.com/RocketChat/Rocket.Chat/pull/24397)) + I changed the shredder icon in OTR contextual bar to the stopwatch icon, recently added to the fuselage. -- Chore: Improve logger to allow log of `unknown` values ([#24726](https://github.com/RocketChat/Rocket.Chat/pull/24726)) +- i18n: Language update from LingoHub 🤖 on 2022-04-04Z ([#25043](https://github.com/RocketChat/Rocket.Chat/pull/25043)) -- Chore: Improvements on role syncing (ldap, oauth and saml) ([#23824](https://github.com/RocketChat/Rocket.Chat/pull/23824)) +- Merge master into develop & Set version to 4.7.0-develop ([#25028](https://github.com/RocketChat/Rocket.Chat/pull/25028)) -- Chore: Micro services fixes and cleanup ([#24753](https://github.com/RocketChat/Rocket.Chat/pull/24753)) +- Regression: Add `isPending` status to message ([#25299](https://github.com/RocketChat/Rocket.Chat/pull/25299)) -- Chore: Remove old scripts ([#24911](https://github.com/RocketChat/Rocket.Chat/pull/24911)) +- Regression: Add eslint package to micro services Dockerfile ([#25311](https://github.com/RocketChat/Rocket.Chat/pull/25311)) -- Chore: Skip local services changes when shutting down duplicated services ([#24810](https://github.com/RocketChat/Rocket.Chat/pull/24810)) +- Regression: Add select message to system message and thread preview and allow select on legacy template ([#25251](https://github.com/RocketChat/Rocket.Chat/pull/25251)) -- Chore: Storybook mocking and examples improved ([#24969](https://github.com/RocketChat/Rocket.Chat/pull/24969)) +- Regression: Avatar not loading on first direct message ([#25211](https://github.com/RocketChat/Rocket.Chat/pull/25211)) - - Stories from `ee/` included; - - Differentiate root story kinds; - - Mocking of `ServerContext` via Storybook parameters. + fix avatar not loading on a first direct message -- Chore: Update Livechat ([#24754](https://github.com/RocketChat/Rocket.Chat/pull/24754)) +- Regression: Better MongoDB connection management for micro services ([#25323](https://github.com/RocketChat/Rocket.Chat/pull/25323)) -- Chore: Update Livechat ([#24990](https://github.com/RocketChat/Rocket.Chat/pull/24990)) +- Regression: bump onboarding-ui version ([#25320](https://github.com/RocketChat/Rocket.Chat/pull/25320)) -- Chore(deps-dev): Bump @types/mock-require from 2.0.0 to 2.0.1 ([#24574](https://github.com/RocketChat/Rocket.Chat/pull/24574) by [@dependabot[bot]](https://github.com/dependabot[bot])) + - Bump to 'next' the onboarding-ui package from fuselage. + - Update from 'companyEmail' to 'email' adminData usage types -- i18n: Language update from LingoHub 🤖 on 2022-02-28Z ([#24644](https://github.com/RocketChat/Rocket.Chat/pull/24644)) +- Regression: Change preference to be default legacy messages ([#25255](https://github.com/RocketChat/Rocket.Chat/pull/25255)) -- i18n: Language update from LingoHub 🤖 on 2022-03-07Z ([#24717](https://github.com/RocketChat/Rocket.Chat/pull/24717)) +- Regression: CI playwright ([#25168](https://github.com/RocketChat/Rocket.Chat/pull/25168)) -- i18n: Language update from LingoHub 🤖 on 2022-03-14Z ([#24823](https://github.com/RocketChat/Rocket.Chat/pull/24823)) +- Regression: eslint not running on packages ([#25305](https://github.com/RocketChat/Rocket.Chat/pull/25305)) -- i18n: Language update from LingoHub 🤖 on 2022-03-21Z ([#24895](https://github.com/RocketChat/Rocket.Chat/pull/24895)) +- Regression: Fix CI monorepo build ([#25107](https://github.com/RocketChat/Rocket.Chat/pull/25107)) -- i18n: Language update from LingoHub 🤖 on 2022-03-28Z ([#24971](https://github.com/RocketChat/Rocket.Chat/pull/24971)) +- Regression: Fix clicking on visitor's chat in the sidebar does not display the chat window ([#25380](https://github.com/RocketChat/Rocket.Chat/pull/25380)) -- Merge master into develop & Set version to 4.6.0-develop ([#24653](https://github.com/RocketChat/Rocket.Chat/pull/24653)) + Fix: livechat room not opening. -- Regression: Add createdOTR index ([#25017](https://github.com/RocketChat/Rocket.Chat/pull/25017)) +- Regression: Fix English i18n react text ([#25368](https://github.com/RocketChat/Rocket.Chat/pull/25368)) -- Regression: Call doesn't stop ringing after agent unregistration ([#24908](https://github.com/RocketChat/Rocket.Chat/pull/24908)) + Incorrect text in reaction tooltip has been fixed -- Regression: Custom roles displaying ID instead of name on some admin screens ([#24999](https://github.com/RocketChat/Rocket.Chat/pull/24999)) +- Regression: Fix federation Matrix bridge startup ([#25273](https://github.com/RocketChat/Rocket.Chat/pull/25273)) - ![image](https://user-images.githubusercontent.com/55164754/160981416-555bcaa1-c075-4260-937c-64523472da43.png) - ![image](https://user-images.githubusercontent.com/55164754/160981452-6eae4e74-8425-4073-8256-472aba72b9db.png) +- Regression: Fix micro services Docker build ([#25193](https://github.com/RocketChat/Rocket.Chat/pull/25193)) -- Regression: Error is raised when there's no Asterisk queue available yet ([#24980](https://github.com/RocketChat/Rocket.Chat/pull/24980)) +- Regression: Fix multi line is not showing an empty line between lines ([#25317](https://github.com/RocketChat/Rocket.Chat/pull/25317)) -- Regression: Fix account service login expiration ([#24920](https://github.com/RocketChat/Rocket.Chat/pull/24920)) +- Regression: Fix reply button not working when hideFlexTab is enabled ([#25306](https://github.com/RocketChat/Rocket.Chat/pull/25306)) -- Regression: Fix ParentRoomWithEndpointData in loop ([#24809](https://github.com/RocketChat/Rocket.Chat/pull/24809)) +- Regression: Fix services Docker build on CI ([#25181](https://github.com/RocketChat/Rocket.Chat/pull/25181)) -- Regression: Fix unexpected errors breaking ddp-streamer ([#24948](https://github.com/RocketChat/Rocket.Chat/pull/24948)) +- Regression: Fix size of custom emoji and render emoji on thread message preview ([#25314](https://github.com/RocketChat/Rocket.Chat/pull/25314)) -- Regression: Improve Sidenav open/close handling and fixed codeql configs and E2E tests ([#24756](https://github.com/RocketChat/Rocket.Chat/pull/24756)) +- Regression: Fix the alpine image and dev UX installing matrix-rust-sdk-bindings ([#25319](https://github.com/RocketChat/Rocket.Chat/pull/25319)) -- Regression: Register services right away ([#24800](https://github.com/RocketChat/Rocket.Chat/pull/24800)) + The package only included a few pre-built which caused all macs to have to compile every time they installed and also caused our alpine not to work. + + This temporarily switches to a fork of the matrix-appservice-bridge package. + + Made changes to one of its child dependencies `matrix-rust-sdk-bindings` that adds pre-built binaries for mac and musl (for alpine). -- Regression: Role Sync not always working ([#24850](https://github.com/RocketChat/Rocket.Chat/pull/24850)) +- Regression: Messages in new message template Crashing. ([#25327](https://github.com/RocketChat/Rocket.Chat/pull/25327)) + +- Regression: Revert Bugsnag version ([#25313](https://github.com/RocketChat/Rocket.Chat/pull/25313)) + +- Regression: Rocket.Chat Webapp not loading. ([#25349](https://github.com/RocketChat/Rocket.Chat/pull/25349)) + +- Regression: Show username and real name on the message system ([#25254](https://github.com/RocketChat/Rocket.Chat/pull/25254)) + +- Regression: Shows error if micro service cannot connect to Mongo ([#25301](https://github.com/RocketChat/Rocket.Chat/pull/25301)) + +- Regression: Use exact Node version on micro services Docker images ([#25287](https://github.com/RocketChat/Rocket.Chat/pull/25287)) + +- Regression: Validate empty fields for Message template ([#25250](https://github.com/RocketChat/Rocket.Chat/pull/25250)) + +- Regression: yarn dev triggers build dependencies ([#25208](https://github.com/RocketChat/Rocket.Chat/pull/25208))
### 👩‍💻👨‍💻 Contributors 😍 -- [@Aman-Maheshwari](https://github.com/Aman-Maheshwari) - [@Himanshu664](https://github.com/Himanshu664) -- [@JMoVS](https://github.com/JMoVS) -- [@Muramatsu2602](https://github.com/Muramatsu2602) -- [@aadishJ01](https://github.com/aadishJ01) -- [@aswinidev](https://github.com/aswinidev) +- [@aakash-gitdev](https://github.com/aakash-gitdev) +- [@cuonghuunguyen](https://github.com/cuonghuunguyen) - [@dependabot[bot]](https://github.com/dependabot[bot]) -- [@eduardofcabrera](https://github.com/eduardofcabrera) -- [@gerzonc](https://github.com/gerzonc) -- [@ostjen](https://github.com/ostjen) -- [@tkurz](https://github.com/tkurz) +- [@kibonusp](https://github.com/kibonusp) +- [@paulobernardoaf](https://github.com/paulobernardoaf) +- [@sidmohanty11](https://github.com/sidmohanty11) ### 👩‍💻👨‍💻 Core Team 🤓 +- [@AllanPazRibeiro](https://github.com/AllanPazRibeiro) - [@KevLehman](https://github.com/KevLehman) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) - [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@alansikora](https://github.com/alansikora) - [@albuquerquefabio](https://github.com/albuquerquefabio) - [@amolghode1981](https://github.com/amolghode1981) -- [@cauefcr](https://github.com/cauefcr) +- [@d-gubert](https://github.com/d-gubert) - [@debdutdeb](https://github.com/debdutdeb) - [@dougfabris](https://github.com/dougfabris) -- [@felipe-rod123](https://github.com/felipe-rod123) - [@filipemarins](https://github.com/filipemarins) - [@gabriellsh](https://github.com/gabriellsh) - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) +- [@guijun13](https://github.com/guijun13) +- [@jeanfbrito](https://github.com/jeanfbrito) - [@juliajforesti](https://github.com/juliajforesti) -- [@matheusbsilva137](https://github.com/matheusbsilva137) - [@murtaza98](https://github.com/murtaza98) - [@nishant23122000](https://github.com/nishant23122000) - [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) -- [@renatobecker](https://github.com/renatobecker) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) +- [@souzaramon](https://github.com/souzaramon) - [@tassoevan](https://github.com/tassoevan) - [@tiagoevanp](https://github.com/tiagoevanp) +- [@tmontini](https://github.com/tmontini) +- [@weslley543](https://github.com/weslley543) - [@yash-rajpal](https://github.com/yash-rajpal) -# 4.5.6 -`2022-04-07 · 2 🐛 · 2 👩‍💻👨‍💻` +# 4.6.3 +`2022-04-19 · 1 🐛 · 1 👩‍💻👨‍💻` + +### Engine versions +- Node: `14.18.3` +- NPM: `6.14.15` +- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- Apps-Engine: `1.31.0` + +### 🐛 Bug fixes + + +- Desktop notification on multi-instance environments ([#25220](https://github.com/RocketChat/Rocket.Chat/pull/25220)) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 4.6.2 +`2022-04-14 · 2 🐛 · 2 👩‍💻👨‍💻` + +### Engine versions +- Node: `14.18.3` +- NPM: `6.14.15` +- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- Apps-Engine: `1.31.0` + +### 🐛 Bug fixes + + +- Database indexes not being created ([#25101](https://github.com/RocketChat/Rocket.Chat/pull/25101)) + +- Deactivating user breaks if user is the only room owner ([#24933](https://github.com/RocketChat/Rocket.Chat/pull/24933) by [@sidmohanty11](https://github.com/sidmohanty11)) + + ## Before + + https://user-images.githubusercontent.com/73601258/160000871-cfc2f2a5-2a59-4d27-8049-7754d003dd48.mp4 + + + + ## After + https://user-images.githubusercontent.com/73601258/159998287-681ab475-ff33-4282-82ff-db751c59a392.mp4 + +### 👩‍💻👨‍💻 Contributors 😍 + +- [@sidmohanty11](https://github.com/sidmohanty11) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 4.6.1 +`2022-04-07 · 6 🐛 · 5 👩‍💻👨‍💻` ### Engine versions - Node: `14.18.3` @@ -2036,19 +3522,36 @@ ### 🐛 Bug fixes +- FormData uploads not working ([#25069](https://github.com/RocketChat/Rocket.Chat/pull/25069)) + +- Invitation links don't redirect to the registration form ([#25082](https://github.com/RocketChat/Rocket.Chat/pull/25082)) + - NPS never finishing sending results ([#25067](https://github.com/RocketChat/Rocket.Chat/pull/25067)) - Proxy settings being ignored ([#25022](https://github.com/RocketChat/Rocket.Chat/pull/25022)) Modify Meteor's `HTTP.call` to add back proxy support +- Upgrade Tab showing for a split second ([#25050](https://github.com/RocketChat/Rocket.Chat/pull/25050)) + +- UserAutoComplete not rendering UserAvatar correctly ([#25055](https://github.com/RocketChat/Rocket.Chat/pull/25055)) + + ### before + ![Screen Shot 2022-04-04 at 16 50 21](https://user-images.githubusercontent.com/27704687/161620921-800bf66a-806d-4f83-b2e1-073c34215001.png) + + ### after + ![Screen Shot 2022-04-04 at 16 49 00](https://user-images.githubusercontent.com/27704687/161620720-3e27774d-c241-46ca-b764-932a9295d709.png) + ### 👩‍💻👨‍💻 Core Team 🤓 +- [@dougfabris](https://github.com/dougfabris) +- [@gabriellsh](https://github.com/gabriellsh) - [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) - [@sampaiodiego](https://github.com/sampaiodiego) +- [@yash-rajpal](https://github.com/yash-rajpal) -# 4.5.5 -`2022-03-30 · 2 🐛 · 2 🔍 · 6 👩‍💻👨‍💻` +# 4.6.0 +`2022-04-01 · 2 🎉 · 7 🚀 · 57 🐛 · 62 🔍 · 34 👩‍💻👨‍💻` ### Engine versions - Node: `14.18.3` @@ -2056,537 +3559,2970 @@ - MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` - Apps-Engine: `1.31.0` -### 🐛 Bug fixes +### 🎉 New features + + +- Telemetry Events ([#24781](https://github.com/RocketChat/Rocket.Chat/pull/24781) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + +- Upgrade Tab ([#24835](https://github.com/RocketChat/Rocket.Chat/pull/24835)) + + ![image](https://user-images.githubusercontent.com/27704687/160172260-c656282e-a487-4092-948d-d11c9bacb598.png) + +### 🚀 Improvements + + +- **ENTERPRISE:** Don't start presence monitor when running micro services ([#24739](https://github.com/RocketChat/Rocket.Chat/pull/24739)) + +- Adding new statistics related to voip and omnichannel ([#24887](https://github.com/RocketChat/Rocket.Chat/pull/24887)) + + - Total of Canned response messages sent + - Total of tags used + - Last-Chatted Agent Preferred (enabled/disabled) + - Assign new conversations to the contact manager (enabled/disabled) + - How to handle Visitor Abandonment setting + - Amount of chats placed on hold + - VoIP Enabled + - Amount of VoIP Calls + - Amount of VoIP Extensions connected + - Amount of Calls placed on hold (1x per call) + - Fixed Session Aggregation type definitions + +- New omnichannel statistics and async statistics processing. ([#24749](https://github.com/RocketChat/Rocket.Chat/pull/24749)) + + https://app.clickup.com/t/1z4zg4e + +- Standarize queue behavior for managers and agents when subscribing ([#24837](https://github.com/RocketChat/Rocket.Chat/pull/24837)) + +- Updated links in readme ([#24028](https://github.com/RocketChat/Rocket.Chat/pull/24028) by [@aswinidev](https://github.com/aswinidev)) + +- UX - VoIP Call Component ([#24748](https://github.com/RocketChat/Rocket.Chat/pull/24748)) + +- Voip Extensions disabled state ([#24750](https://github.com/RocketChat/Rocket.Chat/pull/24750)) + +### 🐛 Bug fixes + + +- "livechat/webrtc.call" endpoint not working ([#24804](https://github.com/RocketChat/Rocket.Chat/pull/24804)) + +- "Match error" when converting a team to a channel ([#24629](https://github.com/RocketChat/Rocket.Chat/pull/24629)) + + - Fix "Match error" when trying to convert a channel to a team; + +- **ENTERPRISE:** Auto reload feature of ddp-streamer micro service ([#24793](https://github.com/RocketChat/Rocket.Chat/pull/24793)) + +- **ENTERPRISE:** DDP streamer not sending data to all clients ([#24738](https://github.com/RocketChat/Rocket.Chat/pull/24738)) + +- **ENTERPRISE:** Notifications not being sent by ddp-streamer ([#24831](https://github.com/RocketChat/Rocket.Chat/pull/24831)) + +- **ENTERPRISE:** Presence micro service logic ([#24724](https://github.com/RocketChat/Rocket.Chat/pull/24724)) + +- **VOIP:** SidebarFooter component ([#24838](https://github.com/RocketChat/Rocket.Chat/pull/24838)) + + - Improve the CallProvider code; + - Adjust the text case of the VoIP component on the FooterSidebar; + - Fix the bad behavior with the changes in queue's name. + +- `PaginatedSelectFiltered` not handling changes ([#24732](https://github.com/RocketChat/Rocket.Chat/pull/24732)) + +- API Error preventing adding an email to users without one (like bot/app users) ([#24709](https://github.com/RocketChat/Rocket.Chat/pull/24709)) + +- Apple login script being loaded even when Apple Login is disabled. ([#24760](https://github.com/RocketChat/Rocket.Chat/pull/24760)) + +- Apple OAuth ([#24879](https://github.com/RocketChat/Rocket.Chat/pull/24879)) + +- auto-join team channels not honoring user preferences ([#24779](https://github.com/RocketChat/Rocket.Chat/pull/24779) by [@ostjen](https://github.com/ostjen)) + +- Broken build caused by PRs modifying same file differently ([#24863](https://github.com/RocketChat/Rocket.Chat/pull/24863)) + +- Broken multiple OAuth integrations ([#24705](https://github.com/RocketChat/Rocket.Chat/pull/24705)) + +- Components for user search ([#24677](https://github.com/RocketChat/Rocket.Chat/pull/24677)) + +- Critical: Incorrect visitor getting assigned to a chat from apps ([#24805](https://github.com/RocketChat/Rocket.Chat/pull/24805)) + +- Custom script not being fired ([#24901](https://github.com/RocketChat/Rocket.Chat/pull/24901)) + +- Date Message Export Filter Fix ([#24542](https://github.com/RocketChat/Rocket.Chat/pull/24542) by [@eduardofcabrera](https://github.com/eduardofcabrera)) + + Fix message export filter to get all messages between "from date" and "to date", including "to date". + +- DDP Rate Limiter Translation key ([#24898](https://github.com/RocketChat/Rocket.Chat/pull/24898)) + + Before: + image + + + Now: + image + +- DDP streamer errors ([#24710](https://github.com/RocketChat/Rocket.Chat/pull/24710)) + +- Disable voip button when call is in progress ([#24864](https://github.com/RocketChat/Rocket.Chat/pull/24864)) + +- Duplicated 'name' log key ([#24590](https://github.com/RocketChat/Rocket.Chat/pull/24590)) + +- Duplicated "jump to message" button on starred messages ([#24867](https://github.com/RocketChat/Rocket.Chat/pull/24867) by [@Himanshu664](https://github.com/Himanshu664)) + +- External search providers not working ([#24860](https://github.com/RocketChat/Rocket.Chat/pull/24860) by [@tkurz](https://github.com/tkurz)) + +- German translation for Monitore ([#24785](https://github.com/RocketChat/Rocket.Chat/pull/24785) by [@JMoVS](https://github.com/JMoVS)) + +- Handle Other Formats inside Upload Avatar ([#24226](https://github.com/RocketChat/Rocket.Chat/pull/24226)) + + After resolving issue #24213 : + + + https://user-images.githubusercontent.com/53515714/150325012-91413025-786e-4ce0-ae75-629f6b05b024.mp4 + +- High CPU usage caused by CallProvider ([#24994](https://github.com/RocketChat/Rocket.Chat/pull/24994)) + + Remove infinity loop inside useVoipClient hook. + + #closes #24970 + +- Ignore customClass on messages ([#24845](https://github.com/RocketChat/Rocket.Chat/pull/24845)) + +- LDAP avatars being rotated according to metadata even if the setting to rotate uploads is off ([#24320](https://github.com/RocketChat/Rocket.Chat/pull/24320)) + + - Use the `FileUpload_RotateImages` setting (**Administration > File Upload > Rotate images on upload**) to control whether avatars should be rotated automatically based on their data (XEIF); + - Display the avatar image preview (orientation) according to the `FileUpload_RotateImages` setting. + +- Missing dependency on useEffect at CallProvider ([#24882](https://github.com/RocketChat/Rocket.Chat/pull/24882)) + +- Missing username on messages imported from Slack ([#24674](https://github.com/RocketChat/Rocket.Chat/pull/24674)) + + - Fix missing sender's username on messages imported from Slack. + +- Nextcloud OAuth for incomplete token URL ([#24476](https://github.com/RocketChat/Rocket.Chat/pull/24476)) + +- no id of room closer in livechat-close message ([#24683](https://github.com/RocketChat/Rocket.Chat/pull/24683)) + +- Opening a new DM from user card ([#24623](https://github.com/RocketChat/Rocket.Chat/pull/24623)) + + A race condition on `useRoomIcon` -- delayed merge of rooms and subscriptions -- was causing a UI crash whenever someone tried to open a DM from the user card component. + +- Prevent call button toggle when user is on call ([#24758](https://github.com/RocketChat/Rocket.Chat/pull/24758)) + +- Prune Message issue ([#24424](https://github.com/RocketChat/Rocket.Chat/pull/24424)) + +- Push privacy config to not show username not being respected ([#24606](https://github.com/RocketChat/Rocket.Chat/pull/24606)) + +- Register with Secret URL ([#24921](https://github.com/RocketChat/Rocket.Chat/pull/24921)) + +- Reload roomslist after successful deletion of a room from admin panel. ([#23795](https://github.com/RocketChat/Rocket.Chat/pull/23795) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari)) + + Removed the logic for calling the `rooms.adminRooms` endPoint from the `RoomsTable` Component and moved it to its parent component `RoomsPage`. + This allows to call the endPoint `rooms.adminRooms` from `EditRoomContextBar` Component which is also has `RoomPage` Component as its parent. + + Also added a succes toast message after the successful deletion of room. + +- Revert AutoComplete ([#24812](https://github.com/RocketChat/Rocket.Chat/pull/24812)) + +- Room archived/unarchived system messages aren't sent when editing room settings ([#24897](https://github.com/RocketChat/Rocket.Chat/pull/24897)) + + - Send the "Room archived" and "Room unarchived" system messages when editing room settings (and not only when rooms are archived/unarchived with the slash-command); + - Fix the "Hide System Messages" option for the "Room archived" and "Room unarchived" system messages; + +- room message not load when is a new message ([#24955](https://github.com/RocketChat/Rocket.Chat/pull/24955)) + + When the room object is searched for the first time, it does not exist on the front object yet (subscription), adding a fallback search for room list will guarantee to search the room details. + + before: + https://user-images.githubusercontent.com/9275105/160223241-d2319f3e-82c5-47d6-867f-695ab2361a17.mp4 + + after: + https://user-images.githubusercontent.com/9275105/160223244-84d0d2a1-3d95-464d-8b8a-e264b0d4d690.mp4 + +- Room's message count not being incremented on import ([#24696](https://github.com/RocketChat/Rocket.Chat/pull/24696)) + + - Fix rooms' message counter not being incremented on message import. + +- SAML Force name to string ([#24930](https://github.com/RocketChat/Rocket.Chat/pull/24930)) + +- Several issues related to custom roles ([#24052](https://github.com/RocketChat/Rocket.Chat/pull/24052)) + + - Throw an error when trying to delete a role (User or Subscription role) that are still being used; + - Fix "Invalid Role" error for custom roles in Role Editing sidebar; + - Fix "Users in Role" screen for custom roles. + +- Show call icon only when user has extension associated ([#24752](https://github.com/RocketChat/Rocket.Chat/pull/24752)) + +- Show only available agents on extension association modal ([#24680](https://github.com/RocketChat/Rocket.Chat/pull/24680)) + +- Show only enabled departments on forward ([#24829](https://github.com/RocketChat/Rocket.Chat/pull/24829)) + +- System messages are sent when adding or removing a group from a team ([#24743](https://github.com/RocketChat/Rocket.Chat/pull/24743)) + + - Do not send system messages when adding or removing a new or existing _group_ from a team. + +- Typo and placeholder on wrap up call modal ([#24737](https://github.com/RocketChat/Rocket.Chat/pull/24737)) + +- Typo in wrap-up term ([#24661](https://github.com/RocketChat/Rocket.Chat/pull/24661)) + +- VoIP button gets disabled whenever user status changes ([#24789](https://github.com/RocketChat/Rocket.Chat/pull/24789)) + +- VoIP Enable/Disable setting on CallContext/CallProvider Notifications ([#24607](https://github.com/RocketChat/Rocket.Chat/pull/24607)) + +- Voip Stream Reinitialization Error ([#24657](https://github.com/RocketChat/Rocket.Chat/pull/24657)) + +- VoipExtensionsPage component call ([#24792](https://github.com/RocketChat/Rocket.Chat/pull/24792)) + +- Wrong business hour behavior ([#24896](https://github.com/RocketChat/Rocket.Chat/pull/24896)) + +- Wrong param usage on queue summary call ([#24799](https://github.com/RocketChat/Rocket.Chat/pull/24799)) + +
+🔍 Minor changes + + +- Bump @rocket.chat/emitter from 0.31.4 to 0.31.9 in /ee/server/services ([#25021](https://github.com/RocketChat/Rocket.Chat/pull/25021) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @rocket.chat/message-parser from 0.31.4 to 0.31.9 in /ee/server/services ([#25019](https://github.com/RocketChat/Rocket.Chat/pull/25019) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @rocket.chat/string-helpers from 0.31.4 to 0.31.9 in /ee/server/services ([#25018](https://github.com/RocketChat/Rocket.Chat/pull/25018) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @rocket.chat/ui-kit from 0.31.4 to 0.31.9 in /ee/server/services ([#25020](https://github.com/RocketChat/Rocket.Chat/pull/25020) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @types/clipboard from 2.0.1 to 2.0.7 ([#24832](https://github.com/RocketChat/Rocket.Chat/pull/24832) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @types/mailparser from 3.0.2 to 3.4.0 ([#24833](https://github.com/RocketChat/Rocket.Chat/pull/24833) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @types/nodemailer from 6.4.2 to 6.4.4 ([#24822](https://github.com/RocketChat/Rocket.Chat/pull/24822) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @types/ws from 8.2.3 to 8.5.2 in /ee/server/services ([#24666](https://github.com/RocketChat/Rocket.Chat/pull/24666) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @types/ws from 8.5.2 to 8.5.3 in /ee/server/services ([#24820](https://github.com/RocketChat/Rocket.Chat/pull/24820) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump actions/checkout from 2 to 3 ([#24668](https://github.com/RocketChat/Rocket.Chat/pull/24668) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump actions/setup-node from 2 to 3 ([#24642](https://github.com/RocketChat/Rocket.Chat/pull/24642) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump body-parser from 1.19.0 to 1.19.2 ([#24821](https://github.com/RocketChat/Rocket.Chat/pull/24821) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump is-svg from 4.3.1 to 4.3.2 ([#24801](https://github.com/RocketChat/Rocket.Chat/pull/24801) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump jschardet from 1.6.0 to 3.0.0 ([#23121](https://github.com/RocketChat/Rocket.Chat/pull/23121) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump pino from 7.8.0 to 7.8.1 in /ee/server/services ([#24783](https://github.com/RocketChat/Rocket.Chat/pull/24783) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump pino from 7.8.1 to 7.9.1 in /ee/server/services ([#24869](https://github.com/RocketChat/Rocket.Chat/pull/24869) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump pino-pretty from 7.5.1 to 7.5.2 in /ee/server/services ([#24689](https://github.com/RocketChat/Rocket.Chat/pull/24689) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump pino-pretty from 7.5.2 to 7.5.3 in /ee/server/services ([#24698](https://github.com/RocketChat/Rocket.Chat/pull/24698) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump pino-pretty from 7.5.3 to 7.5.4 in /ee/server/services ([#24870](https://github.com/RocketChat/Rocket.Chat/pull/24870) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump prometheus-gc-stats from 0.6.2 to 0.6.3 ([#24803](https://github.com/RocketChat/Rocket.Chat/pull/24803) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump ts-node from 10.5.0 to 10.6.0 in /ee/server/services ([#24667](https://github.com/RocketChat/Rocket.Chat/pull/24667) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump ts-node from 10.6.0 to 10.7.0 in /ee/server/services ([#24716](https://github.com/RocketChat/Rocket.Chat/pull/24716) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump url-parse from 1.5.7 to 1.5.10 ([#24640](https://github.com/RocketChat/Rocket.Chat/pull/24640) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Chore: Add E2E tests for livechat/room.close ([#24729](https://github.com/RocketChat/Rocket.Chat/pull/24729) by [@Muramatsu2602](https://github.com/Muramatsu2602)) + + * Create a new test suite file under tests/end-to-end/api/livechat + * Create tests for the following endpoint: + + ivechat/room.close + +- Chore: Add E2E tests for livechat/visitor ([#24764](https://github.com/RocketChat/Rocket.Chat/pull/24764) by [@Muramatsu2602](https://github.com/Muramatsu2602)) + + - Create a new test suite file under tests/end-to-end/api/livechat + - Create tests for the following endpoints: + + livechat/visitor (create visitor, update visitor, add custom fields to visitors) + +- Chore: add some missing REST definitions ([#24925](https://github.com/RocketChat/Rocket.Chat/pull/24925) by [@gerzonc](https://github.com/gerzonc)) + + On the [mobile client](https://github.com/RocketChat/Rocket.Chat.ReactNative), we made an effort to collect more `REST API` definitions that are missing on the server side during our migration to TypeScript. Since we're both migrating to TypeScript, we thought it would be a good idea to share those so you guys can benefit from our initiative. + +- Chore: added Server Instances endpoint types ([#24507](https://github.com/RocketChat/Rocket.Chat/pull/24507)) + + Created typing for endpoint definitions on `instances.ts`. + +- Chore: added settings endpoint types ([#24506](https://github.com/RocketChat/Rocket.Chat/pull/24506)) + + Created typing for endpoint definitions on `settings.ts`. + +- Chore: APIClass types ([#24747](https://github.com/RocketChat/Rocket.Chat/pull/24747)) + + This pull request creates a new `restivus` module (.d.ts) for the `api.js` file. + +- Chore: Bump Fuselage packages ([#25015](https://github.com/RocketChat/Rocket.Chat/pull/25015)) + + It uses the last stable version of Fuselage packages. + +- Chore: Convert server functions from javascript to typescript ([#24384](https://github.com/RocketChat/Rocket.Chat/pull/24384)) + + This pull request will be used to rewrite some functions on the Chat Engine to Typescript, in order to increase security and specify variable types on the code. + +- Chore: converted more hooks to typescript ([#24628](https://github.com/RocketChat/Rocket.Chat/pull/24628)) + + Converted some functions on `client/hooks/` from JavaScript to Typescript. + +- Chore: Fix Cypress tests ([#24544](https://github.com/RocketChat/Rocket.Chat/pull/24544)) + +- Chore: Fix grammatical errors in Code of Conduct ([#24759](https://github.com/RocketChat/Rocket.Chat/pull/24759) by [@aadishJ01](https://github.com/aadishJ01)) + +- Chore: fix grammatical errors in Features ([#24771](https://github.com/RocketChat/Rocket.Chat/pull/24771) by [@aadishJ01](https://github.com/aadishJ01)) + +- Chore: Fix MongoDB versions on release notes ([#24877](https://github.com/RocketChat/Rocket.Chat/pull/24877)) + +- Chore: Get Settings Statistics ([#24397](https://github.com/RocketChat/Rocket.Chat/pull/24397)) + +- Chore: Improve logger to allow log of `unknown` values ([#24726](https://github.com/RocketChat/Rocket.Chat/pull/24726)) + +- Chore: Improvements on role syncing (ldap, oauth and saml) ([#23824](https://github.com/RocketChat/Rocket.Chat/pull/23824)) + +- Chore: Micro services fixes and cleanup ([#24753](https://github.com/RocketChat/Rocket.Chat/pull/24753)) + +- Chore: Remove old scripts ([#24911](https://github.com/RocketChat/Rocket.Chat/pull/24911)) + +- Chore: Skip local services changes when shutting down duplicated services ([#24810](https://github.com/RocketChat/Rocket.Chat/pull/24810)) + +- Chore: Storybook mocking and examples improved ([#24969](https://github.com/RocketChat/Rocket.Chat/pull/24969)) + + - Stories from `ee/` included; + - Differentiate root story kinds; + - Mocking of `ServerContext` via Storybook parameters. + +- Chore: Update Livechat ([#24754](https://github.com/RocketChat/Rocket.Chat/pull/24754)) + +- Chore: Update Livechat ([#24990](https://github.com/RocketChat/Rocket.Chat/pull/24990)) + +- Chore(deps-dev): Bump @types/mock-require from 2.0.0 to 2.0.1 ([#24574](https://github.com/RocketChat/Rocket.Chat/pull/24574) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- i18n: Language update from LingoHub 🤖 on 2022-02-28Z ([#24644](https://github.com/RocketChat/Rocket.Chat/pull/24644)) + +- i18n: Language update from LingoHub 🤖 on 2022-03-07Z ([#24717](https://github.com/RocketChat/Rocket.Chat/pull/24717)) + +- i18n: Language update from LingoHub 🤖 on 2022-03-14Z ([#24823](https://github.com/RocketChat/Rocket.Chat/pull/24823)) + +- i18n: Language update from LingoHub 🤖 on 2022-03-21Z ([#24895](https://github.com/RocketChat/Rocket.Chat/pull/24895)) + +- i18n: Language update from LingoHub 🤖 on 2022-03-28Z ([#24971](https://github.com/RocketChat/Rocket.Chat/pull/24971)) + +- Merge master into develop & Set version to 4.6.0-develop ([#24653](https://github.com/RocketChat/Rocket.Chat/pull/24653)) + +- Regression: Add createdOTR index ([#25017](https://github.com/RocketChat/Rocket.Chat/pull/25017)) + +- Regression: Call doesn't stop ringing after agent unregistration ([#24908](https://github.com/RocketChat/Rocket.Chat/pull/24908)) + +- Regression: Custom roles displaying ID instead of name on some admin screens ([#24999](https://github.com/RocketChat/Rocket.Chat/pull/24999)) + + ![image](https://user-images.githubusercontent.com/55164754/160981416-555bcaa1-c075-4260-937c-64523472da43.png) + ![image](https://user-images.githubusercontent.com/55164754/160981452-6eae4e74-8425-4073-8256-472aba72b9db.png) + +- Regression: Error is raised when there's no Asterisk queue available yet ([#24980](https://github.com/RocketChat/Rocket.Chat/pull/24980)) + +- Regression: Fix account service login expiration ([#24920](https://github.com/RocketChat/Rocket.Chat/pull/24920)) + +- Regression: Fix ParentRoomWithEndpointData in loop ([#24809](https://github.com/RocketChat/Rocket.Chat/pull/24809)) + +- Regression: Fix unexpected errors breaking ddp-streamer ([#24948](https://github.com/RocketChat/Rocket.Chat/pull/24948)) + +- Regression: Improve Sidenav open/close handling and fixed codeql configs and E2E tests ([#24756](https://github.com/RocketChat/Rocket.Chat/pull/24756)) + +- Regression: Register services right away ([#24800](https://github.com/RocketChat/Rocket.Chat/pull/24800)) + +- Regression: Role Sync not always working ([#24850](https://github.com/RocketChat/Rocket.Chat/pull/24850)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@Aman-Maheshwari](https://github.com/Aman-Maheshwari) +- [@Himanshu664](https://github.com/Himanshu664) +- [@JMoVS](https://github.com/JMoVS) +- [@Muramatsu2602](https://github.com/Muramatsu2602) +- [@aadishJ01](https://github.com/aadishJ01) +- [@aswinidev](https://github.com/aswinidev) +- [@dependabot[bot]](https://github.com/dependabot[bot]) +- [@eduardofcabrera](https://github.com/eduardofcabrera) +- [@gerzonc](https://github.com/gerzonc) +- [@ostjen](https://github.com/ostjen) +- [@tkurz](https://github.com/tkurz) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@albuquerquefabio](https://github.com/albuquerquefabio) +- [@amolghode1981](https://github.com/amolghode1981) +- [@cauefcr](https://github.com/cauefcr) +- [@debdutdeb](https://github.com/debdutdeb) +- [@dougfabris](https://github.com/dougfabris) +- [@felipe-rod123](https://github.com/felipe-rod123) +- [@filipemarins](https://github.com/filipemarins) +- [@gabriellsh](https://github.com/gabriellsh) +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@ggazzo](https://github.com/ggazzo) +- [@juliajforesti](https://github.com/juliajforesti) +- [@matheusbsilva137](https://github.com/matheusbsilva137) +- [@murtaza98](https://github.com/murtaza98) +- [@nishant23122000](https://github.com/nishant23122000) +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@renatobecker](https://github.com/renatobecker) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) +- [@tiagoevanp](https://github.com/tiagoevanp) +- [@yash-rajpal](https://github.com/yash-rajpal) + +# 4.5.6 +`2022-04-07 · 2 🐛 · 2 👩‍💻👨‍💻` + +### Engine versions +- Node: `14.18.3` +- NPM: `6.14.15` +- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- Apps-Engine: `1.31.0` + +### 🐛 Bug fixes + + +- NPS never finishing sending results ([#25067](https://github.com/RocketChat/Rocket.Chat/pull/25067)) + +- Proxy settings being ignored ([#25022](https://github.com/RocketChat/Rocket.Chat/pull/25022)) + + Modify Meteor's `HTTP.call` to add back proxy support + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 4.5.5 +`2022-03-30 · 2 🐛 · 2 🔍 · 6 👩‍💻👨‍💻` + +### Engine versions +- Node: `14.18.3` +- NPM: `6.14.15` +- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- Apps-Engine: `1.31.0` + +### 🐛 Bug fixes + + +- High CPU usage caused by CallProvider ([#24994](https://github.com/RocketChat/Rocket.Chat/pull/24994)) + + Remove infinity loop inside useVoipClient hook. + + #closes #24970 + +- Multiple issues starting a new DM ([#24955](https://github.com/RocketChat/Rocket.Chat/pull/24955)) + + When the room object is searched for the first time, it does not exist on the front object yet (subscription), adding a fallback search for room list will guarantee to search the room details. + + before: + https://user-images.githubusercontent.com/9275105/160223241-d2319f3e-82c5-47d6-867f-695ab2361a17.mp4 + + after: + https://user-images.githubusercontent.com/9275105/160223244-84d0d2a1-3d95-464d-8b8a-e264b0d4d690.mp4 + +
+🔍 Minor changes + + +- Chore: Update Livechat ([#24990](https://github.com/RocketChat/Rocket.Chat/pull/24990)) + +- Release 4.5.5 ([#24998](https://github.com/RocketChat/Rocket.Chat/pull/24998)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@filipemarins](https://github.com/filipemarins) +- [@ggazzo](https://github.com/ggazzo) +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tiagoevanp](https://github.com/tiagoevanp) + +# 4.5.4 +`2022-03-24 · 1 🐛 · 1 🔍 · 3 👩‍💻👨‍💻` + +### Engine versions +- Node: `14.18.3` +- NPM: `6.14.15` +- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- Apps-Engine: `1.31.0` + +### 🐛 Bug fixes + + +- SAML Force name to string ([#24930](https://github.com/RocketChat/Rocket.Chat/pull/24930)) + +
+🔍 Minor changes + + +- Release 4.5.4 ([#24938](https://github.com/RocketChat/Rocket.Chat/pull/24938)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@AllanPazRibeiro](https://github.com/AllanPazRibeiro) +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) + +# 4.5.3 +`2022-03-21 · 2 🚀 · 8 🐛 · 1 🔍 · 5 👩‍💻👨‍💻` + +### Engine versions +- Node: `14.18.3` +- NPM: `6.14.15` +- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- Apps-Engine: `1.31.0` + +### 🚀 Improvements + + +- Standarize queue behavior for managers and agents when subscribing ([#24837](https://github.com/RocketChat/Rocket.Chat/pull/24837)) + +- UX - VoIP Call Component ([#24748](https://github.com/RocketChat/Rocket.Chat/pull/24748)) + +### 🐛 Bug fixes + + +- **VOIP:** SidebarFooter component ([#24838](https://github.com/RocketChat/Rocket.Chat/pull/24838)) + + - Improve the CallProvider code; + - Adjust the text case of the VoIP component on the FooterSidebar; + - Fix the bad behavior with the changes in queue's name. + +- Broken build caused by PRs modifying same file differently ([#24863](https://github.com/RocketChat/Rocket.Chat/pull/24863)) + +- Custom script not being fired ([#24901](https://github.com/RocketChat/Rocket.Chat/pull/24901)) + +- Disable voip button when call is in progress ([#24864](https://github.com/RocketChat/Rocket.Chat/pull/24864)) + +- Show call icon only when user has extension associated ([#24752](https://github.com/RocketChat/Rocket.Chat/pull/24752)) + +- Show only enabled departments on forward ([#24829](https://github.com/RocketChat/Rocket.Chat/pull/24829)) + +- VoIP button gets disabled whenever user status changes ([#24789](https://github.com/RocketChat/Rocket.Chat/pull/24789)) + +- Wrong param usage on queue summary call ([#24799](https://github.com/RocketChat/Rocket.Chat/pull/24799)) + +
+🔍 Minor changes + + +- Chore: Fix MongoDB versions on release notes ([#24877](https://github.com/RocketChat/Rocket.Chat/pull/24877)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@amolghode1981](https://github.com/amolghode1981) +- [@ggazzo](https://github.com/ggazzo) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tiagoevanp](https://github.com/tiagoevanp) + +# 4.5.2 +`2022-03-12 · 1 🚀 · 7 🐛 · 1 🔍 · 8 👩‍💻👨‍💻` + +### Engine versions +- Node: `14.18.3` +- NPM: `6.14.15` +- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- Apps-Engine: `1.31.0` + +### 🚀 Improvements + + +- Voip Extensions disabled state ([#24750](https://github.com/RocketChat/Rocket.Chat/pull/24750)) + +### 🐛 Bug fixes + + +- "livechat/webrtc.call" endpoint not working ([#24804](https://github.com/RocketChat/Rocket.Chat/pull/24804)) + +- `PaginatedSelectFiltered` not handling changes ([#24732](https://github.com/RocketChat/Rocket.Chat/pull/24732)) + +- Broken multiple OAuth integrations ([#24705](https://github.com/RocketChat/Rocket.Chat/pull/24705)) + +- Critical: Incorrect visitor getting assigned to a chat from apps ([#24805](https://github.com/RocketChat/Rocket.Chat/pull/24805)) + +- Opening a new DM from user card ([#24623](https://github.com/RocketChat/Rocket.Chat/pull/24623)) + + A race condition on `useRoomIcon` -- delayed merge of rooms and subscriptions -- was causing a UI crash whenever someone tried to open a DM from the user card component. + +- Revert AutoComplete ([#24812](https://github.com/RocketChat/Rocket.Chat/pull/24812)) + +- VoipExtensionsPage component call ([#24792](https://github.com/RocketChat/Rocket.Chat/pull/24792)) + +
+🔍 Minor changes + + +- Regression: Fix ParentRoomWithEndpointData in loop ([#24809](https://github.com/RocketChat/Rocket.Chat/pull/24809)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@debdutdeb](https://github.com/debdutdeb) +- [@ggazzo](https://github.com/ggazzo) +- [@juliajforesti](https://github.com/juliajforesti) +- [@murtaza98](https://github.com/murtaza98) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) + +# 4.5.1 +`2022-03-09 · 13 🐛 · 2 🔍 · 12 👩‍💻👨‍💻` + +### Engine versions +- Node: `14.18.3` +- NPM: `6.14.15` +- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- Apps-Engine: `1.31.0` + +### 🐛 Bug fixes + + +- Apple login script being loaded even when Apple Login is disabled. ([#24760](https://github.com/RocketChat/Rocket.Chat/pull/24760)) + +- Components for user search ([#24677](https://github.com/RocketChat/Rocket.Chat/pull/24677)) + +- Duplicated 'name' log key ([#24590](https://github.com/RocketChat/Rocket.Chat/pull/24590)) + +- Missing username on messages imported from Slack ([#24674](https://github.com/RocketChat/Rocket.Chat/pull/24674)) + + - Fix missing sender's username on messages imported from Slack. + +- no id of room closer in livechat-close message ([#24683](https://github.com/RocketChat/Rocket.Chat/pull/24683)) + +- Reload roomslist after successful deletion of a room from admin panel. ([#23795](https://github.com/RocketChat/Rocket.Chat/pull/23795) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari)) + + Removed the logic for calling the `rooms.adminRooms` endPoint from the `RoomsTable` Component and moved it to its parent component `RoomsPage`. + This allows to call the endPoint `rooms.adminRooms` from `EditRoomContextBar` Component which is also has `RoomPage` Component as its parent. + + Also added a succes toast message after the successful deletion of room. + +- Room's message count not being incremented on import ([#24696](https://github.com/RocketChat/Rocket.Chat/pull/24696)) + + - Fix rooms' message counter not being incremented on message import. + +- Show only available agents on extension association modal ([#24680](https://github.com/RocketChat/Rocket.Chat/pull/24680)) + +- System messages are sent when adding or removing a group from a team ([#24743](https://github.com/RocketChat/Rocket.Chat/pull/24743)) + + - Do not send system messages when adding or removing a new or existing _group_ from a team. + +- Typo and placeholder on wrap up call modal ([#24737](https://github.com/RocketChat/Rocket.Chat/pull/24737)) + +- Typo in wrap-up term ([#24661](https://github.com/RocketChat/Rocket.Chat/pull/24661)) + +- VoIP Enable/Disable setting on CallContext/CallProvider Notifications ([#24607](https://github.com/RocketChat/Rocket.Chat/pull/24607)) + +- Voip Stream Reinitialization Error ([#24657](https://github.com/RocketChat/Rocket.Chat/pull/24657)) + +
+🔍 Minor changes + + +- Chore: Update Livechat ([#24754](https://github.com/RocketChat/Rocket.Chat/pull/24754)) + +- Release 4.5.1 ([#24782](https://github.com/RocketChat/Rocket.Chat/pull/24782) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari) & [@cuonghuunguyen](https://github.com/cuonghuunguyen)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@Aman-Maheshwari](https://github.com/Aman-Maheshwari) +- [@cuonghuunguyen](https://github.com/cuonghuunguyen) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@amolghode1981](https://github.com/amolghode1981) +- [@juliajforesti](https://github.com/juliajforesti) +- [@matheusbsilva137](https://github.com/matheusbsilva137) +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@renatobecker](https://github.com/renatobecker) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) +- [@tiagoevanp](https://github.com/tiagoevanp) + +# 4.5.0 +`2022-02-28 · 3 🎉 · 15 🚀 · 19 🐛 · 72 🔍 · 30 👩‍💻👨‍💻` + +### Engine versions +- Node: `14.18.3` +- NPM: `6.14.15` +- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- Apps-Engine: `1.31.0` + +### 🎉 New features + + +- E2E password generator ([#24114](https://github.com/RocketChat/Rocket.Chat/pull/24114) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + +- Marketplace sort filter ([#24567](https://github.com/RocketChat/Rocket.Chat/pull/24567) by [@ujorgeleite](https://github.com/ujorgeleite)) + + Implemented a sort filter for the marketplace screen. This component sorts the marketplace apps list in 4 ways, alphabetical order(A-Z), inverse alphabetical order(Z-A), most recently updated(MRU), and least recent updated(LRU). Besides that, I've generalized some components and types to increase code reusability, renamed some helpers as well as deleted some useless ones, and inserted the necessary new translations on the English i18n dictionary. + Demo gif: + ![Marketplace sort filter](https://user-images.githubusercontent.com/43561537/155033709-e07a6306-a85a-4f7f-9624-b53ba5dd7fa9.gif) + +- VoIP Support for Omnichannel ([#23102](https://github.com/RocketChat/Rocket.Chat/pull/23102)) + + - Created VoipService to manage VoIP connections and PBX connection + - Created LivechatVoipService that will handle custom cases for livechat (creating rooms, assigning chats to queue, actions when call is finished, etc) + - Created Basic interfaces to support new services and new model + - Created Endpoints for management interfaces + - Implemented asterisk connector on VoIP service + - Created UI components to show calls incoming and to allow answering/rejecting calls + - Added new settings to control call server/management server connection values + - Added endpoints to associate Omnichannel Agents with PBX Extensions + - Added support for event listening on server side, to get metadata about calls being received/ongoing + - Created new pages to update settings & to see user-extension association + - Created new page to see ongoing calls (and past calls) + - Added support for remote hangup/hold on calls + - Implemented call metrics calculation (hold time, waiting time, talk time) + - Show a notificaiton when call is received + +### 🚀 Improvements + + +- **ENTERPRISE:** Improve how micro services are loaded ([#24388](https://github.com/RocketChat/Rocket.Chat/pull/24388)) + +- Add return button in chats opened from the list of current chats ([#24458](https://github.com/RocketChat/Rocket.Chat/pull/24458) by [@LucasFASouza](https://github.com/LucasFASouza)) + + The new return button for Omnichannel chats came out with release 3.15 but the feature was only available for chats that were opened from Omnichannel Contact Center. + Now, the same UI/UX is supported for chats opened from Current Chats list. + + ![image](https://user-images.githubusercontent.com/32396925/153283190-bd5c9748-c36b-4874-a704-6043afc7e3a1.png) + + The chat now opens in the Omnichannel settings and has the return button so the user can go back to the Current Chats list. + + ![image](https://user-images.githubusercontent.com/32396925/153285591-fad8e4a0-d2ea-4a02-8b2a-15e383b3c876.png) + +- Add tooltips on action buttons of Canned Response message composer ([#24483](https://github.com/RocketChat/Rocket.Chat/pull/24483) by [@LucasFASouza](https://github.com/LucasFASouza)) + + The tooltips were missing on the action buttons of CR message composer. + + ![image](https://user-images.githubusercontent.com/32396925/153620327-91107245-4b47-4d39-a99a-6da6d1cf5734.png) + + Users can now feel more encouraged to use these actions knowing what they are supposed to do. + +- Add user to room on "Click to Join!" button press ([#24041](https://github.com/RocketChat/Rocket.Chat/pull/24041) by [@ostjen](https://github.com/ostjen)) + + - Add user to room on "Click to Join!" button press; + - Display the "Join" button in discussions inside channels (keeping the behavior consistent with discussions inside groups). + +- Added a new "All" tab which shows all integrations in Integrations ([#24109](https://github.com/RocketChat/Rocket.Chat/pull/24109) by [@aswinidev](https://github.com/aswinidev)) + +- ChatBox Text to File Description ([#24451](https://github.com/RocketChat/Rocket.Chat/pull/24451) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + + The text content from chatbox goes to the file description when drag and drop a file. + +- Close modal on esc and outside click ([#24275](https://github.com/RocketChat/Rocket.Chat/pull/24275)) + + This is a QUICK change in order to close modals pressing Esc button and clicking outside of it **intentionally**. + +- CloudLoginModal visual consistency ([#24334](https://github.com/RocketChat/Rocket.Chat/pull/24334)) + + ### before + ![image](https://user-images.githubusercontent.com/27704687/151585064-dc6a1e29-9903-4241-8fbd-dfbe6c55fbef.png) + + ### after + ![Screen Shot 2022-01-28 at 13 32 02](https://user-images.githubusercontent.com/27704687/151585101-75b98502-9aae-4198-bc3e-4956750e5d8b.png) + +- Convert tag edit with department data to tsx ([#24369](https://github.com/RocketChat/Rocket.Chat/pull/24369) by [@LucasFASouza](https://github.com/LucasFASouza)) + +- Descriptive tooltip for Encrypted Key on Room Header ([#24121](https://github.com/RocketChat/Rocket.Chat/pull/24121)) + +- OTR system messages ([#24382](https://github.com/RocketChat/Rocket.Chat/pull/24382)) + + OTR system messages to indicate key refresh and joining chat to users. + +- Purchase Type Filter for marketplace apps and Categories filter anchor refactoring ([#24454](https://github.com/RocketChat/Rocket.Chat/pull/24454)) + + Implemented a filter by purchase type(free or paid) component for the apps screen of the marketplace. Besides that, new entries on the dictionary, fixed some parts of the App type (purchaseType was typed as unknown and price as string), and created some helpers to work alongside the filter. Will be refactoring the categories filter anchor and then will open this PR for reviews. + + Demo gif: + ![purchaseTypeFIlter](https://user-images.githubusercontent.com/43561537/153101228-7b7ebdc3-2d34-420f-aa9d-f7cbc8d4b53f.gif) + + Refactored the categories filter anchor from a plain fuselage select to a select button with dynamic colors. + Demo gif: + ![New categories filter anchor(PR)](https://user-images.githubusercontent.com/43561537/153422427-28012b7d-e0ec-45f4-861d-c9368c57ad04.gif) + +- Replace AutoComplete in UserAutoComplete & UserAutoCompleteMultiple components ([#24529](https://github.com/RocketChat/Rocket.Chat/pull/24529)) + + This PR replaces a deprecated fuselage's component `AutoComplete` in favor of `Select` and `MultiSelect` which fixes some of UX/UI issues in selecting users + + ### before + ![Screen Shot 2022-02-19 at 13 33 28](https://user-images.githubusercontent.com/27704687/154809737-8181a06c-4f20-48ea-90f7-01e828b9a452.png) + + ### after + ![Screen Shot 2022-02-19 at 13 30 58](https://user-images.githubusercontent.com/27704687/154809653-a8ec9a80-c0dd-4a25-9c00-0f96147d79e9.png) + +- Skip encryption for slash commands in E2E rooms ([#24475](https://github.com/RocketChat/Rocket.Chat/pull/24475)) + + Currently Slash Commands don't work in an E2EE room, as we encrypt the message before slash command is detected by the server, So removed encryption for slash commands in e2e rooms. + +- Team system messages feedback ([#24209](https://github.com/RocketChat/Rocket.Chat/pull/24209) by [@ostjen](https://github.com/ostjen)) + + - Delete some keys that aren't being used (eg: User_left_female). + - Add new Teams' system messages: + - `added-user-to-team`: **added** @\user to this Team; + - `removed-user-from-team`: **removed** @\user from this Team; + - `user-converted-to-team`: **converted** #\room to a Team; + - `user-converted-to-channel`: **converted** #\room to a Channel; + - `user-removed-room-from-team`: **removed** @\user from this Team; + - `user-deleted-room-from-team`: **deleted** #\room from this Team; + - `user-added-room-to-team`: **deleted** #\room to this Team; + - Add the corresponding options to hide each new system message and the missing `ujt` and `ult` hide options. + +### 🐛 Bug fixes + + +- 2FA via email when logging in using OAuth ([#24572](https://github.com/RocketChat/Rocket.Chat/pull/24572)) + +- Add ?close to OAuth callback url ([#24381](https://github.com/RocketChat/Rocket.Chat/pull/24381)) + +- GDPR action to forget visitor data on request ([#24441](https://github.com/RocketChat/Rocket.Chat/pull/24441)) + +- Implement client errors on ddp-streamer ([#24310](https://github.com/RocketChat/Rocket.Chat/pull/24310)) + +- Inconsistent validation of user's access to rooms ([#24037](https://github.com/RocketChat/Rocket.Chat/pull/24037) by [@ostjen](https://github.com/ostjen)) + +- Issues on selecting users when importing CSV ([#24253](https://github.com/RocketChat/Rocket.Chat/pull/24253)) + + * Fix users selecting by fixing their _id + * Add condition to disable 'Start importing' button if `usersCount`, `channelsCount` and `messageCount` equals 0, or if messageCount is alone + * Remove `disabled={usersCount === 0}` on user Tab + +- OAuth mismatch redirect_uri error ([#24450](https://github.com/RocketChat/Rocket.Chat/pull/24450)) + +- Oembed request not respecting payload limit ([#24418](https://github.com/RocketChat/Rocket.Chat/pull/24418)) + +- Omnichannel managers can't join chats in progress ([#24553](https://github.com/RocketChat/Rocket.Chat/pull/24553)) + +- Outgoing webhook without scripts not saving messages ([#24401](https://github.com/RocketChat/Rocket.Chat/pull/24401)) + +- Prevent Apps Bridge to remove visitor status from room ([#24305](https://github.com/RocketChat/Rocket.Chat/pull/24305)) + +- Read receipts showing first messages of the room as read even if not read by everyone ([#24508](https://github.com/RocketChat/Rocket.Chat/pull/24508)) + +- respect `Accounts_Registration_Users_Default_Roles` setting ([#24173](https://github.com/RocketChat/Rocket.Chat/pull/24173)) + + - Fix `user` role being added as default regardless of the `Accounts_Registration_Users_Default_Roles` setting. + +- Room context tabs not working in Omnichannel current chats page ([#24559](https://github.com/RocketChat/Rocket.Chat/pull/24559)) + +- Skip admin info in setup wizard for servers with admin registered ([#24485](https://github.com/RocketChat/Rocket.Chat/pull/24485)) + +- Skip cloud steps for registered servers on setup wizard ([#24407](https://github.com/RocketChat/Rocket.Chat/pull/24407)) + +- Slash commands previews not working ([#24387](https://github.com/RocketChat/Rocket.Chat/pull/24387) by [@ostjen](https://github.com/ostjen)) + +- Startup errors creating indexes ([#24409](https://github.com/RocketChat/Rocket.Chat/pull/24409)) + + Fix `bio` and `prid` startup index creation errors. + +- typo on register server tooltip of setup wizard ([#24466](https://github.com/RocketChat/Rocket.Chat/pull/24466)) + +
+🔍 Minor changes + + +- Bump @types/ws from 8.2.2 to 8.2.3 in /ee/server/services ([#24556](https://github.com/RocketChat/Rocket.Chat/pull/24556) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump adm-zip from 0.4.14 to 0.5.9 ([#24538](https://github.com/RocketChat/Rocket.Chat/pull/24538) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump body-parser from 1.19.0 to 1.19.1 in /ee/server/services ([#23963](https://github.com/RocketChat/Rocket.Chat/pull/23963) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump body-parser from 1.19.1 to 1.19.2 in /ee/server/services ([#24517](https://github.com/RocketChat/Rocket.Chat/pull/24517) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump cookie from 0.4.1 to 0.4.2 in /ee/server/services ([#24472](https://github.com/RocketChat/Rocket.Chat/pull/24472) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump date-fns from 2.24.0 to 2.28.0 ([#24058](https://github.com/RocketChat/Rocket.Chat/pull/24058) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump express from 4.17.1 to 4.17.2 in /ee/server/services ([#24469](https://github.com/RocketChat/Rocket.Chat/pull/24469) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump express from 4.17.2 to 4.17.3 in /ee/server/services ([#24522](https://github.com/RocketChat/Rocket.Chat/pull/24522) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump follow-redirects from 1.14.7 to 1.14.8 in /ee/server/services ([#24491](https://github.com/RocketChat/Rocket.Chat/pull/24491) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump jaeger-client from 3.18.1 to 3.19.0 in /ee/server/services ([#23961](https://github.com/RocketChat/Rocket.Chat/pull/23961) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump pm2 from 5.1.2 to 5.2.0 in /ee/server/services ([#24537](https://github.com/RocketChat/Rocket.Chat/pull/24537) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump simple-get from 4.0.0 to 4.0.1 ([#24341](https://github.com/RocketChat/Rocket.Chat/pull/24341) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump sodium-native from 3.2.1 to 3.3.0 in /ee/server/services ([#23512](https://github.com/RocketChat/Rocket.Chat/pull/23512) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump underscore.string from 3.3.5 to 3.3.6 in /ee/server/services ([#24498](https://github.com/RocketChat/Rocket.Chat/pull/24498) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump url-parse from 1.5.3 to 1.5.7 ([#24528](https://github.com/RocketChat/Rocket.Chat/pull/24528) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump vm2 from 3.9.5 to 3.9.7 in /ee/server/services ([#24509](https://github.com/RocketChat/Rocket.Chat/pull/24509) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Chore: `twoFactorRequired` signature ([#24518](https://github.com/RocketChat/Rocket.Chat/pull/24518)) + + Improved type checking for decorator `twoFactorRequired`. + +- Chore: Add description to global OTR setting ([#24333](https://github.com/RocketChat/Rocket.Chat/pull/24333) by [@pedrogssouza](https://github.com/pedrogssouza)) + +- Chore: Bump Fuselage packages ([#24573](https://github.com/RocketChat/Rocket.Chat/pull/24573)) + + It uses the last stable version of Fuselage packages. + +- Chore: bump fuselage version ([#24453](https://github.com/RocketChat/Rocket.Chat/pull/24453)) + +- Chore: Convert JS files to Typescript ([#24410](https://github.com/RocketChat/Rocket.Chat/pull/24410)) + + This pull request converts 26 more files from Javascript to Typescript, to check variable types and increase validation on the code. + +- Chore: Convert to typescript the me slashCommands files ([#24321](https://github.com/RocketChat/Rocket.Chat/pull/24321) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + + Convert to typescript the me slashCommands files + +- Chore: Convert to typescript the mute and unmute slash commands files ([#24325](https://github.com/RocketChat/Rocket.Chat/pull/24325) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + + Convert to typescript the mute and unmute slash commands files + +- Chore: Convert to typescript the slash commands create files ([#24306](https://github.com/RocketChat/Rocket.Chat/pull/24306) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + + Convert Slash Commands create files to typescript. + +- Chore: Convert to typescript the slash commands invite files ([#24311](https://github.com/RocketChat/Rocket.Chat/pull/24311) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + + Convert to typescript the slash commands invite files + +- Chore: Convert to typescript the unarchive slash commands files ([#24331](https://github.com/RocketChat/Rocket.Chat/pull/24331) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + + Convert to typescript the unarchive slash commands files + +- Chore: Delete unused file (NewAdminInfoPage.js) ([#24196](https://github.com/RocketChat/Rocket.Chat/pull/24196)) + + Just removing a duplicated/unused file. + +- Chore: Improve PR title validation regex ([#24467](https://github.com/RocketChat/Rocket.Chat/pull/24467)) + +- Chore: Js to ts slash commands archive ([#24304](https://github.com/RocketChat/Rocket.Chat/pull/24304) by [@eduardofcabrera](https://github.com/eduardofcabrera)) + + Convert Slash Commands archive files to typescript + +- Chore: Remove storybook build job from CI ([#24530](https://github.com/RocketChat/Rocket.Chat/pull/24530)) + +- Chore: roomTypes: Stop mixing client and server code together ([#24536](https://github.com/RocketChat/Rocket.Chat/pull/24536)) + +- Chore: Run tests using microservices deployment on CI ([#24513](https://github.com/RocketChat/Rocket.Chat/pull/24513)) + +- Chore: Set Docker image tag to latest only when really latest ([#24366](https://github.com/RocketChat/Rocket.Chat/pull/24366)) + +- Chore: Unify ILivechatAgent with ILivechatAgentRecord ([#24406](https://github.com/RocketChat/Rocket.Chat/pull/24406)) + +- Chore: Update Apps-Engine ([#24568](https://github.com/RocketChat/Rocket.Chat/pull/24568)) + +- Chore: Update Apps-Engine ([#24651](https://github.com/RocketChat/Rocket.Chat/pull/24651)) + +- Chore: Update fuselage deps to match monolith versions ([#24501](https://github.com/RocketChat/Rocket.Chat/pull/24501)) + +- Chore: Update Meteor to 2.5.6 ([#24461](https://github.com/RocketChat/Rocket.Chat/pull/24461)) + +- Chore: Update ws package ([#24477](https://github.com/RocketChat/Rocket.Chat/pull/24477)) + +- Chore(deps-dev): Bump ts-node from 10.0.0 to 10.5.0 in /ee/server/services ([#24435](https://github.com/RocketChat/Rocket.Chat/pull/24435) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Chore(deps): Bump node-fetch from 2.6.1 to 2.6.7 in /ee/server/services ([#24299](https://github.com/RocketChat/Rocket.Chat/pull/24299) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- i18n: Language update from LingoHub 🤖 on 2022-01-31Z ([#24357](https://github.com/RocketChat/Rocket.Chat/pull/24357)) + +- i18n: Language update from LingoHub 🤖 on 2022-02-07Z ([#24429](https://github.com/RocketChat/Rocket.Chat/pull/24429)) + +- i18n: Language update from LingoHub 🤖 on 2022-02-14Z ([#24493](https://github.com/RocketChat/Rocket.Chat/pull/24493)) + +- i18n: Language update from LingoHub 🤖 on 2022-02-21Z ([#24558](https://github.com/RocketChat/Rocket.Chat/pull/24558)) + +- Merge master into develop & Set version to 4.5.0-develop ([#24363](https://github.com/RocketChat/Rocket.Chat/pull/24363)) + +- Regression: Add support to namespace within micro services ([#24581](https://github.com/RocketChat/Rocket.Chat/pull/24581)) + +- Regression: Admin Sidebar colors inverted. ([#24609](https://github.com/RocketChat/Rocket.Chat/pull/24609)) + +- Regression: Bunch of settings fixes for VoIP ([#24594](https://github.com/RocketChat/Rocket.Chat/pull/24594)) + +- Regression: Do not show toast on incoming voip calls ([#24619](https://github.com/RocketChat/Rocket.Chat/pull/24619)) + +- Regression: Encode registration info as JWT when signing key is provided ([#24626](https://github.com/RocketChat/Rocket.Chat/pull/24626)) + +- Regression: Error setting user avatars and mentioning rooms on Slack Import ([#24585](https://github.com/RocketChat/Rocket.Chat/pull/24585)) + + - Fix `Mentioned room not found` error when importing rooms from Slack; + - Fix `Forbidden` error when setting avatars for users imported from Slack (on user import/creation); + - Fix incorrect message count on imported rooms; + - Fix missing username on messages imported from Slack; + +- Regression: Error when trying to load name of dm rooms for avatars and notifications ([#24583](https://github.com/RocketChat/Rocket.Chat/pull/24583)) + +- Regression: Extension List panel UI not aligned with designs ([#24645](https://github.com/RocketChat/Rocket.Chat/pull/24645)) + +- Regression: Fix double value on holdTime and empty msg on last message ([#24630](https://github.com/RocketChat/Rocket.Chat/pull/24630)) + +- Regression: Fix in-correct room status shown to agents ([#24592](https://github.com/RocketChat/Rocket.Chat/pull/24592)) + +- Regression: Fix incoming voip call ringtone is not ringing ([#24616](https://github.com/RocketChat/Rocket.Chat/pull/24616)) + +- Regression: Fix room not getting created due to null visitor status ([#24562](https://github.com/RocketChat/Rocket.Chat/pull/24562)) + +- Regression: Fix time fields and wrap up in Voip Room Contexual bar ([#24625](https://github.com/RocketChat/Rocket.Chat/pull/24625)) + +- Regression: Fix time format on Voip system messages ([#24603](https://github.com/RocketChat/Rocket.Chat/pull/24603)) + +- Regression: Fix translation for call started message ([#24615](https://github.com/RocketChat/Rocket.Chat/pull/24615)) + +- Regression: Fix wrong tab name for VoIP settings ([#24647](https://github.com/RocketChat/Rocket.Chat/pull/24647)) + +- Regression: Fixes in Voice Contextual Bar and Directory ([#24596](https://github.com/RocketChat/Rocket.Chat/pull/24596)) + +- Regression: If Asterisk suddenly goes down, server has no way to know. Causes server to get stuck. Needs restart ([#24624](https://github.com/RocketChat/Rocket.Chat/pull/24624)) + +- Regression: Mark all rooms as read modal closing instantly. ([#24610](https://github.com/RocketChat/Rocket.Chat/pull/24610)) + +- Regression: No audio when call comes from Skype/IP phone ([#24602](https://github.com/RocketChat/Rocket.Chat/pull/24602)) + + The audio was not rendered because of re-rendering of react element based on + queueCounter and roomInfo. queueCounter and roomInfo cause the dom to re-render when call gets accepted + because after accepting call, queueCounter changes or a room gets created. + The audio element gets recreated. But VoIP user probably holds the old one. + The behaviour is not predictable when such case happens. If everything gets cleanly setup, + even if the audio element goes headless, it still continues to play the remote audio. + But in other cases, it is unreferenced the one on dom has its srcObject as null. + This causes no audio. + + This fix provides a way to re-initialise the rendering elements in VoIP user + and calls this function on useEffect() if the re-render has happen. + +- Regression: Prevent button from losing state when rerendering ([#24648](https://github.com/RocketChat/Rocket.Chat/pull/24648)) + +- Regression: Prevent connect to asterisk when VoIP is disabled ([#24601](https://github.com/RocketChat/Rocket.Chat/pull/24601)) + +- Regression: Queue counter aggregator for incoming/hanged calls ([#24635](https://github.com/RocketChat/Rocket.Chat/pull/24635)) + +- Regression: Refresh server connection when MI server settings change ([#24649](https://github.com/RocketChat/Rocket.Chat/pull/24649)) + +- Regression: Server crashing if Voip credentials are invalid ([#24646](https://github.com/RocketChat/Rocket.Chat/pull/24646)) + +- Regression: VoIP service button displayed when VoIP is disabled ([#24598](https://github.com/RocketChat/Rocket.Chat/pull/24598)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@LucasFASouza](https://github.com/LucasFASouza) +- [@aswinidev](https://github.com/aswinidev) +- [@dependabot[bot]](https://github.com/dependabot[bot]) +- [@eduardofcabrera](https://github.com/eduardofcabrera) +- [@ostjen](https://github.com/ostjen) +- [@pedrogssouza](https://github.com/pedrogssouza) +- [@ujorgeleite](https://github.com/ujorgeleite) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@albuquerquefabio](https://github.com/albuquerquefabio) +- [@amolghode1981](https://github.com/amolghode1981) +- [@d-gubert](https://github.com/d-gubert) +- [@debdutdeb](https://github.com/debdutdeb) +- [@dougfabris](https://github.com/dougfabris) +- [@felipe-rod123](https://github.com/felipe-rod123) +- [@filipemarins](https://github.com/filipemarins) +- [@gabriellsh](https://github.com/gabriellsh) +- [@ggazzo](https://github.com/ggazzo) +- [@guijun13](https://github.com/guijun13) +- [@juliajforesti](https://github.com/juliajforesti) +- [@matheusbsilva137](https://github.com/matheusbsilva137) +- [@murtaza98](https://github.com/murtaza98) +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@renatobecker](https://github.com/renatobecker) +- [@rique223](https://github.com/rique223) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) +- [@tiagoevanp](https://github.com/tiagoevanp) +- [@yash-rajpal](https://github.com/yash-rajpal) + +# 4.4.5 +`2022-05-30 · 12 🎉 · 26 🚀 · 79 🐛 · 213 🔍 · 54 👩‍💻👨‍💻` + +### Engine versions +- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` + +### 🎉 New features + + +- Add expire index to integration history ([#25087](https://github.com/RocketChat/Rocket.Chat/pull/25087)) + +- Alpha Matrix Federation ([#23688](https://github.com/RocketChat/Rocket.Chat/pull/23688)) + + Experimental support for Matrix Federation with a Bridge + + https://user-images.githubusercontent.com/51996/164530391-e8b17ecd-a4d0-4ef8-a8b7-81230c1773d3.mp4 + +- E2E password generator ([#24114](https://github.com/RocketChat/Rocket.Chat/pull/24114) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + +- Engagement Statistics ([#24989](https://github.com/RocketChat/Rocket.Chat/pull/24989)) + +- Engagement Statistics ([#24777](https://github.com/RocketChat/Rocket.Chat/pull/24777) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + +- Expand Apps Engine's environment variable allowed list ([#23870](https://github.com/RocketChat/Rocket.Chat/pull/23870) by [@cuonghuunguyen](https://github.com/cuonghuunguyen)) + +- Marketplace sort filter ([#24567](https://github.com/RocketChat/Rocket.Chat/pull/24567) by [@ujorgeleite](https://github.com/ujorgeleite)) + + Implemented a sort filter for the marketplace screen. This component sorts the marketplace apps list in 4 ways, alphabetical order(A-Z), inverse alphabetical order(Z-A), most recently updated(MRU), and least recent updated(LRU). Besides that, I've generalized some components and types to increase code reusability, renamed some helpers as well as deleted some useless ones, and inserted the necessary new translations on the English i18n dictionary. + Demo gif: + ![Marketplace sort filter](https://user-images.githubusercontent.com/43561537/155033709-e07a6306-a85a-4f7f-9624-b53ba5dd7fa9.gif) + +- Message Template React Component ([#23971](https://github.com/RocketChat/Rocket.Chat/pull/23971)) + + Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template). + + + + ![Screen Shot 2022-04-05 at 11 14 18](https://user-images.githubusercontent.com/27704687/161774027-38dd9c7b-eeeb-45e2-b9d8-ea2a9be8486d.png) + In case you encounter any problems, or want to compare, temporarily it is possible to use the old version + + image + +- Telemetry Events ([#24781](https://github.com/RocketChat/Rocket.Chat/pull/24781) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + +- Upgrade Tab ([#24835](https://github.com/RocketChat/Rocket.Chat/pull/24835)) + + ![image](https://user-images.githubusercontent.com/27704687/160172260-c656282e-a487-4092-948d-d11c9bacb598.png) + +- Use setting to determine if initial general channel is needed ([#25441](https://github.com/RocketChat/Rocket.Chat/pull/25441) by [@felipe-menelau](https://github.com/felipe-menelau)) + + - Adds flag responsible for overwriting #general channel creation + +- VoIP Support for Omnichannel ([#23102](https://github.com/RocketChat/Rocket.Chat/pull/23102)) + + - Created VoipService to manage VoIP connections and PBX connection + - Created LivechatVoipService that will handle custom cases for livechat (creating rooms, assigning chats to queue, actions when call is finished, etc) + - Created Basic interfaces to support new services and new model + - Created Endpoints for management interfaces + - Implemented asterisk connector on VoIP service + - Created UI components to show calls incoming and to allow answering/rejecting calls + - Added new settings to control call server/management server connection values + - Added endpoints to associate Omnichannel Agents with PBX Extensions + - Added support for event listening on server side, to get metadata about calls being received/ongoing + - Created new pages to update settings & to see user-extension association + - Created new page to see ongoing calls (and past calls) + - Added support for remote hangup/hold on calls + - Implemented call metrics calculation (hold time, waiting time, talk time) + - Show a notificaiton when call is received + +### 🚀 Improvements + + +- **ENTERPRISE:** Don't start presence monitor when running micro services ([#24739](https://github.com/RocketChat/Rocket.Chat/pull/24739)) + +- **ENTERPRISE:** Improve how micro services are loaded ([#24388](https://github.com/RocketChat/Rocket.Chat/pull/24388)) + +- Add OTR Room States ([#24565](https://github.com/RocketChat/Rocket.Chat/pull/24565)) + + Earlier OTR room uses only 2 states, we need more states to support future features. + This adds more states for the OTR contextualBar. + + - Expired + Screen Shot 2022-04-20 at 13 55 52 + + - Declined + Screen Shot 2022-04-20 at 13 49 28 + + - Error + Screen Shot 2022-04-20 at 13 55 26 + +- Add return button in chats opened from the list of current chats ([#24458](https://github.com/RocketChat/Rocket.Chat/pull/24458) by [@LucasFASouza](https://github.com/LucasFASouza)) + + The new return button for Omnichannel chats came out with release 3.15 but the feature was only available for chats that were opened from Omnichannel Contact Center. + Now, the same UI/UX is supported for chats opened from Current Chats list. + + ![image](https://user-images.githubusercontent.com/32396925/153283190-bd5c9748-c36b-4874-a704-6043afc7e3a1.png) + + The chat now opens in the Omnichannel settings and has the return button so the user can go back to the Current Chats list. + + ![image](https://user-images.githubusercontent.com/32396925/153285591-fad8e4a0-d2ea-4a02-8b2a-15e383b3c876.png) + +- Add tooltip to sidebar room menu ([#24405](https://github.com/RocketChat/Rocket.Chat/pull/24405) by [@Himanshu664](https://github.com/Himanshu664)) + +- Add tooltips on action buttons of Canned Response message composer ([#24483](https://github.com/RocketChat/Rocket.Chat/pull/24483) by [@LucasFASouza](https://github.com/LucasFASouza)) + + The tooltips were missing on the action buttons of CR message composer. + + ![image](https://user-images.githubusercontent.com/32396925/153620327-91107245-4b47-4d39-a99a-6da6d1cf5734.png) + + Users can now feel more encouraged to use these actions knowing what they are supposed to do. + +- Add user to room on "Click to Join!" button press ([#24041](https://github.com/RocketChat/Rocket.Chat/pull/24041) by [@ostjen](https://github.com/ostjen)) + + - Add user to room on "Click to Join!" button press; + - Display the "Join" button in discussions inside channels (keeping the behavior consistent with discussions inside groups). + +- Added a new "All" tab which shows all integrations in Integrations ([#24109](https://github.com/RocketChat/Rocket.Chat/pull/24109) by [@aswinidev](https://github.com/aswinidev)) + +- Added MaxNickNameLength and MaxBioLength constants ([#25231](https://github.com/RocketChat/Rocket.Chat/pull/25231) by [@aakash-gitdev](https://github.com/aakash-gitdev)) + +- Added tooltip options for message menu ([#24431](https://github.com/RocketChat/Rocket.Chat/pull/24431) by [@Himanshu664](https://github.com/Himanshu664)) + +- Adding new statistics related to voip and omnichannel ([#24887](https://github.com/RocketChat/Rocket.Chat/pull/24887)) + + - Total of Canned response messages sent + - Total of tags used + - Last-Chatted Agent Preferred (enabled/disabled) + - Assign new conversations to the contact manager (enabled/disabled) + - How to handle Visitor Abandonment setting + - Amount of chats placed on hold + - VoIP Enabled + - Amount of VoIP Calls + - Amount of VoIP Extensions connected + - Amount of Calls placed on hold (1x per call) + - Fixed Session Aggregation type definitions + +- ChatBox Text to File Description ([#24451](https://github.com/RocketChat/Rocket.Chat/pull/24451) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + + The text content from chatbox goes to the file description when drag and drop a file. + +- Close modal on esc and outside click ([#24275](https://github.com/RocketChat/Rocket.Chat/pull/24275)) + + This is a QUICK change in order to close modals pressing Esc button and clicking outside of it **intentionally**. + +- CloudLoginModal visual consistency ([#24334](https://github.com/RocketChat/Rocket.Chat/pull/24334)) + + ### before + ![image](https://user-images.githubusercontent.com/27704687/151585064-dc6a1e29-9903-4241-8fbd-dfbe6c55fbef.png) + + ### after + ![Screen Shot 2022-01-28 at 13 32 02](https://user-images.githubusercontent.com/27704687/151585101-75b98502-9aae-4198-bc3e-4956750e5d8b.png) + +- Convert tag edit with department data to tsx ([#24369](https://github.com/RocketChat/Rocket.Chat/pull/24369) by [@LucasFASouza](https://github.com/LucasFASouza)) + +- Descriptive tooltip for Encrypted Key on Room Header ([#24121](https://github.com/RocketChat/Rocket.Chat/pull/24121)) + +- Improve active/hover colors in account sidebar ([#25024](https://github.com/RocketChat/Rocket.Chat/pull/25024) by [@Himanshu664](https://github.com/Himanshu664)) + +- New omnichannel statistics and async statistics processing. ([#24749](https://github.com/RocketChat/Rocket.Chat/pull/24749)) + + https://app.clickup.com/t/1z4zg4e + +- OTR system messages ([#24382](https://github.com/RocketChat/Rocket.Chat/pull/24382)) + + OTR system messages to indicate key refresh and joining chat to users. + +- Performance for some Omnichannel features ([#25217](https://github.com/RocketChat/Rocket.Chat/pull/25217)) + +- Purchase Type Filter for marketplace apps and Categories filter anchor refactoring ([#24454](https://github.com/RocketChat/Rocket.Chat/pull/24454)) + + Implemented a filter by purchase type(free or paid) component for the apps screen of the marketplace. Besides that, new entries on the dictionary, fixed some parts of the App type (purchaseType was typed as unknown and price as string), and created some helpers to work alongside the filter. Will be refactoring the categories filter anchor and then will open this PR for reviews. + + Demo gif: + ![purchaseTypeFIlter](https://user-images.githubusercontent.com/43561537/153101228-7b7ebdc3-2d34-420f-aa9d-f7cbc8d4b53f.gif) + + Refactored the categories filter anchor from a plain fuselage select to a select button with dynamic colors. + Demo gif: + ![New categories filter anchor(PR)](https://user-images.githubusercontent.com/43561537/153422427-28012b7d-e0ec-45f4-861d-c9368c57ad04.gif) + +- Rename upgrade tab routes ([#25097](https://github.com/RocketChat/Rocket.Chat/pull/25097)) + + Change 'upgrade tab' routes names from camelCase ('goFullyFeatured') to kebab-case ('go-fully-featured') due to URL naming consistency. Changed types, main function and test. + +- Replace AutoComplete in UserAutoComplete & UserAutoCompleteMultiple components ([#24529](https://github.com/RocketChat/Rocket.Chat/pull/24529)) + + This PR replaces a deprecated fuselage's component `AutoComplete` in favor of `Select` and `MultiSelect` which fixes some of UX/UI issues in selecting users + + ### before + ![Screen Shot 2022-02-19 at 13 33 28](https://user-images.githubusercontent.com/27704687/154809737-8181a06c-4f20-48ea-90f7-01e828b9a452.png) + + ### after + ![Screen Shot 2022-02-19 at 13 30 58](https://user-images.githubusercontent.com/27704687/154809653-a8ec9a80-c0dd-4a25-9c00-0f96147d79e9.png) + +- Skip encryption for slash commands in E2E rooms ([#24475](https://github.com/RocketChat/Rocket.Chat/pull/24475)) + + Currently Slash Commands don't work in an E2EE room, as we encrypt the message before slash command is detected by the server, So removed encryption for slash commands in e2e rooms. + +- Team system messages feedback ([#24209](https://github.com/RocketChat/Rocket.Chat/pull/24209) by [@ostjen](https://github.com/ostjen)) + + - Delete some keys that aren't being used (eg: User_left_female). + - Add new Teams' system messages: + - `added-user-to-team`: **added** @\user to this Team; + - `removed-user-from-team`: **removed** @\user from this Team; + - `user-converted-to-team`: **converted** #\room to a Team; + - `user-converted-to-channel`: **converted** #\room to a Channel; + - `user-removed-room-from-team`: **removed** @\user from this Team; + - `user-deleted-room-from-team`: **deleted** #\room from this Team; + - `user-added-room-to-team`: **deleted** #\room to this Team; + - Add the corresponding options to hide each new system message and the missing `ujt` and `ult` hide options. + +- Updated links in readme ([#24028](https://github.com/RocketChat/Rocket.Chat/pull/24028) by [@aswinidev](https://github.com/aswinidev)) + +### 🐛 Bug fixes + + +- "Match error" when converting a team to a channel ([#24629](https://github.com/RocketChat/Rocket.Chat/pull/24629)) + + - Fix "Match error" when trying to convert a channel to a team; + +- **ENTERPRISE:** Auto reload feature of ddp-streamer micro service ([#24793](https://github.com/RocketChat/Rocket.Chat/pull/24793)) + +- **ENTERPRISE:** DDP streamer not sending data to all clients ([#24738](https://github.com/RocketChat/Rocket.Chat/pull/24738)) + +- **ENTERPRISE:** Notifications not being sent by ddp-streamer ([#24831](https://github.com/RocketChat/Rocket.Chat/pull/24831)) + +- **ENTERPRISE:** Presence micro service logic ([#24724](https://github.com/RocketChat/Rocket.Chat/pull/24724)) + +- 2FA via email when logging in using OAuth ([#24572](https://github.com/RocketChat/Rocket.Chat/pull/24572)) + +- Add ?close to OAuth callback url ([#24381](https://github.com/RocketChat/Rocket.Chat/pull/24381)) + +- Add katex render to new message react template ([#25239](https://github.com/RocketChat/Rocket.Chat/pull/25239)) + +- Add reaction not working in legacy messages ([#25222](https://github.com/RocketChat/Rocket.Chat/pull/25222)) + +- Added invalid password error message ([#24714](https://github.com/RocketChat/Rocket.Chat/pull/24714) by [@Himanshu664](https://github.com/Himanshu664)) + +- Adjust email label in Setup Wizard i18n files ([#25260](https://github.com/RocketChat/Rocket.Chat/pull/25260)) + + - remove 'Company' label on onboarding email keys in certain languages + +- AgentOverview analytics wrong departmentId parameter ([#25073](https://github.com/RocketChat/Rocket.Chat/pull/25073) by [@paulobernardoaf](https://github.com/paulobernardoaf)) + + When filtering the analytics charts by department, data would not appear because the object: + ```js + { + value: "department-id", + label: "department-name" + } + ``` + was being used in the `departmentId` parameter. + + - Before: + ![image](https://user-images.githubusercontent.com/30026625/161832057-d96ffd21-a7dd-421e-bfaa-3b9f4a9127b2.png) + + - After: + ![image](https://user-images.githubusercontent.com/30026625/161831092-9ee77b51-b083-4f45-9c48-ab2e0511c4d6.png) + +- API Error preventing adding an email to users without one (like bot/app users) ([#24709](https://github.com/RocketChat/Rocket.Chat/pull/24709)) + +- Apple OAuth ([#24879](https://github.com/RocketChat/Rocket.Chat/pull/24879)) + +- auto-join team channels not honoring user preferences ([#24779](https://github.com/RocketChat/Rocket.Chat/pull/24779) by [@ostjen](https://github.com/ostjen)) + +- Client disconnection on network loss ([#25170](https://github.com/RocketChat/Rocket.Chat/pull/25170)) + + Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online + unless agent explicitly logs off. + Agent can stop receiving calls in multiple ways due to network loss. Network loss can happen in following ways. + 1. User tries to switch the network. User experiences a glitch of disconnectivity. This can be simulated by turning the network off + in the network tab of chrome's dev tool. This can disconnect the UA if the disconnection happens just before the registration refresh. + 2. Second reason is when computer goes in sleep mode. + 3. Third reason is that when asterisk is crashed/in maintenance mode/explicitly stopped. + + Solution: + The idea is to detect the network disconnection and start the start the attempts to reconnect. + The detection of the disconnection does not happen in case#1. The SIPUA's UserAgent transport does not + call onDisconnected when network loss of such kind happens. To tackle this problem, window's online and offline event handlers are + used. + + The number of retries is configurable but ideally it is to be kept at -1. Whenever disconnection happens, it should keep on trying to + reconnect with increasing backoff time. This behaviour is useful when the asterisk is stopped. + + When the server is disconnected, it should be indicated on the phone button. + +- Close room when dismiss wrap up call modal ([#25056](https://github.com/RocketChat/Rocket.Chat/pull/25056)) + +- Custom sound error toast messages ([#24515](https://github.com/RocketChat/Rocket.Chat/pull/24515) by [@Himanshu664](https://github.com/Himanshu664)) + +- Database indexes not being created ([#25101](https://github.com/RocketChat/Rocket.Chat/pull/25101)) + +- Date Message Export Filter Fix ([#24542](https://github.com/RocketChat/Rocket.Chat/pull/24542) by [@eduardofcabrera](https://github.com/eduardofcabrera)) + + Fix message export filter to get all messages between "from date" and "to date", including "to date". + +- DDP Rate Limiter Translation key ([#24898](https://github.com/RocketChat/Rocket.Chat/pull/24898)) + + Before: + image + + + Now: + image + +- DDP streamer errors ([#24710](https://github.com/RocketChat/Rocket.Chat/pull/24710)) + +- Duplicated "jump to message" button on starred messages ([#24867](https://github.com/RocketChat/Rocket.Chat/pull/24867) by [@Himanshu664](https://github.com/Himanshu664)) + +- Dynamic load matrix is enabled and handle failure ([#25495](https://github.com/RocketChat/Rocket.Chat/pull/25495)) + +- End call button disappearing when on-hold ([#24936](https://github.com/RocketChat/Rocket.Chat/pull/24936)) + +- External search providers not working ([#24860](https://github.com/RocketChat/Rocket.Chat/pull/24860) by [@tkurz](https://github.com/tkurz)) + +- Full error message is visible ([#24856](https://github.com/RocketChat/Rocket.Chat/pull/24856) by [@Himanshu664](https://github.com/Himanshu664)) + +- GDPR action to forget visitor data on request ([#24441](https://github.com/RocketChat/Rocket.Chat/pull/24441)) + +- German translation for Monitore ([#24785](https://github.com/RocketChat/Rocket.Chat/pull/24785) by [@JMoVS](https://github.com/JMoVS)) + +- Handle Other Formats inside Upload Avatar ([#24226](https://github.com/RocketChat/Rocket.Chat/pull/24226)) + + After resolving issue #24213 : + + + https://user-images.githubusercontent.com/53515714/150325012-91413025-786e-4ce0-ae75-629f6b05b024.mp4 + +- Ignore customClass on messages ([#24845](https://github.com/RocketChat/Rocket.Chat/pull/24845)) + +- Implement client errors on ddp-streamer ([#24310](https://github.com/RocketChat/Rocket.Chat/pull/24310)) + +- Inconsistent validation of user's access to rooms ([#24037](https://github.com/RocketChat/Rocket.Chat/pull/24037) by [@ostjen](https://github.com/ostjen)) + +- Incorrect websocket url in livechat widget ([#25261](https://github.com/RocketChat/Rocket.Chat/pull/25261)) + +- Initial User not added to default channel ([#25544](https://github.com/RocketChat/Rocket.Chat/pull/25544)) + + If injecting initial user. The user wasn’t added to the default General channel + +- Issues on selecting users when importing CSV ([#24253](https://github.com/RocketChat/Rocket.Chat/pull/24253)) + + * Fix users selecting by fixing their _id + * Add condition to disable 'Start importing' button if `usersCount`, `channelsCount` and `messageCount` equals 0, or if messageCount is alone + * Remove `disabled={usersCount === 0}` on user Tab + +- LDAP avatars being rotated according to metadata even if the setting to rotate uploads is off ([#24320](https://github.com/RocketChat/Rocket.Chat/pull/24320)) + + - Use the `FileUpload_RotateImages` setting (**Administration > File Upload > Rotate images on upload**) to control whether avatars should be rotated automatically based on their data (XEIF); + - Display the avatar image preview (orientation) according to the `FileUpload_RotateImages` setting. + +- LDAP sync removing users from channels when multiple groups are mapped to it ([#25434](https://github.com/RocketChat/Rocket.Chat/pull/25434)) + +- Message menu action not working on legacy messages. ([#25148](https://github.com/RocketChat/Rocket.Chat/pull/25148)) + +- Message preview not available for queued chats ([#25092](https://github.com/RocketChat/Rocket.Chat/pull/25092)) + +- Missing dependency on useEffect at CallProvider ([#24882](https://github.com/RocketChat/Rocket.Chat/pull/24882)) + +- Nextcloud OAuth for incomplete token URL ([#24476](https://github.com/RocketChat/Rocket.Chat/pull/24476)) + +- OAuth mismatch redirect_uri error ([#24450](https://github.com/RocketChat/Rocket.Chat/pull/24450)) + +- Oembed request not respecting payload limit ([#24418](https://github.com/RocketChat/Rocket.Chat/pull/24418)) + +- Omnichannel managers can't join chats in progress ([#24553](https://github.com/RocketChat/Rocket.Chat/pull/24553)) + +- One of the triggers was not working correctly ([#25409](https://github.com/RocketChat/Rocket.Chat/pull/25409)) + +- Outgoing webhook without scripts not saving messages ([#24401](https://github.com/RocketChat/Rocket.Chat/pull/24401)) + +- Prevent Apps Bridge to remove visitor status from room ([#24305](https://github.com/RocketChat/Rocket.Chat/pull/24305)) + +- Prevent call button toggle when user is on call ([#24758](https://github.com/RocketChat/Rocket.Chat/pull/24758)) + +- Prevent sequential messages edited icon to hide on hover ([#24984](https://github.com/RocketChat/Rocket.Chat/pull/24984)) + + ### before + Screen Shot 2022-03-29 at 13 35 56 + + ### after + Screen Shot 2022-03-29 at 11 48 05 + +- Prune Message issue ([#24424](https://github.com/RocketChat/Rocket.Chat/pull/24424)) + +- Push privacy config to not show username not being respected ([#24606](https://github.com/RocketChat/Rocket.Chat/pull/24606)) + +- Read receipts show with color gray when not read yet ([#25244](https://github.com/RocketChat/Rocket.Chat/pull/25244)) + +- Read receipts showing before message read ([#25216](https://github.com/RocketChat/Rocket.Chat/pull/25216)) + +- Read receipts showing first messages of the room as read even if not read by everyone ([#24508](https://github.com/RocketChat/Rocket.Chat/pull/24508)) + +- Register with Secret URL ([#24921](https://github.com/RocketChat/Rocket.Chat/pull/24921)) + +- Replace encrypted text to Encrypted Message Placeholder ([#24166](https://github.com/RocketChat/Rocket.Chat/pull/24166)) + + ### before + ![image](https://user-images.githubusercontent.com/27704687/150807900-154a9cdb-ee13-4333-8628-f287ab914b40.png) + + ### after + Screenshot 2022-01-13 at 8 57 47 PM + +- Reply button behavior on broadcast channel ([#25175](https://github.com/RocketChat/Rocket.Chat/pull/25175)) + + Hide reply button for the user that sent the message + +- respect `Accounts_Registration_Users_Default_Roles` setting ([#24173](https://github.com/RocketChat/Rocket.Chat/pull/24173)) + + - Fix `user` role being added as default regardless of the `Accounts_Registration_Users_Default_Roles` setting. + +- Room archived/unarchived system messages aren't sent when editing room settings ([#24897](https://github.com/RocketChat/Rocket.Chat/pull/24897)) + + - Send the "Room archived" and "Room unarchived" system messages when editing room settings (and not only when rooms are archived/unarchived with the slash-command); + - Fix the "Hide System Messages" option for the "Room archived" and "Room unarchived" system messages; + +- Room context tabs not working in Omnichannel current chats page ([#24559](https://github.com/RocketChat/Rocket.Chat/pull/24559)) + +- room creation fails if app framework is disabled ([#25200](https://github.com/RocketChat/Rocket.Chat/pull/25200)) + +- Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) + +- Several issues related to custom roles ([#24052](https://github.com/RocketChat/Rocket.Chat/pull/24052)) + + - Throw an error when trying to delete a role (User or Subscription role) that are still being used; + - Fix "Invalid Role" error for custom roles in Role Editing sidebar; + - Fix "Users in Role" screen for custom roles. + +- Showing Blank Message Inside Report ([#25007](https://github.com/RocketChat/Rocket.Chat/pull/25007)) + + https://user-images.githubusercontent.com/53515714/161038085-4a86c7ae-6751-4996-9767-b1c9e0331a6c.mp4 + +- Skip admin info in setup wizard for servers with admin registered ([#24485](https://github.com/RocketChat/Rocket.Chat/pull/24485)) + +- Skip cloud steps for registered servers on setup wizard ([#24407](https://github.com/RocketChat/Rocket.Chat/pull/24407)) + +- Slash commands previews not working ([#24387](https://github.com/RocketChat/Rocket.Chat/pull/24387) by [@ostjen](https://github.com/ostjen)) + +- Spotlight results showing usernames instead of real names ([#25471](https://github.com/RocketChat/Rocket.Chat/pull/25471)) + +- Startup errors creating indexes ([#24409](https://github.com/RocketChat/Rocket.Chat/pull/24409)) + + Fix `bio` and `prid` startup index creation errors. + +- Toolbox hiding under contextual bar ([#25237](https://github.com/RocketChat/Rocket.Chat/pull/25237)) + +- typo on register server tooltip of setup wizard ([#24466](https://github.com/RocketChat/Rocket.Chat/pull/24466)) + +- UI/UX issues on Live Chat widget ([#25407](https://github.com/RocketChat/Rocket.Chat/pull/25407)) + +- Use correct room property for call ended at ([#24932](https://github.com/RocketChat/Rocket.Chat/pull/24932)) + +- User abandonment setting was not working doe to failing event hook ([#25520](https://github.com/RocketChat/Rocket.Chat/pull/25520)) + + A setting watcher and the query for grabbing abandoned chats were broken, now they're not. + +- UserCard sanitization ([#25089](https://github.com/RocketChat/Rocket.Chat/pull/25089)) + + - Rewrites the component to TS + - Fixes some visual issues + + ### before + ![Screen Shot 2022-04-07 at 00 23 11](https://user-images.githubusercontent.com/27704687/162113925-5c9484d1-23e9-4623-8b86-3fbc71b461a1.png) + + ### after + ![Screen Shot 2022-04-07 at 00 07 13](https://user-images.githubusercontent.com/27704687/162112353-afd6aac6-b27c-4470-a642-631b8080d59e.png) + +- Video and Audio not skipping forward ([#19866](https://github.com/RocketChat/Rocket.Chat/pull/19866)) + +- VoIP disabled/enabled sequence puts voip agent in error state ([#25230](https://github.com/RocketChat/Rocket.Chat/pull/25230)) + + Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side) + + It was realised that the race does not happen because of the database or server noticing the changes late. But because of the time taken to establish the AMI connection with Asterisk. + + Solution: + + 1. Change apps/meteor/app/voip/server/startup.ts. When VoIP_Enabled is changed, await for Voip.init() to complete and then broadcast connector.statuschanged with changed value. + 2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance. + 3. in apps/meteor/client/providers/CallProvider/hooks/useVoipClient.ts add the event handler that receives this event. Change voipEnabled from constant to state. Change this state based on the 'value' that is received by the handler. + +- Wrong business hour behavior ([#24896](https://github.com/RocketChat/Rocket.Chat/pull/24896)) + +
+🔍 Minor changes + + +- Bump @rocket.chat/emitter from 0.31.4 to 0.31.9 in /ee/server/services ([#25021](https://github.com/RocketChat/Rocket.Chat/pull/25021) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @rocket.chat/message-parser from 0.31.4 to 0.31.9 in /ee/server/services ([#25019](https://github.com/RocketChat/Rocket.Chat/pull/25019) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @rocket.chat/string-helpers from 0.31.4 to 0.31.9 in /ee/server/services ([#25018](https://github.com/RocketChat/Rocket.Chat/pull/25018) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @rocket.chat/ui-kit from 0.31.4 to 0.31.9 in /ee/server/services ([#25020](https://github.com/RocketChat/Rocket.Chat/pull/25020) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @types/clipboard from 2.0.1 to 2.0.7 ([#24832](https://github.com/RocketChat/Rocket.Chat/pull/24832) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @types/mailparser from 3.0.2 to 3.4.0 ([#24833](https://github.com/RocketChat/Rocket.Chat/pull/24833) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @types/nodemailer from 6.4.2 to 6.4.4 ([#24822](https://github.com/RocketChat/Rocket.Chat/pull/24822) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @types/ws from 8.2.2 to 8.2.3 in /ee/server/services ([#24556](https://github.com/RocketChat/Rocket.Chat/pull/24556) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @types/ws from 8.2.3 to 8.5.2 in /ee/server/services ([#24666](https://github.com/RocketChat/Rocket.Chat/pull/24666) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @types/ws from 8.5.2 to 8.5.3 in /ee/server/services ([#24820](https://github.com/RocketChat/Rocket.Chat/pull/24820) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump actions/checkout from 2 to 3 ([#24668](https://github.com/RocketChat/Rocket.Chat/pull/24668) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump actions/setup-node from 2 to 3 ([#24642](https://github.com/RocketChat/Rocket.Chat/pull/24642) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump adm-zip from 0.4.14 to 0.5.9 ([#24538](https://github.com/RocketChat/Rocket.Chat/pull/24538) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump body-parser from 1.19.0 to 1.19.1 in /ee/server/services ([#23963](https://github.com/RocketChat/Rocket.Chat/pull/23963) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump body-parser from 1.19.0 to 1.19.2 ([#24821](https://github.com/RocketChat/Rocket.Chat/pull/24821) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump body-parser from 1.19.1 to 1.19.2 in /ee/server/services ([#24517](https://github.com/RocketChat/Rocket.Chat/pull/24517) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump body-parser from 1.19.2 to 1.20.0 in /ee/server/services ([#25042](https://github.com/RocketChat/Rocket.Chat/pull/25042) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump cookie from 0.4.1 to 0.4.2 in /ee/server/services ([#24472](https://github.com/RocketChat/Rocket.Chat/pull/24472) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump date-fns from 2.24.0 to 2.28.0 ([#24058](https://github.com/RocketChat/Rocket.Chat/pull/24058) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump ejson from 2.2.1 to 2.2.2 ([#25057](https://github.com/RocketChat/Rocket.Chat/pull/25057) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump eslint-plugin-anti-trojan-source from 1.0.6 to 1.1.0 ([#25076](https://github.com/RocketChat/Rocket.Chat/pull/25076) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump express from 4.17.1 to 4.17.2 in /ee/server/services ([#24469](https://github.com/RocketChat/Rocket.Chat/pull/24469) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump express from 4.17.2 to 4.17.3 in /ee/server/services ([#24522](https://github.com/RocketChat/Rocket.Chat/pull/24522) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump follow-redirects from 1.14.7 to 1.14.8 in /ee/server/services ([#24491](https://github.com/RocketChat/Rocket.Chat/pull/24491) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump is-svg from 4.3.1 to 4.3.2 ([#24801](https://github.com/RocketChat/Rocket.Chat/pull/24801) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump jaeger-client from 3.18.1 to 3.19.0 in /ee/server/services ([#23961](https://github.com/RocketChat/Rocket.Chat/pull/23961) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump jschardet from 1.6.0 to 3.0.0 ([#23121](https://github.com/RocketChat/Rocket.Chat/pull/23121) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump minimist from 1.2.5 to 1.2.6 in /ee/server/services ([#24991](https://github.com/RocketChat/Rocket.Chat/pull/24991) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump pino and pino-pretty ([#25052](https://github.com/RocketChat/Rocket.Chat/pull/25052)) + +- Bump pino from 7.8.0 to 7.8.1 in /ee/server/services ([#24783](https://github.com/RocketChat/Rocket.Chat/pull/24783) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump pino from 7.8.1 to 7.9.1 in /ee/server/services ([#24869](https://github.com/RocketChat/Rocket.Chat/pull/24869) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump pino-pretty from 7.5.1 to 7.5.2 in /ee/server/services ([#24689](https://github.com/RocketChat/Rocket.Chat/pull/24689) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump pino-pretty from 7.5.2 to 7.5.3 in /ee/server/services ([#24698](https://github.com/RocketChat/Rocket.Chat/pull/24698) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump pino-pretty from 7.5.3 to 7.5.4 in /ee/server/services ([#24870](https://github.com/RocketChat/Rocket.Chat/pull/24870) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump pm2 from 5.1.2 to 5.2.0 in /ee/server/services ([#24537](https://github.com/RocketChat/Rocket.Chat/pull/24537) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump prometheus-gc-stats from 0.6.2 to 0.6.3 ([#24803](https://github.com/RocketChat/Rocket.Chat/pull/24803) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump simple-get from 4.0.0 to 4.0.1 ([#24341](https://github.com/RocketChat/Rocket.Chat/pull/24341) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump sodium-native from 3.2.1 to 3.3.0 in /ee/server/services ([#23512](https://github.com/RocketChat/Rocket.Chat/pull/23512) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump template-file from 6.0.0 to 6.0.1 ([#25002](https://github.com/RocketChat/Rocket.Chat/pull/25002) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump ts-node from 10.5.0 to 10.6.0 in /ee/server/services ([#24667](https://github.com/RocketChat/Rocket.Chat/pull/24667) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump ts-node from 10.6.0 to 10.7.0 in /ee/server/services ([#24716](https://github.com/RocketChat/Rocket.Chat/pull/24716) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump underscore.string from 3.3.5 to 3.3.6 in /ee/server/services ([#24498](https://github.com/RocketChat/Rocket.Chat/pull/24498) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump url-parse from 1.5.3 to 1.5.7 ([#24528](https://github.com/RocketChat/Rocket.Chat/pull/24528) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump url-parse from 1.5.7 to 1.5.10 ([#24640](https://github.com/RocketChat/Rocket.Chat/pull/24640) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump vm2 from 3.9.5 to 3.9.7 in /ee/server/services ([#24509](https://github.com/RocketChat/Rocket.Chat/pull/24509) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Chore: `twoFactorRequired` signature ([#24518](https://github.com/RocketChat/Rocket.Chat/pull/24518)) + + Improved type checking for decorator `twoFactorRequired`. + +- Chore: Add description to global OTR setting ([#24333](https://github.com/RocketChat/Rocket.Chat/pull/24333) by [@pedrogssouza](https://github.com/pedrogssouza)) + +- Chore: Add E2E tests for livechat/room.close ([#24729](https://github.com/RocketChat/Rocket.Chat/pull/24729) by [@Muramatsu2602](https://github.com/Muramatsu2602)) + + * Create a new test suite file under tests/end-to-end/api/livechat + * Create tests for the following endpoint: + + ivechat/room.close + +- Chore: Add E2E tests for livechat/visitor ([#24764](https://github.com/RocketChat/Rocket.Chat/pull/24764) by [@Muramatsu2602](https://github.com/Muramatsu2602)) + + - Create a new test suite file under tests/end-to-end/api/livechat + - Create tests for the following endpoints: + + livechat/visitor (create visitor, update visitor, add custom fields to visitors) + +- Chore: Add error boundary to message component ([#25223](https://github.com/RocketChat/Rocket.Chat/pull/25223)) + + Not crash the whole application if something goes wrong in the MessageList component. + + ![image](https://user-images.githubusercontent.com/40830821/162269915-931c5c3c-c979-4234-b74c-371f67467ce0.png) + +- Chore: Add Livechat repo into Monorepo packages ([#25312](https://github.com/RocketChat/Rocket.Chat/pull/25312)) + +- Chore: Add options to debug stdout and rate limiter ([#25336](https://github.com/RocketChat/Rocket.Chat/pull/25336)) + +- Chore: Add root package.json to houston files ([#25286](https://github.com/RocketChat/Rocket.Chat/pull/25286)) + + See title + +- Chore: add some missing REST definitions ([#24925](https://github.com/RocketChat/Rocket.Chat/pull/24925) by [@gerzonc](https://github.com/gerzonc)) + + On the [mobile client](https://github.com/RocketChat/Rocket.Chat.ReactNative), we made an effort to collect more `REST API` definitions that are missing on the server side during our migration to TypeScript. Since we're both migrating to TypeScript, we thought it would be a good idea to share those so you guys can benefit from our initiative. + +- Chore: Add yarn plugin to check node and yarn version ([#25224](https://github.com/RocketChat/Rocket.Chat/pull/25224)) + +- Chore: added Server Instances endpoint types ([#24507](https://github.com/RocketChat/Rocket.Chat/pull/24507)) + + Created typing for endpoint definitions on `instances.ts`. + +- Chore: added settings endpoint types ([#24506](https://github.com/RocketChat/Rocket.Chat/pull/24506)) + + Created typing for endpoint definitions on `settings.ts`. + +- Chore: APIClass types ([#24747](https://github.com/RocketChat/Rocket.Chat/pull/24747)) + + This pull request creates a new `restivus` module (.d.ts) for the `api.js` file. + +- Chore: Bump fuselage ([#25371](https://github.com/RocketChat/Rocket.Chat/pull/25371)) + +- Chore: Bump Fuselage packages ([#25259](https://github.com/RocketChat/Rocket.Chat/pull/25259)) + +- Chore: Bump Fuselage packages ([#25015](https://github.com/RocketChat/Rocket.Chat/pull/25015)) + + It uses the last stable version of Fuselage packages. + +- Chore: Bump Fuselage packages ([#24573](https://github.com/RocketChat/Rocket.Chat/pull/24573)) + + It uses the last stable version of Fuselage packages. + +- Chore: bump fuselage version ([#24453](https://github.com/RocketChat/Rocket.Chat/pull/24453)) + +- Chore: Cancel running jobs if PR is updated ([#24708](https://github.com/RocketChat/Rocket.Chat/pull/24708)) + +- Chore: Convert admin custom sound to tsx ([#25128](https://github.com/RocketChat/Rocket.Chat/pull/25128)) + +- Chore: Convert JS files to Typescript ([#24410](https://github.com/RocketChat/Rocket.Chat/pull/24410)) + + This pull request converts 26 more files from Javascript to Typescript, to check variable types and increase validation on the code. + +- Chore: Convert LivechatAgentActivity to raw model and TS ([#25123](https://github.com/RocketChat/Rocket.Chat/pull/25123)) + +- Chore: Convert Mailer to TS ([#25121](https://github.com/RocketChat/Rocket.Chat/pull/25121)) + +- Chore: Convert NotificationStatus to TS ([#25125](https://github.com/RocketChat/Rocket.Chat/pull/25125)) + +- Chore: Convert server functions from javascript to typescript ([#24384](https://github.com/RocketChat/Rocket.Chat/pull/24384)) + + This pull request will be used to rewrite some functions on the Chat Engine to Typescript, in order to increase security and specify variable types on the code. + +- Chore: Convert to typescript the me slashCommands files ([#24321](https://github.com/RocketChat/Rocket.Chat/pull/24321) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + + Convert to typescript the me slashCommands files + +- Chore: Convert to typescript the mute and unmute slash commands files ([#24325](https://github.com/RocketChat/Rocket.Chat/pull/24325) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + + Convert to typescript the mute and unmute slash commands files + +- Chore: Convert to typescript the slash commands create files ([#24306](https://github.com/RocketChat/Rocket.Chat/pull/24306) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + + Convert Slash Commands create files to typescript. + +- Chore: Convert to typescript the slash commands invite files ([#24311](https://github.com/RocketChat/Rocket.Chat/pull/24311) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + + Convert to typescript the slash commands invite files + +- Chore: Convert to typescript the unarchive slash commands files ([#24331](https://github.com/RocketChat/Rocket.Chat/pull/24331) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + + Convert to typescript the unarchive slash commands files + +- Chore: converted more hooks to typescript ([#24628](https://github.com/RocketChat/Rocket.Chat/pull/24628)) + + Converted some functions on `client/hooks/` from JavaScript to Typescript. + +- Chore: Create README.md for Rest Typings ([#25335](https://github.com/RocketChat/Rocket.Chat/pull/25335)) + +- Chore: Delete unused file (NewAdminInfoPage.js) ([#24196](https://github.com/RocketChat/Rocket.Chat/pull/24196)) + + Just removing a duplicated/unused file. + +- Chore: ensure scripts use cross-env and ignore some dirs (ROC-54) ([#25218](https://github.com/RocketChat/Rocket.Chat/pull/25218)) + + - data and test-failure should be ignored + - ensure scripts use cross-env + +- Chore: Fix Cypress tests ([#24544](https://github.com/RocketChat/Rocket.Chat/pull/24544)) + +- Chore: Fix grammatical errors in Code of Conduct ([#24759](https://github.com/RocketChat/Rocket.Chat/pull/24759) by [@aadishJ01](https://github.com/aadishJ01)) + +- Chore: fix grammatical errors in Features ([#24771](https://github.com/RocketChat/Rocket.Chat/pull/24771) by [@aadishJ01](https://github.com/aadishJ01)) + +- Chore: Fix return type warnings ([#25275](https://github.com/RocketChat/Rocket.Chat/pull/25275)) + +- Chore: Get Settings Statistics ([#24397](https://github.com/RocketChat/Rocket.Chat/pull/24397)) + +- Chore: Improve logger to allow log of `unknown` values ([#24726](https://github.com/RocketChat/Rocket.Chat/pull/24726)) + +- Chore: Improve PR title validation regex ([#24467](https://github.com/RocketChat/Rocket.Chat/pull/24467)) + +- Chore: Improvements on role syncing (ldap, oauth and saml) ([#23824](https://github.com/RocketChat/Rocket.Chat/pull/23824)) + +- Chore: Js to ts slash commands archive ([#24304](https://github.com/RocketChat/Rocket.Chat/pull/24304) by [@eduardofcabrera](https://github.com/eduardofcabrera)) + + Convert Slash Commands archive files to typescript + +- Chore: Micro services fixes and cleanup ([#24753](https://github.com/RocketChat/Rocket.Chat/pull/24753)) + +- Chore: Migrate oauth2server to typescript ([#25126](https://github.com/RocketChat/Rocket.Chat/pull/25126)) + +- Chore: Minor dependency updates ([#25269](https://github.com/RocketChat/Rocket.Chat/pull/25269)) + +- Chore: Missing keys in APIsDisplay ([#24464](https://github.com/RocketChat/Rocket.Chat/pull/24464)) + +- Chore: Monorepo ([#25074](https://github.com/RocketChat/Rocket.Chat/pull/25074)) + +- Chore: move definitions to packages ([#25085](https://github.com/RocketChat/Rocket.Chat/pull/25085)) + +- Chore: organize test files and fix code coverage ([#24900](https://github.com/RocketChat/Rocket.Chat/pull/24900)) + +- Chore: Remove Alpine image deps after using them ([#25053](https://github.com/RocketChat/Rocket.Chat/pull/25053)) + +- Chore: Remove duplicated useUserRoom ([#25180](https://github.com/RocketChat/Rocket.Chat/pull/25180)) + +- Chore: Remove old files from removed Omnichannel feature ([#25129](https://github.com/RocketChat/Rocket.Chat/pull/25129)) + +- Chore: Remove old scripts ([#24911](https://github.com/RocketChat/Rocket.Chat/pull/24911)) + +- Chore: Remove package-lock.json from houston files ([#25280](https://github.com/RocketChat/Rocket.Chat/pull/25280)) + + Houston config in the `package.json` file still mentioned `package-lock.json`, but it doesn't exist anymore + +- Chore: Remove storybook build job from CI ([#24530](https://github.com/RocketChat/Rocket.Chat/pull/24530)) + +- Chore: Remove unused Drone CI files ([#25124](https://github.com/RocketChat/Rocket.Chat/pull/25124)) + +- Chore: roomTypes: Stop mixing client and server code together ([#24536](https://github.com/RocketChat/Rocket.Chat/pull/24536)) + +- Chore: Run tests using microservices deployment on CI ([#24513](https://github.com/RocketChat/Rocket.Chat/pull/24513)) + +- Chore: Set Docker image tag to latest only when really latest ([#24366](https://github.com/RocketChat/Rocket.Chat/pull/24366)) + +- Chore: Skip local services changes when shutting down duplicated services ([#24810](https://github.com/RocketChat/Rocket.Chat/pull/24810)) + +- Chore: Storybook mocking and examples improved ([#24969](https://github.com/RocketChat/Rocket.Chat/pull/24969)) + + - Stories from `ee/` included; + - Differentiate root story kinds; + - Mocking of `ServerContext` via Storybook parameters. + +- Chore: Sync with master ([#25284](https://github.com/RocketChat/Rocket.Chat/pull/25284)) + +- Chore: Template to generate packages ([#25174](https://github.com/RocketChat/Rocket.Chat/pull/25174)) + + ``` + npx hygen package new test + ``` + +- Chore: Tests with Playwright (task: All works) ([#25122](https://github.com/RocketChat/Rocket.Chat/pull/25122)) + +- Chore: Tests with Playwright (task: ROC-28, 09-channels) ([#25196](https://github.com/RocketChat/Rocket.Chat/pull/25196)) + +- Chore: TS conversion folder client ([#25031](https://github.com/RocketChat/Rocket.Chat/pull/25031)) + +- Chore: TS migration SortList ([#25167](https://github.com/RocketChat/Rocket.Chat/pull/25167)) + +- Chore: Unify ILivechatAgent with ILivechatAgentRecord ([#24406](https://github.com/RocketChat/Rocket.Chat/pull/24406)) + +- Chore: Update Apps-Engine ([#24651](https://github.com/RocketChat/Rocket.Chat/pull/24651)) + +- Chore: Update Apps-Engine ([#24568](https://github.com/RocketChat/Rocket.Chat/pull/24568)) + +- Chore: Update fuselage deps to match monolith versions ([#24501](https://github.com/RocketChat/Rocket.Chat/pull/24501)) + +- Chore: Update Livechat to the last version ([#25257](https://github.com/RocketChat/Rocket.Chat/pull/25257)) + +- Chore: Update Livechat version ([#25130](https://github.com/RocketChat/Rocket.Chat/pull/25130)) + +- Chore: Update Meteor to 2.5.6 ([#24461](https://github.com/RocketChat/Rocket.Chat/pull/24461)) + +- Chore: update OTR icon ([#24521](https://github.com/RocketChat/Rocket.Chat/pull/24521) by [@kibonusp](https://github.com/kibonusp)) + + I changed the shredder icon in OTR contextual bar to the stopwatch icon, recently added to the fuselage. + +- Chore: Update ws package ([#24477](https://github.com/RocketChat/Rocket.Chat/pull/24477)) + +- Chore(deps-dev): Bump @types/mock-require from 2.0.0 to 2.0.1 ([#24574](https://github.com/RocketChat/Rocket.Chat/pull/24574) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Chore(deps-dev): Bump ts-node from 10.0.0 to 10.5.0 in /ee/server/services ([#24435](https://github.com/RocketChat/Rocket.Chat/pull/24435) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Chore(deps): Bump node-fetch from 2.6.1 to 2.6.7 in /ee/server/services ([#24299](https://github.com/RocketChat/Rocket.Chat/pull/24299) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- i18n: Language update from LingoHub 🤖 on 2022-01-31Z ([#24357](https://github.com/RocketChat/Rocket.Chat/pull/24357)) + +- i18n: Language update from LingoHub 🤖 on 2022-02-07Z ([#24429](https://github.com/RocketChat/Rocket.Chat/pull/24429)) + +- i18n: Language update from LingoHub 🤖 on 2022-02-14Z ([#24493](https://github.com/RocketChat/Rocket.Chat/pull/24493)) + +- i18n: Language update from LingoHub 🤖 on 2022-02-21Z ([#24558](https://github.com/RocketChat/Rocket.Chat/pull/24558)) + +- i18n: Language update from LingoHub 🤖 on 2022-02-28Z ([#24644](https://github.com/RocketChat/Rocket.Chat/pull/24644)) + +- i18n: Language update from LingoHub 🤖 on 2022-03-07Z ([#24717](https://github.com/RocketChat/Rocket.Chat/pull/24717)) + +- i18n: Language update from LingoHub 🤖 on 2022-03-14Z ([#24823](https://github.com/RocketChat/Rocket.Chat/pull/24823)) + +- i18n: Language update from LingoHub 🤖 on 2022-03-21Z ([#24895](https://github.com/RocketChat/Rocket.Chat/pull/24895)) + +- i18n: Language update from LingoHub 🤖 on 2022-03-28Z ([#24971](https://github.com/RocketChat/Rocket.Chat/pull/24971)) + +- i18n: Language update from LingoHub 🤖 on 2022-04-04Z ([#25043](https://github.com/RocketChat/Rocket.Chat/pull/25043)) + +- Merge master into develop & Set version to 4.5.0-develop ([#24363](https://github.com/RocketChat/Rocket.Chat/pull/24363)) + +- Merge master into develop & Set version to 4.6.0-develop ([#24653](https://github.com/RocketChat/Rocket.Chat/pull/24653)) + +- Merge master into develop & Set version to 4.7.0-develop ([#25028](https://github.com/RocketChat/Rocket.Chat/pull/25028)) + +- Regression: Add `isPending` status to message ([#25299](https://github.com/RocketChat/Rocket.Chat/pull/25299)) + +- Regression: Add createdOTR index ([#25017](https://github.com/RocketChat/Rocket.Chat/pull/25017)) + +- Regression: Add eslint package to micro services Dockerfile ([#25311](https://github.com/RocketChat/Rocket.Chat/pull/25311)) + +- Regression: Add select message to system message and thread preview and allow select on legacy template ([#25251](https://github.com/RocketChat/Rocket.Chat/pull/25251)) + +- Regression: Add support to namespace within micro services ([#24581](https://github.com/RocketChat/Rocket.Chat/pull/24581)) + +- Regression: Admin Sidebar colors inverted. ([#24609](https://github.com/RocketChat/Rocket.Chat/pull/24609)) + +- Regression: Avatar not loading on first direct message ([#25211](https://github.com/RocketChat/Rocket.Chat/pull/25211)) + + fix avatar not loading on a first direct message + +- Regression: Better MongoDB connection management for micro services ([#25323](https://github.com/RocketChat/Rocket.Chat/pull/25323)) + +- Regression: bump onboarding-ui version ([#25320](https://github.com/RocketChat/Rocket.Chat/pull/25320)) + + - Bump to 'next' the onboarding-ui package from fuselage. + - Update from 'companyEmail' to 'email' adminData usage types + +- Regression: Bunch of settings fixes for VoIP ([#24594](https://github.com/RocketChat/Rocket.Chat/pull/24594)) + +- Regression: Call doesn't stop ringing after agent unregistration ([#24908](https://github.com/RocketChat/Rocket.Chat/pull/24908)) + +- Regression: Change preference to be default legacy messages ([#25255](https://github.com/RocketChat/Rocket.Chat/pull/25255)) + +- Regression: CI playwright ([#25168](https://github.com/RocketChat/Rocket.Chat/pull/25168)) + +- Regression: Custom roles displaying ID instead of name on some admin screens ([#24999](https://github.com/RocketChat/Rocket.Chat/pull/24999)) + + ![image](https://user-images.githubusercontent.com/55164754/160981416-555bcaa1-c075-4260-937c-64523472da43.png) + ![image](https://user-images.githubusercontent.com/55164754/160981452-6eae4e74-8425-4073-8256-472aba72b9db.png) + +- Regression: Do not show toast on incoming voip calls ([#24619](https://github.com/RocketChat/Rocket.Chat/pull/24619)) + +- Regression: Encode registration info as JWT when signing key is provided ([#24626](https://github.com/RocketChat/Rocket.Chat/pull/24626)) + +- Regression: Error is raised when there's no Asterisk queue available yet ([#24980](https://github.com/RocketChat/Rocket.Chat/pull/24980)) + +- Regression: Error setting user avatars and mentioning rooms on Slack Import ([#24585](https://github.com/RocketChat/Rocket.Chat/pull/24585)) + + - Fix `Mentioned room not found` error when importing rooms from Slack; + - Fix `Forbidden` error when setting avatars for users imported from Slack (on user import/creation); + - Fix incorrect message count on imported rooms; + - Fix missing username on messages imported from Slack; + +- Regression: Error when trying to load name of dm rooms for avatars and notifications ([#24583](https://github.com/RocketChat/Rocket.Chat/pull/24583)) + +- Regression: eslint not running on packages ([#25305](https://github.com/RocketChat/Rocket.Chat/pull/25305)) + +- Regression: Extension List panel UI not aligned with designs ([#24645](https://github.com/RocketChat/Rocket.Chat/pull/24645)) + +- Regression: Fix account service login expiration ([#24920](https://github.com/RocketChat/Rocket.Chat/pull/24920)) + +- Regression: Fix CI monorepo build ([#25107](https://github.com/RocketChat/Rocket.Chat/pull/25107)) + +- Regression: Fix clicking on visitor's chat in the sidebar does not display the chat window ([#25380](https://github.com/RocketChat/Rocket.Chat/pull/25380)) + + Fix: livechat room not opening. + +- Regression: Fix double value on holdTime and empty msg on last message ([#24630](https://github.com/RocketChat/Rocket.Chat/pull/24630)) + +- Regression: Fix English i18n react text ([#25368](https://github.com/RocketChat/Rocket.Chat/pull/25368)) + + Incorrect text in reaction tooltip has been fixed + +- Regression: Fix federation Matrix bridge startup ([#25273](https://github.com/RocketChat/Rocket.Chat/pull/25273)) + +- Regression: Fix in-correct room status shown to agents ([#24592](https://github.com/RocketChat/Rocket.Chat/pull/24592)) + +- Regression: Fix incoming voip call ringtone is not ringing ([#24616](https://github.com/RocketChat/Rocket.Chat/pull/24616)) + +- Regression: Fix micro services Docker build ([#25193](https://github.com/RocketChat/Rocket.Chat/pull/25193)) + +- Regression: Fix multi line is not showing an empty line between lines ([#25317](https://github.com/RocketChat/Rocket.Chat/pull/25317)) + +- Regression: Fix reply button not working when hideFlexTab is enabled ([#25306](https://github.com/RocketChat/Rocket.Chat/pull/25306)) + +- Regression: Fix room not getting created due to null visitor status ([#24562](https://github.com/RocketChat/Rocket.Chat/pull/24562)) + +- Regression: Fix services Docker build on CI ([#25181](https://github.com/RocketChat/Rocket.Chat/pull/25181)) + +- Regression: Fix size of custom emoji and render emoji on thread message preview ([#25314](https://github.com/RocketChat/Rocket.Chat/pull/25314)) + +- Regression: Fix the alpine image and dev UX installing matrix-rust-sdk-bindings ([#25319](https://github.com/RocketChat/Rocket.Chat/pull/25319)) + + The package only included a few pre-built which caused all macs to have to compile every time they installed and also caused our alpine not to work. + + This temporarily switches to a fork of the matrix-appservice-bridge package. + + Made changes to one of its child dependencies `matrix-rust-sdk-bindings` that adds pre-built binaries for mac and musl (for alpine). + +- Regression: Fix time fields and wrap up in Voip Room Contexual bar ([#24625](https://github.com/RocketChat/Rocket.Chat/pull/24625)) + +- Regression: Fix time format on Voip system messages ([#24603](https://github.com/RocketChat/Rocket.Chat/pull/24603)) + +- Regression: Fix translation for call started message ([#24615](https://github.com/RocketChat/Rocket.Chat/pull/24615)) + +- Regression: Fix unexpected errors breaking ddp-streamer ([#24948](https://github.com/RocketChat/Rocket.Chat/pull/24948)) + +- Regression: Fix wrong tab name for VoIP settings ([#24647](https://github.com/RocketChat/Rocket.Chat/pull/24647)) + +- Regression: Fixes in Voice Contextual Bar and Directory ([#24596](https://github.com/RocketChat/Rocket.Chat/pull/24596)) + +- Regression: If Asterisk suddenly goes down, server has no way to know. Causes server to get stuck. Needs restart ([#24624](https://github.com/RocketChat/Rocket.Chat/pull/24624)) + +- Regression: Improve Sidenav open/close handling and fixed codeql configs and E2E tests ([#24756](https://github.com/RocketChat/Rocket.Chat/pull/24756)) + +- Regression: Mark all rooms as read modal closing instantly. ([#24610](https://github.com/RocketChat/Rocket.Chat/pull/24610)) + +- Regression: Messages in new message template Crashing. ([#25327](https://github.com/RocketChat/Rocket.Chat/pull/25327)) + +- Regression: No audio when call comes from Skype/IP phone ([#24602](https://github.com/RocketChat/Rocket.Chat/pull/24602)) + + The audio was not rendered because of re-rendering of react element based on + queueCounter and roomInfo. queueCounter and roomInfo cause the dom to re-render when call gets accepted + because after accepting call, queueCounter changes or a room gets created. + The audio element gets recreated. But VoIP user probably holds the old one. + The behaviour is not predictable when such case happens. If everything gets cleanly setup, + even if the audio element goes headless, it still continues to play the remote audio. + But in other cases, it is unreferenced the one on dom has its srcObject as null. + This causes no audio. + + This fix provides a way to re-initialise the rendering elements in VoIP user + and calls this function on useEffect() if the re-render has happen. + +- Regression: Prevent button from losing state when rerendering ([#24648](https://github.com/RocketChat/Rocket.Chat/pull/24648)) + +- Regression: Prevent connect to asterisk when VoIP is disabled ([#24601](https://github.com/RocketChat/Rocket.Chat/pull/24601)) + +- Regression: Queue counter aggregator for incoming/hanged calls ([#24635](https://github.com/RocketChat/Rocket.Chat/pull/24635)) + +- Regression: Refresh server connection when MI server settings change ([#24649](https://github.com/RocketChat/Rocket.Chat/pull/24649)) + +- Regression: Register services right away ([#24800](https://github.com/RocketChat/Rocket.Chat/pull/24800)) + +- Regression: Revert Bugsnag version ([#25313](https://github.com/RocketChat/Rocket.Chat/pull/25313)) + +- Regression: Rocket.Chat Webapp not loading. ([#25349](https://github.com/RocketChat/Rocket.Chat/pull/25349)) + +- Regression: Role Sync not always working ([#24850](https://github.com/RocketChat/Rocket.Chat/pull/24850)) + +- Regression: Server crashing if Voip credentials are invalid ([#24646](https://github.com/RocketChat/Rocket.Chat/pull/24646)) + +- Regression: Show username and real name on the message system ([#25254](https://github.com/RocketChat/Rocket.Chat/pull/25254)) + +- Regression: Shows error if micro service cannot connect to Mongo ([#25301](https://github.com/RocketChat/Rocket.Chat/pull/25301)) + +- Regression: Use exact Node version on micro services Docker images ([#25287](https://github.com/RocketChat/Rocket.Chat/pull/25287)) + +- Regression: Validate empty fields for Message template ([#25250](https://github.com/RocketChat/Rocket.Chat/pull/25250)) + +- Regression: VoIP service button displayed when VoIP is disabled ([#24598](https://github.com/RocketChat/Rocket.Chat/pull/24598)) + +- Regression: yarn dev triggers build dependencies ([#25208](https://github.com/RocketChat/Rocket.Chat/pull/25208)) + +- Release 4.5.0 ([#24652](https://github.com/RocketChat/Rocket.Chat/pull/24652) by [@LucasFASouza](https://github.com/LucasFASouza) & [@aswinidev](https://github.com/aswinidev) & [@dependabot[bot]](https://github.com/dependabot[bot]) & [@lingohub[bot]](https://github.com/lingohub[bot]) & [@ostjen](https://github.com/ostjen)) + +- Release 4.5.1 ([#24782](https://github.com/RocketChat/Rocket.Chat/pull/24782) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari) & [@cuonghuunguyen](https://github.com/cuonghuunguyen)) + +- Release 4.5.2 ([#24814](https://github.com/RocketChat/Rocket.Chat/pull/24814)) + +- Release 4.5.3 ([#24884](https://github.com/RocketChat/Rocket.Chat/pull/24884)) + +- Release 4.5.4 ([#24938](https://github.com/RocketChat/Rocket.Chat/pull/24938)) + +- Release 4.5.5 ([#24998](https://github.com/RocketChat/Rocket.Chat/pull/24998)) + +- Release 4.6.0 ([#25027](https://github.com/RocketChat/Rocket.Chat/pull/25027) by [@aswinidev](https://github.com/aswinidev) & [@dependabot[bot]](https://github.com/dependabot[bot]) & [@eduardofcabrera](https://github.com/eduardofcabrera) & [@lingohub[bot]](https://github.com/lingohub[bot])) + +- Release 4.6.1 ([#25095](https://github.com/RocketChat/Rocket.Chat/pull/25095)) + +- Release 4.6.2 ([#25191](https://github.com/RocketChat/Rocket.Chat/pull/25191) by [@sidmohanty11](https://github.com/sidmohanty11)) + +- Release 4.6.3 ([#25235](https://github.com/RocketChat/Rocket.Chat/pull/25235)) + +- Release 4.7.0 ([#25390](https://github.com/RocketChat/Rocket.Chat/pull/25390) by [@Himanshu664](https://github.com/Himanshu664) & [@dependabot[bot]](https://github.com/dependabot[bot]) & [@lingohub[bot]](https://github.com/lingohub[bot])) + +- Release 4.7.1 ([#25510](https://github.com/RocketChat/Rocket.Chat/pull/25510) by [@felipe-menelau](https://github.com/felipe-menelau)) + +- Release 4.7.2 ([#25580](https://github.com/RocketChat/Rocket.Chat/pull/25580)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@Aman-Maheshwari](https://github.com/Aman-Maheshwari) +- [@Himanshu664](https://github.com/Himanshu664) +- [@JMoVS](https://github.com/JMoVS) +- [@LucasFASouza](https://github.com/LucasFASouza) +- [@Muramatsu2602](https://github.com/Muramatsu2602) +- [@aadishJ01](https://github.com/aadishJ01) +- [@aakash-gitdev](https://github.com/aakash-gitdev) +- [@aswinidev](https://github.com/aswinidev) +- [@cuonghuunguyen](https://github.com/cuonghuunguyen) +- [@dependabot[bot]](https://github.com/dependabot[bot]) +- [@eduardofcabrera](https://github.com/eduardofcabrera) +- [@felipe-menelau](https://github.com/felipe-menelau) +- [@gerzonc](https://github.com/gerzonc) +- [@kibonusp](https://github.com/kibonusp) +- [@lingohub[bot]](https://github.com/lingohub[bot]) +- [@ostjen](https://github.com/ostjen) +- [@paulobernardoaf](https://github.com/paulobernardoaf) +- [@pedrogssouza](https://github.com/pedrogssouza) +- [@sidmohanty11](https://github.com/sidmohanty11) +- [@tkurz](https://github.com/tkurz) +- [@ujorgeleite](https://github.com/ujorgeleite) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@AllanPazRibeiro](https://github.com/AllanPazRibeiro) +- [@KevLehman](https://github.com/KevLehman) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@alansikora](https://github.com/alansikora) +- [@albuquerquefabio](https://github.com/albuquerquefabio) +- [@amolghode1981](https://github.com/amolghode1981) +- [@cauefcr](https://github.com/cauefcr) +- [@d-gubert](https://github.com/d-gubert) +- [@debdutdeb](https://github.com/debdutdeb) +- [@dougfabris](https://github.com/dougfabris) +- [@felipe-rod123](https://github.com/felipe-rod123) +- [@filipemarins](https://github.com/filipemarins) +- [@gabriellsh](https://github.com/gabriellsh) +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@ggazzo](https://github.com/ggazzo) +- [@guijun13](https://github.com/guijun13) +- [@jeanfbrito](https://github.com/jeanfbrito) +- [@juliajforesti](https://github.com/juliajforesti) +- [@matheusbsilva137](https://github.com/matheusbsilva137) +- [@murtaza98](https://github.com/murtaza98) +- [@nishant23122000](https://github.com/nishant23122000) +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@renatobecker](https://github.com/renatobecker) +- [@rique223](https://github.com/rique223) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@souzaramon](https://github.com/souzaramon) +- [@tassoevan](https://github.com/tassoevan) +- [@tiagoevanp](https://github.com/tiagoevanp) +- [@tmontini](https://github.com/tmontini) +- [@weslley543](https://github.com/weslley543) +- [@yash-rajpal](https://github.com/yash-rajpal) + +# 4.4.4 +`2022-05-20 · 12 🎉 · 26 🚀 · 79 🐛 · 213 🔍 · 54 👩‍💻👨‍💻` + +### Engine versions +- Node: `14.18.3` +- NPM: `6.14.15` +- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` + +### 🎉 New features + + +- Add expire index to integration history ([#25087](https://github.com/RocketChat/Rocket.Chat/pull/25087)) + +- Alpha Matrix Federation ([#23688](https://github.com/RocketChat/Rocket.Chat/pull/23688)) + + Experimental support for Matrix Federation with a Bridge + + https://user-images.githubusercontent.com/51996/164530391-e8b17ecd-a4d0-4ef8-a8b7-81230c1773d3.mp4 + +- E2E password generator ([#24114](https://github.com/RocketChat/Rocket.Chat/pull/24114) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + +- Engagement Statistics ([#24989](https://github.com/RocketChat/Rocket.Chat/pull/24989)) + +- Engagement Statistics ([#24777](https://github.com/RocketChat/Rocket.Chat/pull/24777) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + +- Expand Apps Engine's environment variable allowed list ([#23870](https://github.com/RocketChat/Rocket.Chat/pull/23870) by [@cuonghuunguyen](https://github.com/cuonghuunguyen)) + +- Marketplace sort filter ([#24567](https://github.com/RocketChat/Rocket.Chat/pull/24567) by [@ujorgeleite](https://github.com/ujorgeleite)) + + Implemented a sort filter for the marketplace screen. This component sorts the marketplace apps list in 4 ways, alphabetical order(A-Z), inverse alphabetical order(Z-A), most recently updated(MRU), and least recent updated(LRU). Besides that, I've generalized some components and types to increase code reusability, renamed some helpers as well as deleted some useless ones, and inserted the necessary new translations on the English i18n dictionary. + Demo gif: + ![Marketplace sort filter](https://user-images.githubusercontent.com/43561537/155033709-e07a6306-a85a-4f7f-9624-b53ba5dd7fa9.gif) + +- Message Template React Component ([#23971](https://github.com/RocketChat/Rocket.Chat/pull/23971)) + + Complete rewrite of the messages component in react. Visual changes should be minimal as well as user impact, with no break changes (unless you've customized the blaze template). + + + + ![Screen Shot 2022-04-05 at 11 14 18](https://user-images.githubusercontent.com/27704687/161774027-38dd9c7b-eeeb-45e2-b9d8-ea2a9be8486d.png) + In case you encounter any problems, or want to compare, temporarily it is possible to use the old version + + image + +- Telemetry Events ([#24781](https://github.com/RocketChat/Rocket.Chat/pull/24781) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + +- Upgrade Tab ([#24835](https://github.com/RocketChat/Rocket.Chat/pull/24835)) + + ![image](https://user-images.githubusercontent.com/27704687/160172260-c656282e-a487-4092-948d-d11c9bacb598.png) + +- Use setting to determine if initial general channel is needed ([#25441](https://github.com/RocketChat/Rocket.Chat/pull/25441) by [@felipe-menelau](https://github.com/felipe-menelau)) + + - Adds flag responsible for overwriting #general channel creation + +- VoIP Support for Omnichannel ([#23102](https://github.com/RocketChat/Rocket.Chat/pull/23102)) + + - Created VoipService to manage VoIP connections and PBX connection + - Created LivechatVoipService that will handle custom cases for livechat (creating rooms, assigning chats to queue, actions when call is finished, etc) + - Created Basic interfaces to support new services and new model + - Created Endpoints for management interfaces + - Implemented asterisk connector on VoIP service + - Created UI components to show calls incoming and to allow answering/rejecting calls + - Added new settings to control call server/management server connection values + - Added endpoints to associate Omnichannel Agents with PBX Extensions + - Added support for event listening on server side, to get metadata about calls being received/ongoing + - Created new pages to update settings & to see user-extension association + - Created new page to see ongoing calls (and past calls) + - Added support for remote hangup/hold on calls + - Implemented call metrics calculation (hold time, waiting time, talk time) + - Show a notificaiton when call is received + +### 🚀 Improvements + + +- **ENTERPRISE:** Don't start presence monitor when running micro services ([#24739](https://github.com/RocketChat/Rocket.Chat/pull/24739)) + +- **ENTERPRISE:** Improve how micro services are loaded ([#24388](https://github.com/RocketChat/Rocket.Chat/pull/24388)) + +- Add OTR Room States ([#24565](https://github.com/RocketChat/Rocket.Chat/pull/24565)) + + Earlier OTR room uses only 2 states, we need more states to support future features. + This adds more states for the OTR contextualBar. + + - Expired + Screen Shot 2022-04-20 at 13 55 52 + + - Declined + Screen Shot 2022-04-20 at 13 49 28 + + - Error + Screen Shot 2022-04-20 at 13 55 26 + +- Add return button in chats opened from the list of current chats ([#24458](https://github.com/RocketChat/Rocket.Chat/pull/24458) by [@LucasFASouza](https://github.com/LucasFASouza)) + + The new return button for Omnichannel chats came out with release 3.15 but the feature was only available for chats that were opened from Omnichannel Contact Center. + Now, the same UI/UX is supported for chats opened from Current Chats list. + + ![image](https://user-images.githubusercontent.com/32396925/153283190-bd5c9748-c36b-4874-a704-6043afc7e3a1.png) + + The chat now opens in the Omnichannel settings and has the return button so the user can go back to the Current Chats list. + + ![image](https://user-images.githubusercontent.com/32396925/153285591-fad8e4a0-d2ea-4a02-8b2a-15e383b3c876.png) + +- Add tooltip to sidebar room menu ([#24405](https://github.com/RocketChat/Rocket.Chat/pull/24405) by [@Himanshu664](https://github.com/Himanshu664)) + +- Add tooltips on action buttons of Canned Response message composer ([#24483](https://github.com/RocketChat/Rocket.Chat/pull/24483) by [@LucasFASouza](https://github.com/LucasFASouza)) + + The tooltips were missing on the action buttons of CR message composer. + + ![image](https://user-images.githubusercontent.com/32396925/153620327-91107245-4b47-4d39-a99a-6da6d1cf5734.png) + + Users can now feel more encouraged to use these actions knowing what they are supposed to do. + +- Add user to room on "Click to Join!" button press ([#24041](https://github.com/RocketChat/Rocket.Chat/pull/24041) by [@ostjen](https://github.com/ostjen)) + + - Add user to room on "Click to Join!" button press; + - Display the "Join" button in discussions inside channels (keeping the behavior consistent with discussions inside groups). + +- Added a new "All" tab which shows all integrations in Integrations ([#24109](https://github.com/RocketChat/Rocket.Chat/pull/24109) by [@aswinidev](https://github.com/aswinidev)) + +- Added MaxNickNameLength and MaxBioLength constants ([#25231](https://github.com/RocketChat/Rocket.Chat/pull/25231) by [@aakash-gitdev](https://github.com/aakash-gitdev)) + +- Added tooltip options for message menu ([#24431](https://github.com/RocketChat/Rocket.Chat/pull/24431) by [@Himanshu664](https://github.com/Himanshu664)) + +- Adding new statistics related to voip and omnichannel ([#24887](https://github.com/RocketChat/Rocket.Chat/pull/24887)) + + - Total of Canned response messages sent + - Total of tags used + - Last-Chatted Agent Preferred (enabled/disabled) + - Assign new conversations to the contact manager (enabled/disabled) + - How to handle Visitor Abandonment setting + - Amount of chats placed on hold + - VoIP Enabled + - Amount of VoIP Calls + - Amount of VoIP Extensions connected + - Amount of Calls placed on hold (1x per call) + - Fixed Session Aggregation type definitions + +- ChatBox Text to File Description ([#24451](https://github.com/RocketChat/Rocket.Chat/pull/24451) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + + The text content from chatbox goes to the file description when drag and drop a file. + +- Close modal on esc and outside click ([#24275](https://github.com/RocketChat/Rocket.Chat/pull/24275)) + + This is a QUICK change in order to close modals pressing Esc button and clicking outside of it **intentionally**. + +- CloudLoginModal visual consistency ([#24334](https://github.com/RocketChat/Rocket.Chat/pull/24334)) + + ### before + ![image](https://user-images.githubusercontent.com/27704687/151585064-dc6a1e29-9903-4241-8fbd-dfbe6c55fbef.png) + + ### after + ![Screen Shot 2022-01-28 at 13 32 02](https://user-images.githubusercontent.com/27704687/151585101-75b98502-9aae-4198-bc3e-4956750e5d8b.png) + +- Convert tag edit with department data to tsx ([#24369](https://github.com/RocketChat/Rocket.Chat/pull/24369) by [@LucasFASouza](https://github.com/LucasFASouza)) + +- Descriptive tooltip for Encrypted Key on Room Header ([#24121](https://github.com/RocketChat/Rocket.Chat/pull/24121)) + +- Improve active/hover colors in account sidebar ([#25024](https://github.com/RocketChat/Rocket.Chat/pull/25024) by [@Himanshu664](https://github.com/Himanshu664)) + +- New omnichannel statistics and async statistics processing. ([#24749](https://github.com/RocketChat/Rocket.Chat/pull/24749)) + + https://app.clickup.com/t/1z4zg4e + +- OTR system messages ([#24382](https://github.com/RocketChat/Rocket.Chat/pull/24382)) + + OTR system messages to indicate key refresh and joining chat to users. + +- Performance for some Omnichannel features ([#25217](https://github.com/RocketChat/Rocket.Chat/pull/25217)) + +- Purchase Type Filter for marketplace apps and Categories filter anchor refactoring ([#24454](https://github.com/RocketChat/Rocket.Chat/pull/24454)) + + Implemented a filter by purchase type(free or paid) component for the apps screen of the marketplace. Besides that, new entries on the dictionary, fixed some parts of the App type (purchaseType was typed as unknown and price as string), and created some helpers to work alongside the filter. Will be refactoring the categories filter anchor and then will open this PR for reviews. + + Demo gif: + ![purchaseTypeFIlter](https://user-images.githubusercontent.com/43561537/153101228-7b7ebdc3-2d34-420f-aa9d-f7cbc8d4b53f.gif) + + Refactored the categories filter anchor from a plain fuselage select to a select button with dynamic colors. + Demo gif: + ![New categories filter anchor(PR)](https://user-images.githubusercontent.com/43561537/153422427-28012b7d-e0ec-45f4-861d-c9368c57ad04.gif) + +- Rename upgrade tab routes ([#25097](https://github.com/RocketChat/Rocket.Chat/pull/25097)) + + Change 'upgrade tab' routes names from camelCase ('goFullyFeatured') to kebab-case ('go-fully-featured') due to URL naming consistency. Changed types, main function and test. + +- Replace AutoComplete in UserAutoComplete & UserAutoCompleteMultiple components ([#24529](https://github.com/RocketChat/Rocket.Chat/pull/24529)) + + This PR replaces a deprecated fuselage's component `AutoComplete` in favor of `Select` and `MultiSelect` which fixes some of UX/UI issues in selecting users + + ### before + ![Screen Shot 2022-02-19 at 13 33 28](https://user-images.githubusercontent.com/27704687/154809737-8181a06c-4f20-48ea-90f7-01e828b9a452.png) + + ### after + ![Screen Shot 2022-02-19 at 13 30 58](https://user-images.githubusercontent.com/27704687/154809653-a8ec9a80-c0dd-4a25-9c00-0f96147d79e9.png) + +- Skip encryption for slash commands in E2E rooms ([#24475](https://github.com/RocketChat/Rocket.Chat/pull/24475)) + + Currently Slash Commands don't work in an E2EE room, as we encrypt the message before slash command is detected by the server, So removed encryption for slash commands in e2e rooms. + +- Team system messages feedback ([#24209](https://github.com/RocketChat/Rocket.Chat/pull/24209) by [@ostjen](https://github.com/ostjen)) + + - Delete some keys that aren't being used (eg: User_left_female). + - Add new Teams' system messages: + - `added-user-to-team`: **added** @\user to this Team; + - `removed-user-from-team`: **removed** @\user from this Team; + - `user-converted-to-team`: **converted** #\room to a Team; + - `user-converted-to-channel`: **converted** #\room to a Channel; + - `user-removed-room-from-team`: **removed** @\user from this Team; + - `user-deleted-room-from-team`: **deleted** #\room from this Team; + - `user-added-room-to-team`: **deleted** #\room to this Team; + - Add the corresponding options to hide each new system message and the missing `ujt` and `ult` hide options. + +- Updated links in readme ([#24028](https://github.com/RocketChat/Rocket.Chat/pull/24028) by [@aswinidev](https://github.com/aswinidev)) + +### 🐛 Bug fixes + + +- "Match error" when converting a team to a channel ([#24629](https://github.com/RocketChat/Rocket.Chat/pull/24629)) + + - Fix "Match error" when trying to convert a channel to a team; + +- **ENTERPRISE:** Auto reload feature of ddp-streamer micro service ([#24793](https://github.com/RocketChat/Rocket.Chat/pull/24793)) + +- **ENTERPRISE:** DDP streamer not sending data to all clients ([#24738](https://github.com/RocketChat/Rocket.Chat/pull/24738)) + +- **ENTERPRISE:** Notifications not being sent by ddp-streamer ([#24831](https://github.com/RocketChat/Rocket.Chat/pull/24831)) + +- **ENTERPRISE:** Presence micro service logic ([#24724](https://github.com/RocketChat/Rocket.Chat/pull/24724)) + +- 2FA via email when logging in using OAuth ([#24572](https://github.com/RocketChat/Rocket.Chat/pull/24572)) + +- Add ?close to OAuth callback url ([#24381](https://github.com/RocketChat/Rocket.Chat/pull/24381)) + +- Add katex render to new message react template ([#25239](https://github.com/RocketChat/Rocket.Chat/pull/25239)) + +- Add reaction not working in legacy messages ([#25222](https://github.com/RocketChat/Rocket.Chat/pull/25222)) + +- Added invalid password error message ([#24714](https://github.com/RocketChat/Rocket.Chat/pull/24714) by [@Himanshu664](https://github.com/Himanshu664)) + +- Adjust email label in Setup Wizard i18n files ([#25260](https://github.com/RocketChat/Rocket.Chat/pull/25260)) + + - remove 'Company' label on onboarding email keys in certain languages + +- AgentOverview analytics wrong departmentId parameter ([#25073](https://github.com/RocketChat/Rocket.Chat/pull/25073) by [@paulobernardoaf](https://github.com/paulobernardoaf)) + + When filtering the analytics charts by department, data would not appear because the object: + ```js + { + value: "department-id", + label: "department-name" + } + ``` + was being used in the `departmentId` parameter. + + - Before: + ![image](https://user-images.githubusercontent.com/30026625/161832057-d96ffd21-a7dd-421e-bfaa-3b9f4a9127b2.png) + + - After: + ![image](https://user-images.githubusercontent.com/30026625/161831092-9ee77b51-b083-4f45-9c48-ab2e0511c4d6.png) + +- API Error preventing adding an email to users without one (like bot/app users) ([#24709](https://github.com/RocketChat/Rocket.Chat/pull/24709)) + +- Apple OAuth ([#24879](https://github.com/RocketChat/Rocket.Chat/pull/24879)) + +- auto-join team channels not honoring user preferences ([#24779](https://github.com/RocketChat/Rocket.Chat/pull/24779) by [@ostjen](https://github.com/ostjen)) + +- Client disconnection on network loss ([#25170](https://github.com/RocketChat/Rocket.Chat/pull/25170)) + + Agent gets disconnected (or Unregistered) from asterisk in multiple ways. The goal is that agent should remain online + unless agent explicitly logs off. + Agent can stop receiving calls in multiple ways due to network loss. Network loss can happen in following ways. + 1. User tries to switch the network. User experiences a glitch of disconnectivity. This can be simulated by turning the network off + in the network tab of chrome's dev tool. This can disconnect the UA if the disconnection happens just before the registration refresh. + 2. Second reason is when computer goes in sleep mode. + 3. Third reason is that when asterisk is crashed/in maintenance mode/explicitly stopped. + + Solution: + The idea is to detect the network disconnection and start the start the attempts to reconnect. + The detection of the disconnection does not happen in case#1. The SIPUA's UserAgent transport does not + call onDisconnected when network loss of such kind happens. To tackle this problem, window's online and offline event handlers are + used. + + The number of retries is configurable but ideally it is to be kept at -1. Whenever disconnection happens, it should keep on trying to + reconnect with increasing backoff time. This behaviour is useful when the asterisk is stopped. + + When the server is disconnected, it should be indicated on the phone button. + +- Close room when dismiss wrap up call modal ([#25056](https://github.com/RocketChat/Rocket.Chat/pull/25056)) + +- Custom sound error toast messages ([#24515](https://github.com/RocketChat/Rocket.Chat/pull/24515) by [@Himanshu664](https://github.com/Himanshu664)) + +- Database indexes not being created ([#25101](https://github.com/RocketChat/Rocket.Chat/pull/25101)) + +- Date Message Export Filter Fix ([#24542](https://github.com/RocketChat/Rocket.Chat/pull/24542) by [@eduardofcabrera](https://github.com/eduardofcabrera)) + + Fix message export filter to get all messages between "from date" and "to date", including "to date". + +- DDP Rate Limiter Translation key ([#24898](https://github.com/RocketChat/Rocket.Chat/pull/24898)) + + Before: + image + + + Now: + image + +- DDP streamer errors ([#24710](https://github.com/RocketChat/Rocket.Chat/pull/24710)) + +- Duplicated "jump to message" button on starred messages ([#24867](https://github.com/RocketChat/Rocket.Chat/pull/24867) by [@Himanshu664](https://github.com/Himanshu664)) + +- Dynamic load matrix is enabled and handle failure ([#25495](https://github.com/RocketChat/Rocket.Chat/pull/25495)) + +- End call button disappearing when on-hold ([#24936](https://github.com/RocketChat/Rocket.Chat/pull/24936)) + +- External search providers not working ([#24860](https://github.com/RocketChat/Rocket.Chat/pull/24860) by [@tkurz](https://github.com/tkurz)) +- Full error message is visible ([#24856](https://github.com/RocketChat/Rocket.Chat/pull/24856) by [@Himanshu664](https://github.com/Himanshu664)) -- High CPU usage caused by CallProvider ([#24994](https://github.com/RocketChat/Rocket.Chat/pull/24994)) +- GDPR action to forget visitor data on request ([#24441](https://github.com/RocketChat/Rocket.Chat/pull/24441)) - Remove infinity loop inside useVoipClient hook. - - #closes #24970 +- German translation for Monitore ([#24785](https://github.com/RocketChat/Rocket.Chat/pull/24785) by [@JMoVS](https://github.com/JMoVS)) -- Multiple issues starting a new DM ([#24955](https://github.com/RocketChat/Rocket.Chat/pull/24955)) +- Handle Other Formats inside Upload Avatar ([#24226](https://github.com/RocketChat/Rocket.Chat/pull/24226)) - When the room object is searched for the first time, it does not exist on the front object yet (subscription), adding a fallback search for room list will guarantee to search the room details. + After resolving issue #24213 : - before: - https://user-images.githubusercontent.com/9275105/160223241-d2319f3e-82c5-47d6-867f-695ab2361a17.mp4 - after: - https://user-images.githubusercontent.com/9275105/160223244-84d0d2a1-3d95-464d-8b8a-e264b0d4d690.mp4 + https://user-images.githubusercontent.com/53515714/150325012-91413025-786e-4ce0-ae75-629f6b05b024.mp4 -
-🔍 Minor changes +- Ignore customClass on messages ([#24845](https://github.com/RocketChat/Rocket.Chat/pull/24845)) +- Implement client errors on ddp-streamer ([#24310](https://github.com/RocketChat/Rocket.Chat/pull/24310)) -- Chore: Update Livechat ([#24990](https://github.com/RocketChat/Rocket.Chat/pull/24990)) +- Inconsistent validation of user's access to rooms ([#24037](https://github.com/RocketChat/Rocket.Chat/pull/24037) by [@ostjen](https://github.com/ostjen)) -- Release 4.5.5 ([#24998](https://github.com/RocketChat/Rocket.Chat/pull/24998)) +- Incorrect websocket url in livechat widget ([#25261](https://github.com/RocketChat/Rocket.Chat/pull/25261)) -
+- Initial User not added to default channel ([#25544](https://github.com/RocketChat/Rocket.Chat/pull/25544)) -### 👩‍💻👨‍💻 Core Team 🤓 + If injecting initial user. The user wasn’t added to the default General channel -- [@MartinSchoeler](https://github.com/MartinSchoeler) -- [@filipemarins](https://github.com/filipemarins) -- [@ggazzo](https://github.com/ggazzo) -- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) -- [@sampaiodiego](https://github.com/sampaiodiego) -- [@tiagoevanp](https://github.com/tiagoevanp) +- Issues on selecting users when importing CSV ([#24253](https://github.com/RocketChat/Rocket.Chat/pull/24253)) -# 4.5.4 -`2022-03-24 · 1 🐛 · 1 🔍 · 3 👩‍💻👨‍💻` + * Fix users selecting by fixing their _id + * Add condition to disable 'Start importing' button if `usersCount`, `channelsCount` and `messageCount` equals 0, or if messageCount is alone + * Remove `disabled={usersCount === 0}` on user Tab -### Engine versions -- Node: `14.18.3` -- NPM: `6.14.15` -- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` -- Apps-Engine: `1.31.0` +- LDAP avatars being rotated according to metadata even if the setting to rotate uploads is off ([#24320](https://github.com/RocketChat/Rocket.Chat/pull/24320)) -### 🐛 Bug fixes + - Use the `FileUpload_RotateImages` setting (**Administration > File Upload > Rotate images on upload**) to control whether avatars should be rotated automatically based on their data (XEIF); + - Display the avatar image preview (orientation) according to the `FileUpload_RotateImages` setting. +- LDAP sync removing users from channels when multiple groups are mapped to it ([#25434](https://github.com/RocketChat/Rocket.Chat/pull/25434)) -- SAML Force name to string ([#24930](https://github.com/RocketChat/Rocket.Chat/pull/24930)) +- Message menu action not working on legacy messages. ([#25148](https://github.com/RocketChat/Rocket.Chat/pull/25148)) -
-🔍 Minor changes +- Message preview not available for queued chats ([#25092](https://github.com/RocketChat/Rocket.Chat/pull/25092)) +- Missing dependency on useEffect at CallProvider ([#24882](https://github.com/RocketChat/Rocket.Chat/pull/24882)) -- Release 4.5.4 ([#24938](https://github.com/RocketChat/Rocket.Chat/pull/24938)) +- Nextcloud OAuth for incomplete token URL ([#24476](https://github.com/RocketChat/Rocket.Chat/pull/24476)) -
+- OAuth mismatch redirect_uri error ([#24450](https://github.com/RocketChat/Rocket.Chat/pull/24450)) -### 👩‍💻👨‍💻 Core Team 🤓 +- Oembed request not respecting payload limit ([#24418](https://github.com/RocketChat/Rocket.Chat/pull/24418)) -- [@AllanPazRibeiro](https://github.com/AllanPazRibeiro) -- [@geekgonecrazy](https://github.com/geekgonecrazy) -- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- Omnichannel managers can't join chats in progress ([#24553](https://github.com/RocketChat/Rocket.Chat/pull/24553)) -# 4.5.3 -`2022-03-21 · 2 🚀 · 8 🐛 · 1 🔍 · 5 👩‍💻👨‍💻` +- One of the triggers was not working correctly ([#25409](https://github.com/RocketChat/Rocket.Chat/pull/25409)) -### Engine versions -- Node: `14.18.3` -- NPM: `6.14.15` -- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` -- Apps-Engine: `1.31.0` +- Outgoing webhook without scripts not saving messages ([#24401](https://github.com/RocketChat/Rocket.Chat/pull/24401)) -### 🚀 Improvements +- Prevent Apps Bridge to remove visitor status from room ([#24305](https://github.com/RocketChat/Rocket.Chat/pull/24305)) +- Prevent call button toggle when user is on call ([#24758](https://github.com/RocketChat/Rocket.Chat/pull/24758)) -- Standarize queue behavior for managers and agents when subscribing ([#24837](https://github.com/RocketChat/Rocket.Chat/pull/24837)) +- Prevent sequential messages edited icon to hide on hover ([#24984](https://github.com/RocketChat/Rocket.Chat/pull/24984)) -- UX - VoIP Call Component ([#24748](https://github.com/RocketChat/Rocket.Chat/pull/24748)) + ### before + Screen Shot 2022-03-29 at 13 35 56 + + ### after + Screen Shot 2022-03-29 at 11 48 05 -### 🐛 Bug fixes +- Prune Message issue ([#24424](https://github.com/RocketChat/Rocket.Chat/pull/24424)) +- Push privacy config to not show username not being respected ([#24606](https://github.com/RocketChat/Rocket.Chat/pull/24606)) -- **VOIP:** SidebarFooter component ([#24838](https://github.com/RocketChat/Rocket.Chat/pull/24838)) +- Read receipts show with color gray when not read yet ([#25244](https://github.com/RocketChat/Rocket.Chat/pull/25244)) - - Improve the CallProvider code; - - Adjust the text case of the VoIP component on the FooterSidebar; - - Fix the bad behavior with the changes in queue's name. +- Read receipts showing before message read ([#25216](https://github.com/RocketChat/Rocket.Chat/pull/25216)) -- Broken build caused by PRs modifying same file differently ([#24863](https://github.com/RocketChat/Rocket.Chat/pull/24863)) +- Read receipts showing first messages of the room as read even if not read by everyone ([#24508](https://github.com/RocketChat/Rocket.Chat/pull/24508)) -- Custom script not being fired ([#24901](https://github.com/RocketChat/Rocket.Chat/pull/24901)) +- Register with Secret URL ([#24921](https://github.com/RocketChat/Rocket.Chat/pull/24921)) -- Disable voip button when call is in progress ([#24864](https://github.com/RocketChat/Rocket.Chat/pull/24864)) +- Replace encrypted text to Encrypted Message Placeholder ([#24166](https://github.com/RocketChat/Rocket.Chat/pull/24166)) -- Show call icon only when user has extension associated ([#24752](https://github.com/RocketChat/Rocket.Chat/pull/24752)) + ### before + ![image](https://user-images.githubusercontent.com/27704687/150807900-154a9cdb-ee13-4333-8628-f287ab914b40.png) + + ### after + Screenshot 2022-01-13 at 8 57 47 PM -- Show only enabled departments on forward ([#24829](https://github.com/RocketChat/Rocket.Chat/pull/24829)) +- Reply button behavior on broadcast channel ([#25175](https://github.com/RocketChat/Rocket.Chat/pull/25175)) -- VoIP button gets disabled whenever user status changes ([#24789](https://github.com/RocketChat/Rocket.Chat/pull/24789)) + Hide reply button for the user that sent the message -- Wrong param usage on queue summary call ([#24799](https://github.com/RocketChat/Rocket.Chat/pull/24799)) +- respect `Accounts_Registration_Users_Default_Roles` setting ([#24173](https://github.com/RocketChat/Rocket.Chat/pull/24173)) -
-🔍 Minor changes + - Fix `user` role being added as default regardless of the `Accounts_Registration_Users_Default_Roles` setting. +- Room archived/unarchived system messages aren't sent when editing room settings ([#24897](https://github.com/RocketChat/Rocket.Chat/pull/24897)) -- Chore: Fix MongoDB versions on release notes ([#24877](https://github.com/RocketChat/Rocket.Chat/pull/24877)) + - Send the "Room archived" and "Room unarchived" system messages when editing room settings (and not only when rooms are archived/unarchived with the slash-command); + - Fix the "Hide System Messages" option for the "Room archived" and "Room unarchived" system messages; -
+- Room context tabs not working in Omnichannel current chats page ([#24559](https://github.com/RocketChat/Rocket.Chat/pull/24559)) -### 👩‍💻👨‍💻 Core Team 🤓 +- room creation fails if app framework is disabled ([#25200](https://github.com/RocketChat/Rocket.Chat/pull/25200)) -- [@KevLehman](https://github.com/KevLehman) -- [@amolghode1981](https://github.com/amolghode1981) -- [@ggazzo](https://github.com/ggazzo) -- [@sampaiodiego](https://github.com/sampaiodiego) -- [@tiagoevanp](https://github.com/tiagoevanp) +- Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) -# 4.5.2 -`2022-03-12 · 1 🚀 · 7 🐛 · 1 🔍 · 8 👩‍💻👨‍💻` +- Several issues related to custom roles ([#24052](https://github.com/RocketChat/Rocket.Chat/pull/24052)) -### Engine versions -- Node: `14.18.3` -- NPM: `6.14.15` -- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` -- Apps-Engine: `1.31.0` + - Throw an error when trying to delete a role (User or Subscription role) that are still being used; + - Fix "Invalid Role" error for custom roles in Role Editing sidebar; + - Fix "Users in Role" screen for custom roles. -### 🚀 Improvements +- Showing Blank Message Inside Report ([#25007](https://github.com/RocketChat/Rocket.Chat/pull/25007)) + https://user-images.githubusercontent.com/53515714/161038085-4a86c7ae-6751-4996-9767-b1c9e0331a6c.mp4 -- Voip Extensions disabled state ([#24750](https://github.com/RocketChat/Rocket.Chat/pull/24750)) +- Skip admin info in setup wizard for servers with admin registered ([#24485](https://github.com/RocketChat/Rocket.Chat/pull/24485)) -### 🐛 Bug fixes +- Skip cloud steps for registered servers on setup wizard ([#24407](https://github.com/RocketChat/Rocket.Chat/pull/24407)) +- Slash commands previews not working ([#24387](https://github.com/RocketChat/Rocket.Chat/pull/24387) by [@ostjen](https://github.com/ostjen)) -- "livechat/webrtc.call" endpoint not working ([#24804](https://github.com/RocketChat/Rocket.Chat/pull/24804)) +- Spotlight results showing usernames instead of real names ([#25471](https://github.com/RocketChat/Rocket.Chat/pull/25471)) -- `PaginatedSelectFiltered` not handling changes ([#24732](https://github.com/RocketChat/Rocket.Chat/pull/24732)) +- Startup errors creating indexes ([#24409](https://github.com/RocketChat/Rocket.Chat/pull/24409)) -- Broken multiple OAuth integrations ([#24705](https://github.com/RocketChat/Rocket.Chat/pull/24705)) + Fix `bio` and `prid` startup index creation errors. -- Critical: Incorrect visitor getting assigned to a chat from apps ([#24805](https://github.com/RocketChat/Rocket.Chat/pull/24805)) +- Toolbox hiding under contextual bar ([#25237](https://github.com/RocketChat/Rocket.Chat/pull/25237)) -- Opening a new DM from user card ([#24623](https://github.com/RocketChat/Rocket.Chat/pull/24623)) +- typo on register server tooltip of setup wizard ([#24466](https://github.com/RocketChat/Rocket.Chat/pull/24466)) - A race condition on `useRoomIcon` -- delayed merge of rooms and subscriptions -- was causing a UI crash whenever someone tried to open a DM from the user card component. +- UI/UX issues on Live Chat widget ([#25407](https://github.com/RocketChat/Rocket.Chat/pull/25407)) -- Revert AutoComplete ([#24812](https://github.com/RocketChat/Rocket.Chat/pull/24812)) +- Use correct room property for call ended at ([#24932](https://github.com/RocketChat/Rocket.Chat/pull/24932)) -- VoipExtensionsPage component call ([#24792](https://github.com/RocketChat/Rocket.Chat/pull/24792)) +- User abandonment setting was not working doe to failing event hook ([#25520](https://github.com/RocketChat/Rocket.Chat/pull/25520)) -
-🔍 Minor changes + A setting watcher and the query for grabbing abandoned chats were broken, now they're not. +- UserCard sanitization ([#25089](https://github.com/RocketChat/Rocket.Chat/pull/25089)) -- Regression: Fix ParentRoomWithEndpointData in loop ([#24809](https://github.com/RocketChat/Rocket.Chat/pull/24809)) + - Rewrites the component to TS + - Fixes some visual issues + + ### before + ![Screen Shot 2022-04-07 at 00 23 11](https://user-images.githubusercontent.com/27704687/162113925-5c9484d1-23e9-4623-8b86-3fbc71b461a1.png) + + ### after + ![Screen Shot 2022-04-07 at 00 07 13](https://user-images.githubusercontent.com/27704687/162112353-afd6aac6-b27c-4470-a642-631b8080d59e.png) -
+- Video and Audio not skipping forward ([#19866](https://github.com/RocketChat/Rocket.Chat/pull/19866)) -### 👩‍💻👨‍💻 Core Team 🤓 +- VoIP disabled/enabled sequence puts voip agent in error state ([#25230](https://github.com/RocketChat/Rocket.Chat/pull/25230)) -- [@KevLehman](https://github.com/KevLehman) -- [@MartinSchoeler](https://github.com/MartinSchoeler) -- [@debdutdeb](https://github.com/debdutdeb) -- [@ggazzo](https://github.com/ggazzo) -- [@juliajforesti](https://github.com/juliajforesti) -- [@murtaza98](https://github.com/murtaza98) -- [@sampaiodiego](https://github.com/sampaiodiego) -- [@tassoevan](https://github.com/tassoevan) + Initially it was thought that the issue occurs because of the race condition while changing the client settings vs those settings reflected on server side. So a natural solution to solve this is to wait for setting change event 'private-settings-changed'. Then if 'VoIP_Enabled' is updated and it is true, set voipEnabled to true in useVoipClient.ts (on client side) + + It was realised that the race does not happen because of the database or server noticing the changes late. But because of the time taken to establish the AMI connection with Asterisk. + + Solution: + + 1. Change apps/meteor/app/voip/server/startup.ts. When VoIP_Enabled is changed, await for Voip.init() to complete and then broadcast connector.statuschanged with changed value. + 2. From apps/meteor/server/modules/listeners/listeners.module.ts use notifyLoggedInThisInstance to notify all logged in users on current instance. + 3. in apps/meteor/client/providers/CallProvider/hooks/useVoipClient.ts add the event handler that receives this event. Change voipEnabled from constant to state. Change this state based on the 'value' that is received by the handler. -# 4.5.1 -`2022-03-09 · 13 🐛 · 2 🔍 · 12 👩‍💻👨‍💻` +- Wrong business hour behavior ([#24896](https://github.com/RocketChat/Rocket.Chat/pull/24896)) -### Engine versions -- Node: `14.18.3` -- NPM: `6.14.15` -- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` -- Apps-Engine: `1.31.0` +
+🔍 Minor changes -### 🐛 Bug fixes +- Bump @rocket.chat/emitter from 0.31.4 to 0.31.9 in /ee/server/services ([#25021](https://github.com/RocketChat/Rocket.Chat/pull/25021) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Apple login script being loaded even when Apple Login is disabled. ([#24760](https://github.com/RocketChat/Rocket.Chat/pull/24760)) +- Bump @rocket.chat/message-parser from 0.31.4 to 0.31.9 in /ee/server/services ([#25019](https://github.com/RocketChat/Rocket.Chat/pull/25019) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Components for user search ([#24677](https://github.com/RocketChat/Rocket.Chat/pull/24677)) +- Bump @rocket.chat/string-helpers from 0.31.4 to 0.31.9 in /ee/server/services ([#25018](https://github.com/RocketChat/Rocket.Chat/pull/25018) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Duplicated 'name' log key ([#24590](https://github.com/RocketChat/Rocket.Chat/pull/24590)) +- Bump @rocket.chat/ui-kit from 0.31.4 to 0.31.9 in /ee/server/services ([#25020](https://github.com/RocketChat/Rocket.Chat/pull/25020) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Missing username on messages imported from Slack ([#24674](https://github.com/RocketChat/Rocket.Chat/pull/24674)) +- Bump @types/clipboard from 2.0.1 to 2.0.7 ([#24832](https://github.com/RocketChat/Rocket.Chat/pull/24832) by [@dependabot[bot]](https://github.com/dependabot[bot])) - - Fix missing sender's username on messages imported from Slack. +- Bump @types/mailparser from 3.0.2 to 3.4.0 ([#24833](https://github.com/RocketChat/Rocket.Chat/pull/24833) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- no id of room closer in livechat-close message ([#24683](https://github.com/RocketChat/Rocket.Chat/pull/24683)) +- Bump @types/nodemailer from 6.4.2 to 6.4.4 ([#24822](https://github.com/RocketChat/Rocket.Chat/pull/24822) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Reload roomslist after successful deletion of a room from admin panel. ([#23795](https://github.com/RocketChat/Rocket.Chat/pull/23795) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari)) +- Bump @types/ws from 8.2.2 to 8.2.3 in /ee/server/services ([#24556](https://github.com/RocketChat/Rocket.Chat/pull/24556) by [@dependabot[bot]](https://github.com/dependabot[bot])) - Removed the logic for calling the `rooms.adminRooms` endPoint from the `RoomsTable` Component and moved it to its parent component `RoomsPage`. - This allows to call the endPoint `rooms.adminRooms` from `EditRoomContextBar` Component which is also has `RoomPage` Component as its parent. - - Also added a succes toast message after the successful deletion of room. +- Bump @types/ws from 8.2.3 to 8.5.2 in /ee/server/services ([#24666](https://github.com/RocketChat/Rocket.Chat/pull/24666) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Room's message count not being incremented on import ([#24696](https://github.com/RocketChat/Rocket.Chat/pull/24696)) +- Bump @types/ws from 8.5.2 to 8.5.3 in /ee/server/services ([#24820](https://github.com/RocketChat/Rocket.Chat/pull/24820) by [@dependabot[bot]](https://github.com/dependabot[bot])) - - Fix rooms' message counter not being incremented on message import. +- Bump actions/checkout from 2 to 3 ([#24668](https://github.com/RocketChat/Rocket.Chat/pull/24668) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Show only available agents on extension association modal ([#24680](https://github.com/RocketChat/Rocket.Chat/pull/24680)) +- Bump actions/setup-node from 2 to 3 ([#24642](https://github.com/RocketChat/Rocket.Chat/pull/24642) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- System messages are sent when adding or removing a group from a team ([#24743](https://github.com/RocketChat/Rocket.Chat/pull/24743)) +- Bump adm-zip from 0.4.14 to 0.5.9 ([#24538](https://github.com/RocketChat/Rocket.Chat/pull/24538) by [@dependabot[bot]](https://github.com/dependabot[bot])) - - Do not send system messages when adding or removing a new or existing _group_ from a team. +- Bump body-parser from 1.19.0 to 1.19.1 in /ee/server/services ([#23963](https://github.com/RocketChat/Rocket.Chat/pull/23963) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Typo and placeholder on wrap up call modal ([#24737](https://github.com/RocketChat/Rocket.Chat/pull/24737)) +- Bump body-parser from 1.19.0 to 1.19.2 ([#24821](https://github.com/RocketChat/Rocket.Chat/pull/24821) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Typo in wrap-up term ([#24661](https://github.com/RocketChat/Rocket.Chat/pull/24661)) +- Bump body-parser from 1.19.1 to 1.19.2 in /ee/server/services ([#24517](https://github.com/RocketChat/Rocket.Chat/pull/24517) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump body-parser from 1.19.2 to 1.20.0 in /ee/server/services ([#25042](https://github.com/RocketChat/Rocket.Chat/pull/25042) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump cookie from 0.4.1 to 0.4.2 in /ee/server/services ([#24472](https://github.com/RocketChat/Rocket.Chat/pull/24472) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump date-fns from 2.24.0 to 2.28.0 ([#24058](https://github.com/RocketChat/Rocket.Chat/pull/24058) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump ejson from 2.2.1 to 2.2.2 ([#25057](https://github.com/RocketChat/Rocket.Chat/pull/25057) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump eslint-plugin-anti-trojan-source from 1.0.6 to 1.1.0 ([#25076](https://github.com/RocketChat/Rocket.Chat/pull/25076) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- VoIP Enable/Disable setting on CallContext/CallProvider Notifications ([#24607](https://github.com/RocketChat/Rocket.Chat/pull/24607)) +- Bump express from 4.17.1 to 4.17.2 in /ee/server/services ([#24469](https://github.com/RocketChat/Rocket.Chat/pull/24469) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Voip Stream Reinitialization Error ([#24657](https://github.com/RocketChat/Rocket.Chat/pull/24657)) +- Bump express from 4.17.2 to 4.17.3 in /ee/server/services ([#24522](https://github.com/RocketChat/Rocket.Chat/pull/24522) by [@dependabot[bot]](https://github.com/dependabot[bot])) -
-🔍 Minor changes +- Bump follow-redirects from 1.14.7 to 1.14.8 in /ee/server/services ([#24491](https://github.com/RocketChat/Rocket.Chat/pull/24491) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Bump is-svg from 4.3.1 to 4.3.2 ([#24801](https://github.com/RocketChat/Rocket.Chat/pull/24801) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Chore: Update Livechat ([#24754](https://github.com/RocketChat/Rocket.Chat/pull/24754)) +- Bump jaeger-client from 3.18.1 to 3.19.0 in /ee/server/services ([#23961](https://github.com/RocketChat/Rocket.Chat/pull/23961) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Release 4.5.1 ([#24782](https://github.com/RocketChat/Rocket.Chat/pull/24782) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari) & [@cuonghuunguyen](https://github.com/cuonghuunguyen)) +- Bump jschardet from 1.6.0 to 3.0.0 ([#23121](https://github.com/RocketChat/Rocket.Chat/pull/23121) by [@dependabot[bot]](https://github.com/dependabot[bot])) -
+- Bump minimist from 1.2.5 to 1.2.6 in /ee/server/services ([#24991](https://github.com/RocketChat/Rocket.Chat/pull/24991) by [@dependabot[bot]](https://github.com/dependabot[bot])) -### 👩‍💻👨‍💻 Contributors 😍 +- Bump pino and pino-pretty ([#25052](https://github.com/RocketChat/Rocket.Chat/pull/25052)) -- [@Aman-Maheshwari](https://github.com/Aman-Maheshwari) -- [@cuonghuunguyen](https://github.com/cuonghuunguyen) +- Bump pino from 7.8.0 to 7.8.1 in /ee/server/services ([#24783](https://github.com/RocketChat/Rocket.Chat/pull/24783) by [@dependabot[bot]](https://github.com/dependabot[bot])) -### 👩‍💻👨‍💻 Core Team 🤓 +- Bump pino from 7.8.1 to 7.9.1 in /ee/server/services ([#24869](https://github.com/RocketChat/Rocket.Chat/pull/24869) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- [@KevLehman](https://github.com/KevLehman) -- [@MartinSchoeler](https://github.com/MartinSchoeler) -- [@amolghode1981](https://github.com/amolghode1981) -- [@juliajforesti](https://github.com/juliajforesti) -- [@matheusbsilva137](https://github.com/matheusbsilva137) -- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) -- [@renatobecker](https://github.com/renatobecker) -- [@sampaiodiego](https://github.com/sampaiodiego) -- [@tassoevan](https://github.com/tassoevan) -- [@tiagoevanp](https://github.com/tiagoevanp) +- Bump pino-pretty from 7.5.1 to 7.5.2 in /ee/server/services ([#24689](https://github.com/RocketChat/Rocket.Chat/pull/24689) by [@dependabot[bot]](https://github.com/dependabot[bot])) -# 4.5.0 -`2022-02-28 · 3 🎉 · 15 🚀 · 19 🐛 · 72 🔍 · 30 👩‍💻👨‍💻` +- Bump pino-pretty from 7.5.2 to 7.5.3 in /ee/server/services ([#24698](https://github.com/RocketChat/Rocket.Chat/pull/24698) by [@dependabot[bot]](https://github.com/dependabot[bot])) -### Engine versions -- Node: `14.18.3` -- NPM: `6.14.15` -- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` -- Apps-Engine: `1.31.0` +- Bump pino-pretty from 7.5.3 to 7.5.4 in /ee/server/services ([#24870](https://github.com/RocketChat/Rocket.Chat/pull/24870) by [@dependabot[bot]](https://github.com/dependabot[bot])) -### 🎉 New features +- Bump pm2 from 5.1.2 to 5.2.0 in /ee/server/services ([#24537](https://github.com/RocketChat/Rocket.Chat/pull/24537) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Bump prometheus-gc-stats from 0.6.2 to 0.6.3 ([#24803](https://github.com/RocketChat/Rocket.Chat/pull/24803) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- E2E password generator ([#24114](https://github.com/RocketChat/Rocket.Chat/pull/24114) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) +- Bump simple-get from 4.0.0 to 4.0.1 ([#24341](https://github.com/RocketChat/Rocket.Chat/pull/24341) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Marketplace sort filter ([#24567](https://github.com/RocketChat/Rocket.Chat/pull/24567) by [@ujorgeleite](https://github.com/ujorgeleite)) +- Bump sodium-native from 3.2.1 to 3.3.0 in /ee/server/services ([#23512](https://github.com/RocketChat/Rocket.Chat/pull/23512) by [@dependabot[bot]](https://github.com/dependabot[bot])) - Implemented a sort filter for the marketplace screen. This component sorts the marketplace apps list in 4 ways, alphabetical order(A-Z), inverse alphabetical order(Z-A), most recently updated(MRU), and least recent updated(LRU). Besides that, I've generalized some components and types to increase code reusability, renamed some helpers as well as deleted some useless ones, and inserted the necessary new translations on the English i18n dictionary. - Demo gif: - ![Marketplace sort filter](https://user-images.githubusercontent.com/43561537/155033709-e07a6306-a85a-4f7f-9624-b53ba5dd7fa9.gif) +- Bump template-file from 6.0.0 to 6.0.1 ([#25002](https://github.com/RocketChat/Rocket.Chat/pull/25002) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- VoIP Support for Omnichannel ([#23102](https://github.com/RocketChat/Rocket.Chat/pull/23102)) +- Bump ts-node from 10.5.0 to 10.6.0 in /ee/server/services ([#24667](https://github.com/RocketChat/Rocket.Chat/pull/24667) by [@dependabot[bot]](https://github.com/dependabot[bot])) - - Created VoipService to manage VoIP connections and PBX connection - - Created LivechatVoipService that will handle custom cases for livechat (creating rooms, assigning chats to queue, actions when call is finished, etc) - - Created Basic interfaces to support new services and new model - - Created Endpoints for management interfaces - - Implemented asterisk connector on VoIP service - - Created UI components to show calls incoming and to allow answering/rejecting calls - - Added new settings to control call server/management server connection values - - Added endpoints to associate Omnichannel Agents with PBX Extensions - - Added support for event listening on server side, to get metadata about calls being received/ongoing - - Created new pages to update settings & to see user-extension association - - Created new page to see ongoing calls (and past calls) - - Added support for remote hangup/hold on calls - - Implemented call metrics calculation (hold time, waiting time, talk time) - - Show a notificaiton when call is received +- Bump ts-node from 10.6.0 to 10.7.0 in /ee/server/services ([#24716](https://github.com/RocketChat/Rocket.Chat/pull/24716) by [@dependabot[bot]](https://github.com/dependabot[bot])) -### 🚀 Improvements +- Bump underscore.string from 3.3.5 to 3.3.6 in /ee/server/services ([#24498](https://github.com/RocketChat/Rocket.Chat/pull/24498) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Bump url-parse from 1.5.3 to 1.5.7 ([#24528](https://github.com/RocketChat/Rocket.Chat/pull/24528) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- **ENTERPRISE:** Improve how micro services are loaded ([#24388](https://github.com/RocketChat/Rocket.Chat/pull/24388)) +- Bump url-parse from 1.5.7 to 1.5.10 ([#24640](https://github.com/RocketChat/Rocket.Chat/pull/24640) by [@dependabot[bot]](https://github.com/dependabot[bot])) -- Add return button in chats opened from the list of current chats ([#24458](https://github.com/RocketChat/Rocket.Chat/pull/24458) by [@LucasFASouza](https://github.com/LucasFASouza)) +- Bump vm2 from 3.9.5 to 3.9.7 in /ee/server/services ([#24509](https://github.com/RocketChat/Rocket.Chat/pull/24509) by [@dependabot[bot]](https://github.com/dependabot[bot])) - The new return button for Omnichannel chats came out with release 3.15 but the feature was only available for chats that were opened from Omnichannel Contact Center. - Now, the same UI/UX is supported for chats opened from Current Chats list. - - ![image](https://user-images.githubusercontent.com/32396925/153283190-bd5c9748-c36b-4874-a704-6043afc7e3a1.png) - - The chat now opens in the Omnichannel settings and has the return button so the user can go back to the Current Chats list. - - ![image](https://user-images.githubusercontent.com/32396925/153285591-fad8e4a0-d2ea-4a02-8b2a-15e383b3c876.png) +- Chore: `twoFactorRequired` signature ([#24518](https://github.com/RocketChat/Rocket.Chat/pull/24518)) -- Add tooltips on action buttons of Canned Response message composer ([#24483](https://github.com/RocketChat/Rocket.Chat/pull/24483) by [@LucasFASouza](https://github.com/LucasFASouza)) + Improved type checking for decorator `twoFactorRequired`. - The tooltips were missing on the action buttons of CR message composer. - - ![image](https://user-images.githubusercontent.com/32396925/153620327-91107245-4b47-4d39-a99a-6da6d1cf5734.png) - - Users can now feel more encouraged to use these actions knowing what they are supposed to do. +- Chore: Add description to global OTR setting ([#24333](https://github.com/RocketChat/Rocket.Chat/pull/24333) by [@pedrogssouza](https://github.com/pedrogssouza)) -- Add user to room on "Click to Join!" button press ([#24041](https://github.com/RocketChat/Rocket.Chat/pull/24041) by [@ostjen](https://github.com/ostjen)) +- Chore: Add E2E tests for livechat/room.close ([#24729](https://github.com/RocketChat/Rocket.Chat/pull/24729) by [@Muramatsu2602](https://github.com/Muramatsu2602)) - - Add user to room on "Click to Join!" button press; - - Display the "Join" button in discussions inside channels (keeping the behavior consistent with discussions inside groups). + * Create a new test suite file under tests/end-to-end/api/livechat + * Create tests for the following endpoint: + + ivechat/room.close -- Added a new "All" tab which shows all integrations in Integrations ([#24109](https://github.com/RocketChat/Rocket.Chat/pull/24109) by [@aswinidev](https://github.com/aswinidev)) +- Chore: Add E2E tests for livechat/visitor ([#24764](https://github.com/RocketChat/Rocket.Chat/pull/24764) by [@Muramatsu2602](https://github.com/Muramatsu2602)) -- ChatBox Text to File Description ([#24451](https://github.com/RocketChat/Rocket.Chat/pull/24451) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) + - Create a new test suite file under tests/end-to-end/api/livechat + - Create tests for the following endpoints: + + livechat/visitor (create visitor, update visitor, add custom fields to visitors) - The text content from chatbox goes to the file description when drag and drop a file. +- Chore: Add error boundary to message component ([#25223](https://github.com/RocketChat/Rocket.Chat/pull/25223)) -- Close modal on esc and outside click ([#24275](https://github.com/RocketChat/Rocket.Chat/pull/24275)) + Not crash the whole application if something goes wrong in the MessageList component. + + ![image](https://user-images.githubusercontent.com/40830821/162269915-931c5c3c-c979-4234-b74c-371f67467ce0.png) - This is a QUICK change in order to close modals pressing Esc button and clicking outside of it **intentionally**. +- Chore: Add Livechat repo into Monorepo packages ([#25312](https://github.com/RocketChat/Rocket.Chat/pull/25312)) -- CloudLoginModal visual consistency ([#24334](https://github.com/RocketChat/Rocket.Chat/pull/24334)) +- Chore: Add options to debug stdout and rate limiter ([#25336](https://github.com/RocketChat/Rocket.Chat/pull/25336)) - ### before - ![image](https://user-images.githubusercontent.com/27704687/151585064-dc6a1e29-9903-4241-8fbd-dfbe6c55fbef.png) - - ### after - ![Screen Shot 2022-01-28 at 13 32 02](https://user-images.githubusercontent.com/27704687/151585101-75b98502-9aae-4198-bc3e-4956750e5d8b.png) +- Chore: Add root package.json to houston files ([#25286](https://github.com/RocketChat/Rocket.Chat/pull/25286)) -- Convert tag edit with department data to tsx ([#24369](https://github.com/RocketChat/Rocket.Chat/pull/24369) by [@LucasFASouza](https://github.com/LucasFASouza)) + See title -- Descriptive tooltip for Encrypted Key on Room Header ([#24121](https://github.com/RocketChat/Rocket.Chat/pull/24121)) +- Chore: add some missing REST definitions ([#24925](https://github.com/RocketChat/Rocket.Chat/pull/24925) by [@gerzonc](https://github.com/gerzonc)) -- OTR system messages ([#24382](https://github.com/RocketChat/Rocket.Chat/pull/24382)) + On the [mobile client](https://github.com/RocketChat/Rocket.Chat.ReactNative), we made an effort to collect more `REST API` definitions that are missing on the server side during our migration to TypeScript. Since we're both migrating to TypeScript, we thought it would be a good idea to share those so you guys can benefit from our initiative. - OTR system messages to indicate key refresh and joining chat to users. +- Chore: Add yarn plugin to check node and yarn version ([#25224](https://github.com/RocketChat/Rocket.Chat/pull/25224)) -- Purchase Type Filter for marketplace apps and Categories filter anchor refactoring ([#24454](https://github.com/RocketChat/Rocket.Chat/pull/24454)) +- Chore: added Server Instances endpoint types ([#24507](https://github.com/RocketChat/Rocket.Chat/pull/24507)) - Implemented a filter by purchase type(free or paid) component for the apps screen of the marketplace. Besides that, new entries on the dictionary, fixed some parts of the App type (purchaseType was typed as unknown and price as string), and created some helpers to work alongside the filter. Will be refactoring the categories filter anchor and then will open this PR for reviews. - - Demo gif: - ![purchaseTypeFIlter](https://user-images.githubusercontent.com/43561537/153101228-7b7ebdc3-2d34-420f-aa9d-f7cbc8d4b53f.gif) - - Refactored the categories filter anchor from a plain fuselage select to a select button with dynamic colors. - Demo gif: - ![New categories filter anchor(PR)](https://user-images.githubusercontent.com/43561537/153422427-28012b7d-e0ec-45f4-861d-c9368c57ad04.gif) + Created typing for endpoint definitions on `instances.ts`. -- Replace AutoComplete in UserAutoComplete & UserAutoCompleteMultiple components ([#24529](https://github.com/RocketChat/Rocket.Chat/pull/24529)) +- Chore: added settings endpoint types ([#24506](https://github.com/RocketChat/Rocket.Chat/pull/24506)) - This PR replaces a deprecated fuselage's component `AutoComplete` in favor of `Select` and `MultiSelect` which fixes some of UX/UI issues in selecting users - - ### before - ![Screen Shot 2022-02-19 at 13 33 28](https://user-images.githubusercontent.com/27704687/154809737-8181a06c-4f20-48ea-90f7-01e828b9a452.png) - - ### after - ![Screen Shot 2022-02-19 at 13 30 58](https://user-images.githubusercontent.com/27704687/154809653-a8ec9a80-c0dd-4a25-9c00-0f96147d79e9.png) + Created typing for endpoint definitions on `settings.ts`. -- Skip encryption for slash commands in E2E rooms ([#24475](https://github.com/RocketChat/Rocket.Chat/pull/24475)) +- Chore: APIClass types ([#24747](https://github.com/RocketChat/Rocket.Chat/pull/24747)) - Currently Slash Commands don't work in an E2EE room, as we encrypt the message before slash command is detected by the server, So removed encryption for slash commands in e2e rooms. + This pull request creates a new `restivus` module (.d.ts) for the `api.js` file. -- Team system messages feedback ([#24209](https://github.com/RocketChat/Rocket.Chat/pull/24209) by [@ostjen](https://github.com/ostjen)) +- Chore: Bump fuselage ([#25371](https://github.com/RocketChat/Rocket.Chat/pull/25371)) - - Delete some keys that aren't being used (eg: User_left_female). - - Add new Teams' system messages: - - `added-user-to-team`: **added** @\user to this Team; - - `removed-user-from-team`: **removed** @\user from this Team; - - `user-converted-to-team`: **converted** #\room to a Team; - - `user-converted-to-channel`: **converted** #\room to a Channel; - - `user-removed-room-from-team`: **removed** @\user from this Team; - - `user-deleted-room-from-team`: **deleted** #\room from this Team; - - `user-added-room-to-team`: **deleted** #\room to this Team; - - Add the corresponding options to hide each new system message and the missing `ujt` and `ult` hide options. +- Chore: Bump Fuselage packages ([#25259](https://github.com/RocketChat/Rocket.Chat/pull/25259)) -### 🐛 Bug fixes +- Chore: Bump Fuselage packages ([#25015](https://github.com/RocketChat/Rocket.Chat/pull/25015)) + It uses the last stable version of Fuselage packages. -- 2FA via email when logging in using OAuth ([#24572](https://github.com/RocketChat/Rocket.Chat/pull/24572)) +- Chore: Bump Fuselage packages ([#24573](https://github.com/RocketChat/Rocket.Chat/pull/24573)) -- Add ?close to OAuth callback url ([#24381](https://github.com/RocketChat/Rocket.Chat/pull/24381)) + It uses the last stable version of Fuselage packages. -- GDPR action to forget visitor data on request ([#24441](https://github.com/RocketChat/Rocket.Chat/pull/24441)) +- Chore: bump fuselage version ([#24453](https://github.com/RocketChat/Rocket.Chat/pull/24453)) -- Implement client errors on ddp-streamer ([#24310](https://github.com/RocketChat/Rocket.Chat/pull/24310)) +- Chore: Cancel running jobs if PR is updated ([#24708](https://github.com/RocketChat/Rocket.Chat/pull/24708)) -- Inconsistent validation of user's access to rooms ([#24037](https://github.com/RocketChat/Rocket.Chat/pull/24037) by [@ostjen](https://github.com/ostjen)) +- Chore: Convert admin custom sound to tsx ([#25128](https://github.com/RocketChat/Rocket.Chat/pull/25128)) -- Issues on selecting users when importing CSV ([#24253](https://github.com/RocketChat/Rocket.Chat/pull/24253)) +- Chore: Convert JS files to Typescript ([#24410](https://github.com/RocketChat/Rocket.Chat/pull/24410)) - * Fix users selecting by fixing their _id - * Add condition to disable 'Start importing' button if `usersCount`, `channelsCount` and `messageCount` equals 0, or if messageCount is alone - * Remove `disabled={usersCount === 0}` on user Tab + This pull request converts 26 more files from Javascript to Typescript, to check variable types and increase validation on the code. -- OAuth mismatch redirect_uri error ([#24450](https://github.com/RocketChat/Rocket.Chat/pull/24450)) +- Chore: Convert LivechatAgentActivity to raw model and TS ([#25123](https://github.com/RocketChat/Rocket.Chat/pull/25123)) -- Oembed request not respecting payload limit ([#24418](https://github.com/RocketChat/Rocket.Chat/pull/24418)) +- Chore: Convert Mailer to TS ([#25121](https://github.com/RocketChat/Rocket.Chat/pull/25121)) -- Omnichannel managers can't join chats in progress ([#24553](https://github.com/RocketChat/Rocket.Chat/pull/24553)) +- Chore: Convert NotificationStatus to TS ([#25125](https://github.com/RocketChat/Rocket.Chat/pull/25125)) -- Outgoing webhook without scripts not saving messages ([#24401](https://github.com/RocketChat/Rocket.Chat/pull/24401)) +- Chore: Convert server functions from javascript to typescript ([#24384](https://github.com/RocketChat/Rocket.Chat/pull/24384)) -- Prevent Apps Bridge to remove visitor status from room ([#24305](https://github.com/RocketChat/Rocket.Chat/pull/24305)) + This pull request will be used to rewrite some functions on the Chat Engine to Typescript, in order to increase security and specify variable types on the code. -- Read receipts showing first messages of the room as read even if not read by everyone ([#24508](https://github.com/RocketChat/Rocket.Chat/pull/24508)) +- Chore: Convert to typescript the me slashCommands files ([#24321](https://github.com/RocketChat/Rocket.Chat/pull/24321) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) -- respect `Accounts_Registration_Users_Default_Roles` setting ([#24173](https://github.com/RocketChat/Rocket.Chat/pull/24173)) + Convert to typescript the me slashCommands files - - Fix `user` role being added as default regardless of the `Accounts_Registration_Users_Default_Roles` setting. +- Chore: Convert to typescript the mute and unmute slash commands files ([#24325](https://github.com/RocketChat/Rocket.Chat/pull/24325) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) -- Room context tabs not working in Omnichannel current chats page ([#24559](https://github.com/RocketChat/Rocket.Chat/pull/24559)) + Convert to typescript the mute and unmute slash commands files -- Skip admin info in setup wizard for servers with admin registered ([#24485](https://github.com/RocketChat/Rocket.Chat/pull/24485)) +- Chore: Convert to typescript the slash commands create files ([#24306](https://github.com/RocketChat/Rocket.Chat/pull/24306) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) -- Skip cloud steps for registered servers on setup wizard ([#24407](https://github.com/RocketChat/Rocket.Chat/pull/24407)) + Convert Slash Commands create files to typescript. -- Slash commands previews not working ([#24387](https://github.com/RocketChat/Rocket.Chat/pull/24387) by [@ostjen](https://github.com/ostjen)) +- Chore: Convert to typescript the slash commands invite files ([#24311](https://github.com/RocketChat/Rocket.Chat/pull/24311) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) -- Startup errors creating indexes ([#24409](https://github.com/RocketChat/Rocket.Chat/pull/24409)) + Convert to typescript the slash commands invite files - Fix `bio` and `prid` startup index creation errors. +- Chore: Convert to typescript the unarchive slash commands files ([#24331](https://github.com/RocketChat/Rocket.Chat/pull/24331) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) -- typo on register server tooltip of setup wizard ([#24466](https://github.com/RocketChat/Rocket.Chat/pull/24466)) + Convert to typescript the unarchive slash commands files -
-🔍 Minor changes +- Chore: converted more hooks to typescript ([#24628](https://github.com/RocketChat/Rocket.Chat/pull/24628)) + Converted some functions on `client/hooks/` from JavaScript to Typescript. -- Bump @types/ws from 8.2.2 to 8.2.3 in /ee/server/services ([#24556](https://github.com/RocketChat/Rocket.Chat/pull/24556) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Create README.md for Rest Typings ([#25335](https://github.com/RocketChat/Rocket.Chat/pull/25335)) -- Bump adm-zip from 0.4.14 to 0.5.9 ([#24538](https://github.com/RocketChat/Rocket.Chat/pull/24538) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Delete unused file (NewAdminInfoPage.js) ([#24196](https://github.com/RocketChat/Rocket.Chat/pull/24196)) -- Bump body-parser from 1.19.0 to 1.19.1 in /ee/server/services ([#23963](https://github.com/RocketChat/Rocket.Chat/pull/23963) by [@dependabot[bot]](https://github.com/dependabot[bot])) + Just removing a duplicated/unused file. -- Bump body-parser from 1.19.1 to 1.19.2 in /ee/server/services ([#24517](https://github.com/RocketChat/Rocket.Chat/pull/24517) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: ensure scripts use cross-env and ignore some dirs (ROC-54) ([#25218](https://github.com/RocketChat/Rocket.Chat/pull/25218)) -- Bump cookie from 0.4.1 to 0.4.2 in /ee/server/services ([#24472](https://github.com/RocketChat/Rocket.Chat/pull/24472) by [@dependabot[bot]](https://github.com/dependabot[bot])) + - data and test-failure should be ignored + - ensure scripts use cross-env -- Bump date-fns from 2.24.0 to 2.28.0 ([#24058](https://github.com/RocketChat/Rocket.Chat/pull/24058) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Fix Cypress tests ([#24544](https://github.com/RocketChat/Rocket.Chat/pull/24544)) -- Bump express from 4.17.1 to 4.17.2 in /ee/server/services ([#24469](https://github.com/RocketChat/Rocket.Chat/pull/24469) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Fix grammatical errors in Code of Conduct ([#24759](https://github.com/RocketChat/Rocket.Chat/pull/24759) by [@aadishJ01](https://github.com/aadishJ01)) -- Bump express from 4.17.2 to 4.17.3 in /ee/server/services ([#24522](https://github.com/RocketChat/Rocket.Chat/pull/24522) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: fix grammatical errors in Features ([#24771](https://github.com/RocketChat/Rocket.Chat/pull/24771) by [@aadishJ01](https://github.com/aadishJ01)) -- Bump follow-redirects from 1.14.7 to 1.14.8 in /ee/server/services ([#24491](https://github.com/RocketChat/Rocket.Chat/pull/24491) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Fix return type warnings ([#25275](https://github.com/RocketChat/Rocket.Chat/pull/25275)) -- Bump jaeger-client from 3.18.1 to 3.19.0 in /ee/server/services ([#23961](https://github.com/RocketChat/Rocket.Chat/pull/23961) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Get Settings Statistics ([#24397](https://github.com/RocketChat/Rocket.Chat/pull/24397)) -- Bump pm2 from 5.1.2 to 5.2.0 in /ee/server/services ([#24537](https://github.com/RocketChat/Rocket.Chat/pull/24537) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Improve logger to allow log of `unknown` values ([#24726](https://github.com/RocketChat/Rocket.Chat/pull/24726)) -- Bump simple-get from 4.0.0 to 4.0.1 ([#24341](https://github.com/RocketChat/Rocket.Chat/pull/24341) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Improve PR title validation regex ([#24467](https://github.com/RocketChat/Rocket.Chat/pull/24467)) -- Bump sodium-native from 3.2.1 to 3.3.0 in /ee/server/services ([#23512](https://github.com/RocketChat/Rocket.Chat/pull/23512) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Improvements on role syncing (ldap, oauth and saml) ([#23824](https://github.com/RocketChat/Rocket.Chat/pull/23824)) -- Bump underscore.string from 3.3.5 to 3.3.6 in /ee/server/services ([#24498](https://github.com/RocketChat/Rocket.Chat/pull/24498) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Js to ts slash commands archive ([#24304](https://github.com/RocketChat/Rocket.Chat/pull/24304) by [@eduardofcabrera](https://github.com/eduardofcabrera)) -- Bump url-parse from 1.5.3 to 1.5.7 ([#24528](https://github.com/RocketChat/Rocket.Chat/pull/24528) by [@dependabot[bot]](https://github.com/dependabot[bot])) + Convert Slash Commands archive files to typescript -- Bump vm2 from 3.9.5 to 3.9.7 in /ee/server/services ([#24509](https://github.com/RocketChat/Rocket.Chat/pull/24509) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Chore: Micro services fixes and cleanup ([#24753](https://github.com/RocketChat/Rocket.Chat/pull/24753)) -- Chore: `twoFactorRequired` signature ([#24518](https://github.com/RocketChat/Rocket.Chat/pull/24518)) +- Chore: Migrate oauth2server to typescript ([#25126](https://github.com/RocketChat/Rocket.Chat/pull/25126)) - Improved type checking for decorator `twoFactorRequired`. +- Chore: Minor dependency updates ([#25269](https://github.com/RocketChat/Rocket.Chat/pull/25269)) -- Chore: Add description to global OTR setting ([#24333](https://github.com/RocketChat/Rocket.Chat/pull/24333) by [@pedrogssouza](https://github.com/pedrogssouza)) +- Chore: Missing keys in APIsDisplay ([#24464](https://github.com/RocketChat/Rocket.Chat/pull/24464)) -- Chore: Bump Fuselage packages ([#24573](https://github.com/RocketChat/Rocket.Chat/pull/24573)) +- Chore: Monorepo ([#25074](https://github.com/RocketChat/Rocket.Chat/pull/25074)) - It uses the last stable version of Fuselage packages. +- Chore: move definitions to packages ([#25085](https://github.com/RocketChat/Rocket.Chat/pull/25085)) -- Chore: bump fuselage version ([#24453](https://github.com/RocketChat/Rocket.Chat/pull/24453)) +- Chore: organize test files and fix code coverage ([#24900](https://github.com/RocketChat/Rocket.Chat/pull/24900)) -- Chore: Convert JS files to Typescript ([#24410](https://github.com/RocketChat/Rocket.Chat/pull/24410)) +- Chore: Remove Alpine image deps after using them ([#25053](https://github.com/RocketChat/Rocket.Chat/pull/25053)) - This pull request converts 26 more files from Javascript to Typescript, to check variable types and increase validation on the code. +- Chore: Remove duplicated useUserRoom ([#25180](https://github.com/RocketChat/Rocket.Chat/pull/25180)) -- Chore: Convert to typescript the me slashCommands files ([#24321](https://github.com/RocketChat/Rocket.Chat/pull/24321) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) +- Chore: Remove old files from removed Omnichannel feature ([#25129](https://github.com/RocketChat/Rocket.Chat/pull/25129)) - Convert to typescript the me slashCommands files +- Chore: Remove old scripts ([#24911](https://github.com/RocketChat/Rocket.Chat/pull/24911)) -- Chore: Convert to typescript the mute and unmute slash commands files ([#24325](https://github.com/RocketChat/Rocket.Chat/pull/24325) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) +- Chore: Remove package-lock.json from houston files ([#25280](https://github.com/RocketChat/Rocket.Chat/pull/25280)) - Convert to typescript the mute and unmute slash commands files + Houston config in the `package.json` file still mentioned `package-lock.json`, but it doesn't exist anymore -- Chore: Convert to typescript the slash commands create files ([#24306](https://github.com/RocketChat/Rocket.Chat/pull/24306) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) +- Chore: Remove storybook build job from CI ([#24530](https://github.com/RocketChat/Rocket.Chat/pull/24530)) - Convert Slash Commands create files to typescript. +- Chore: Remove unused Drone CI files ([#25124](https://github.com/RocketChat/Rocket.Chat/pull/25124)) -- Chore: Convert to typescript the slash commands invite files ([#24311](https://github.com/RocketChat/Rocket.Chat/pull/24311) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) +- Chore: roomTypes: Stop mixing client and server code together ([#24536](https://github.com/RocketChat/Rocket.Chat/pull/24536)) - Convert to typescript the slash commands invite files +- Chore: Run tests using microservices deployment on CI ([#24513](https://github.com/RocketChat/Rocket.Chat/pull/24513)) -- Chore: Convert to typescript the unarchive slash commands files ([#24331](https://github.com/RocketChat/Rocket.Chat/pull/24331) by [@eduardofcabrera](https://github.com/eduardofcabrera) & [@ostjen](https://github.com/ostjen)) +- Chore: Set Docker image tag to latest only when really latest ([#24366](https://github.com/RocketChat/Rocket.Chat/pull/24366)) - Convert to typescript the unarchive slash commands files +- Chore: Skip local services changes when shutting down duplicated services ([#24810](https://github.com/RocketChat/Rocket.Chat/pull/24810)) -- Chore: Delete unused file (NewAdminInfoPage.js) ([#24196](https://github.com/RocketChat/Rocket.Chat/pull/24196)) +- Chore: Storybook mocking and examples improved ([#24969](https://github.com/RocketChat/Rocket.Chat/pull/24969)) - Just removing a duplicated/unused file. + - Stories from `ee/` included; + - Differentiate root story kinds; + - Mocking of `ServerContext` via Storybook parameters. -- Chore: Improve PR title validation regex ([#24467](https://github.com/RocketChat/Rocket.Chat/pull/24467)) +- Chore: Sync with master ([#25284](https://github.com/RocketChat/Rocket.Chat/pull/25284)) -- Chore: Js to ts slash commands archive ([#24304](https://github.com/RocketChat/Rocket.Chat/pull/24304) by [@eduardofcabrera](https://github.com/eduardofcabrera)) +- Chore: Template to generate packages ([#25174](https://github.com/RocketChat/Rocket.Chat/pull/25174)) - Convert Slash Commands archive files to typescript + ``` + npx hygen package new test + ``` -- Chore: Remove storybook build job from CI ([#24530](https://github.com/RocketChat/Rocket.Chat/pull/24530)) +- Chore: Tests with Playwright (task: All works) ([#25122](https://github.com/RocketChat/Rocket.Chat/pull/25122)) -- Chore: roomTypes: Stop mixing client and server code together ([#24536](https://github.com/RocketChat/Rocket.Chat/pull/24536)) +- Chore: Tests with Playwright (task: ROC-28, 09-channels) ([#25196](https://github.com/RocketChat/Rocket.Chat/pull/25196)) -- Chore: Run tests using microservices deployment on CI ([#24513](https://github.com/RocketChat/Rocket.Chat/pull/24513)) +- Chore: TS conversion folder client ([#25031](https://github.com/RocketChat/Rocket.Chat/pull/25031)) -- Chore: Set Docker image tag to latest only when really latest ([#24366](https://github.com/RocketChat/Rocket.Chat/pull/24366)) +- Chore: TS migration SortList ([#25167](https://github.com/RocketChat/Rocket.Chat/pull/25167)) - Chore: Unify ILivechatAgent with ILivechatAgentRecord ([#24406](https://github.com/RocketChat/Rocket.Chat/pull/24406)) -- Chore: Update Apps-Engine ([#24568](https://github.com/RocketChat/Rocket.Chat/pull/24568)) - - Chore: Update Apps-Engine ([#24651](https://github.com/RocketChat/Rocket.Chat/pull/24651)) +- Chore: Update Apps-Engine ([#24568](https://github.com/RocketChat/Rocket.Chat/pull/24568)) + - Chore: Update fuselage deps to match monolith versions ([#24501](https://github.com/RocketChat/Rocket.Chat/pull/24501)) +- Chore: Update Livechat to the last version ([#25257](https://github.com/RocketChat/Rocket.Chat/pull/25257)) + +- Chore: Update Livechat version ([#25130](https://github.com/RocketChat/Rocket.Chat/pull/25130)) + - Chore: Update Meteor to 2.5.6 ([#24461](https://github.com/RocketChat/Rocket.Chat/pull/24461)) +- Chore: update OTR icon ([#24521](https://github.com/RocketChat/Rocket.Chat/pull/24521) by [@kibonusp](https://github.com/kibonusp)) + + I changed the shredder icon in OTR contextual bar to the stopwatch icon, recently added to the fuselage. + - Chore: Update ws package ([#24477](https://github.com/RocketChat/Rocket.Chat/pull/24477)) +- Chore(deps-dev): Bump @types/mock-require from 2.0.0 to 2.0.1 ([#24574](https://github.com/RocketChat/Rocket.Chat/pull/24574) by [@dependabot[bot]](https://github.com/dependabot[bot])) + - Chore(deps-dev): Bump ts-node from 10.0.0 to 10.5.0 in /ee/server/services ([#24435](https://github.com/RocketChat/Rocket.Chat/pull/24435) by [@dependabot[bot]](https://github.com/dependabot[bot])) - Chore(deps): Bump node-fetch from 2.6.1 to 2.6.7 in /ee/server/services ([#24299](https://github.com/RocketChat/Rocket.Chat/pull/24299) by [@dependabot[bot]](https://github.com/dependabot[bot])) @@ -2599,18 +6535,66 @@ - i18n: Language update from LingoHub 🤖 on 2022-02-21Z ([#24558](https://github.com/RocketChat/Rocket.Chat/pull/24558)) +- i18n: Language update from LingoHub 🤖 on 2022-02-28Z ([#24644](https://github.com/RocketChat/Rocket.Chat/pull/24644)) + +- i18n: Language update from LingoHub 🤖 on 2022-03-07Z ([#24717](https://github.com/RocketChat/Rocket.Chat/pull/24717)) + +- i18n: Language update from LingoHub 🤖 on 2022-03-14Z ([#24823](https://github.com/RocketChat/Rocket.Chat/pull/24823)) + +- i18n: Language update from LingoHub 🤖 on 2022-03-21Z ([#24895](https://github.com/RocketChat/Rocket.Chat/pull/24895)) + +- i18n: Language update from LingoHub 🤖 on 2022-03-28Z ([#24971](https://github.com/RocketChat/Rocket.Chat/pull/24971)) + +- i18n: Language update from LingoHub 🤖 on 2022-04-04Z ([#25043](https://github.com/RocketChat/Rocket.Chat/pull/25043)) + - Merge master into develop & Set version to 4.5.0-develop ([#24363](https://github.com/RocketChat/Rocket.Chat/pull/24363)) +- Merge master into develop & Set version to 4.6.0-develop ([#24653](https://github.com/RocketChat/Rocket.Chat/pull/24653)) + +- Merge master into develop & Set version to 4.7.0-develop ([#25028](https://github.com/RocketChat/Rocket.Chat/pull/25028)) + +- Regression: Add `isPending` status to message ([#25299](https://github.com/RocketChat/Rocket.Chat/pull/25299)) + +- Regression: Add createdOTR index ([#25017](https://github.com/RocketChat/Rocket.Chat/pull/25017)) + +- Regression: Add eslint package to micro services Dockerfile ([#25311](https://github.com/RocketChat/Rocket.Chat/pull/25311)) + +- Regression: Add select message to system message and thread preview and allow select on legacy template ([#25251](https://github.com/RocketChat/Rocket.Chat/pull/25251)) + - Regression: Add support to namespace within micro services ([#24581](https://github.com/RocketChat/Rocket.Chat/pull/24581)) - Regression: Admin Sidebar colors inverted. ([#24609](https://github.com/RocketChat/Rocket.Chat/pull/24609)) +- Regression: Avatar not loading on first direct message ([#25211](https://github.com/RocketChat/Rocket.Chat/pull/25211)) + + fix avatar not loading on a first direct message + +- Regression: Better MongoDB connection management for micro services ([#25323](https://github.com/RocketChat/Rocket.Chat/pull/25323)) + +- Regression: bump onboarding-ui version ([#25320](https://github.com/RocketChat/Rocket.Chat/pull/25320)) + + - Bump to 'next' the onboarding-ui package from fuselage. + - Update from 'companyEmail' to 'email' adminData usage types + - Regression: Bunch of settings fixes for VoIP ([#24594](https://github.com/RocketChat/Rocket.Chat/pull/24594)) +- Regression: Call doesn't stop ringing after agent unregistration ([#24908](https://github.com/RocketChat/Rocket.Chat/pull/24908)) + +- Regression: Change preference to be default legacy messages ([#25255](https://github.com/RocketChat/Rocket.Chat/pull/25255)) + +- Regression: CI playwright ([#25168](https://github.com/RocketChat/Rocket.Chat/pull/25168)) + +- Regression: Custom roles displaying ID instead of name on some admin screens ([#24999](https://github.com/RocketChat/Rocket.Chat/pull/24999)) + + ![image](https://user-images.githubusercontent.com/55164754/160981416-555bcaa1-c075-4260-937c-64523472da43.png) + ![image](https://user-images.githubusercontent.com/55164754/160981452-6eae4e74-8425-4073-8256-472aba72b9db.png) + - Regression: Do not show toast on incoming voip calls ([#24619](https://github.com/RocketChat/Rocket.Chat/pull/24619)) - Regression: Encode registration info as JWT when signing key is provided ([#24626](https://github.com/RocketChat/Rocket.Chat/pull/24626)) +- Regression: Error is raised when there's no Asterisk queue available yet ([#24980](https://github.com/RocketChat/Rocket.Chat/pull/24980)) + - Regression: Error setting user avatars and mentioning rooms on Slack Import ([#24585](https://github.com/RocketChat/Rocket.Chat/pull/24585)) - Fix `Mentioned room not found` error when importing rooms from Slack; @@ -2620,30 +6604,70 @@ - Regression: Error when trying to load name of dm rooms for avatars and notifications ([#24583](https://github.com/RocketChat/Rocket.Chat/pull/24583)) +- Regression: eslint not running on packages ([#25305](https://github.com/RocketChat/Rocket.Chat/pull/25305)) + - Regression: Extension List panel UI not aligned with designs ([#24645](https://github.com/RocketChat/Rocket.Chat/pull/24645)) +- Regression: Fix account service login expiration ([#24920](https://github.com/RocketChat/Rocket.Chat/pull/24920)) + +- Regression: Fix CI monorepo build ([#25107](https://github.com/RocketChat/Rocket.Chat/pull/25107)) + +- Regression: Fix clicking on visitor's chat in the sidebar does not display the chat window ([#25380](https://github.com/RocketChat/Rocket.Chat/pull/25380)) + + Fix: livechat room not opening. + - Regression: Fix double value on holdTime and empty msg on last message ([#24630](https://github.com/RocketChat/Rocket.Chat/pull/24630)) +- Regression: Fix English i18n react text ([#25368](https://github.com/RocketChat/Rocket.Chat/pull/25368)) + + Incorrect text in reaction tooltip has been fixed + +- Regression: Fix federation Matrix bridge startup ([#25273](https://github.com/RocketChat/Rocket.Chat/pull/25273)) + - Regression: Fix in-correct room status shown to agents ([#24592](https://github.com/RocketChat/Rocket.Chat/pull/24592)) - Regression: Fix incoming voip call ringtone is not ringing ([#24616](https://github.com/RocketChat/Rocket.Chat/pull/24616)) +- Regression: Fix micro services Docker build ([#25193](https://github.com/RocketChat/Rocket.Chat/pull/25193)) + +- Regression: Fix multi line is not showing an empty line between lines ([#25317](https://github.com/RocketChat/Rocket.Chat/pull/25317)) + +- Regression: Fix reply button not working when hideFlexTab is enabled ([#25306](https://github.com/RocketChat/Rocket.Chat/pull/25306)) + - Regression: Fix room not getting created due to null visitor status ([#24562](https://github.com/RocketChat/Rocket.Chat/pull/24562)) +- Regression: Fix services Docker build on CI ([#25181](https://github.com/RocketChat/Rocket.Chat/pull/25181)) + +- Regression: Fix size of custom emoji and render emoji on thread message preview ([#25314](https://github.com/RocketChat/Rocket.Chat/pull/25314)) + +- Regression: Fix the alpine image and dev UX installing matrix-rust-sdk-bindings ([#25319](https://github.com/RocketChat/Rocket.Chat/pull/25319)) + + The package only included a few pre-built which caused all macs to have to compile every time they installed and also caused our alpine not to work. + + This temporarily switches to a fork of the matrix-appservice-bridge package. + + Made changes to one of its child dependencies `matrix-rust-sdk-bindings` that adds pre-built binaries for mac and musl (for alpine). + - Regression: Fix time fields and wrap up in Voip Room Contexual bar ([#24625](https://github.com/RocketChat/Rocket.Chat/pull/24625)) - Regression: Fix time format on Voip system messages ([#24603](https://github.com/RocketChat/Rocket.Chat/pull/24603)) - Regression: Fix translation for call started message ([#24615](https://github.com/RocketChat/Rocket.Chat/pull/24615)) +- Regression: Fix unexpected errors breaking ddp-streamer ([#24948](https://github.com/RocketChat/Rocket.Chat/pull/24948)) + - Regression: Fix wrong tab name for VoIP settings ([#24647](https://github.com/RocketChat/Rocket.Chat/pull/24647)) - Regression: Fixes in Voice Contextual Bar and Directory ([#24596](https://github.com/RocketChat/Rocket.Chat/pull/24596)) - Regression: If Asterisk suddenly goes down, server has no way to know. Causes server to get stuck. Needs restart ([#24624](https://github.com/RocketChat/Rocket.Chat/pull/24624)) +- Regression: Improve Sidenav open/close handling and fixed codeql configs and E2E tests ([#24756](https://github.com/RocketChat/Rocket.Chat/pull/24756)) + - Regression: Mark all rooms as read modal closing instantly. ([#24610](https://github.com/RocketChat/Rocket.Chat/pull/24610)) +- Regression: Messages in new message template Crashing. ([#25327](https://github.com/RocketChat/Rocket.Chat/pull/25327)) + - Regression: No audio when call comes from Skype/IP phone ([#24602](https://github.com/RocketChat/Rocket.Chat/pull/24602)) The audio was not rendered because of re-rendering of react element based on @@ -2666,80 +6690,116 @@ - Regression: Refresh server connection when MI server settings change ([#24649](https://github.com/RocketChat/Rocket.Chat/pull/24649)) +- Regression: Register services right away ([#24800](https://github.com/RocketChat/Rocket.Chat/pull/24800)) + +- Regression: Revert Bugsnag version ([#25313](https://github.com/RocketChat/Rocket.Chat/pull/25313)) + +- Regression: Rocket.Chat Webapp not loading. ([#25349](https://github.com/RocketChat/Rocket.Chat/pull/25349)) + +- Regression: Role Sync not always working ([#24850](https://github.com/RocketChat/Rocket.Chat/pull/24850)) + - Regression: Server crashing if Voip credentials are invalid ([#24646](https://github.com/RocketChat/Rocket.Chat/pull/24646)) +- Regression: Show username and real name on the message system ([#25254](https://github.com/RocketChat/Rocket.Chat/pull/25254)) + +- Regression: Shows error if micro service cannot connect to Mongo ([#25301](https://github.com/RocketChat/Rocket.Chat/pull/25301)) + +- Regression: Use exact Node version on micro services Docker images ([#25287](https://github.com/RocketChat/Rocket.Chat/pull/25287)) + +- Regression: Validate empty fields for Message template ([#25250](https://github.com/RocketChat/Rocket.Chat/pull/25250)) + - Regression: VoIP service button displayed when VoIP is disabled ([#24598](https://github.com/RocketChat/Rocket.Chat/pull/24598)) +- Regression: yarn dev triggers build dependencies ([#25208](https://github.com/RocketChat/Rocket.Chat/pull/25208)) + +- Release 4.5.0 ([#24652](https://github.com/RocketChat/Rocket.Chat/pull/24652) by [@LucasFASouza](https://github.com/LucasFASouza) & [@aswinidev](https://github.com/aswinidev) & [@dependabot[bot]](https://github.com/dependabot[bot]) & [@lingohub[bot]](https://github.com/lingohub[bot]) & [@ostjen](https://github.com/ostjen)) + +- Release 4.5.1 ([#24782](https://github.com/RocketChat/Rocket.Chat/pull/24782) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari) & [@cuonghuunguyen](https://github.com/cuonghuunguyen)) + +- Release 4.5.2 ([#24814](https://github.com/RocketChat/Rocket.Chat/pull/24814)) + +- Release 4.5.3 ([#24884](https://github.com/RocketChat/Rocket.Chat/pull/24884)) + +- Release 4.5.4 ([#24938](https://github.com/RocketChat/Rocket.Chat/pull/24938)) + +- Release 4.5.5 ([#24998](https://github.com/RocketChat/Rocket.Chat/pull/24998)) + +- Release 4.6.0 ([#25027](https://github.com/RocketChat/Rocket.Chat/pull/25027) by [@aswinidev](https://github.com/aswinidev) & [@dependabot[bot]](https://github.com/dependabot[bot]) & [@eduardofcabrera](https://github.com/eduardofcabrera) & [@lingohub[bot]](https://github.com/lingohub[bot])) + +- Release 4.6.1 ([#25095](https://github.com/RocketChat/Rocket.Chat/pull/25095)) + +- Release 4.6.2 ([#25191](https://github.com/RocketChat/Rocket.Chat/pull/25191) by [@sidmohanty11](https://github.com/sidmohanty11)) + +- Release 4.6.3 ([#25235](https://github.com/RocketChat/Rocket.Chat/pull/25235)) + +- Release 4.7.0 ([#25390](https://github.com/RocketChat/Rocket.Chat/pull/25390) by [@Himanshu664](https://github.com/Himanshu664) & [@dependabot[bot]](https://github.com/dependabot[bot]) & [@lingohub[bot]](https://github.com/lingohub[bot])) + +- Release 4.7.1 ([#25510](https://github.com/RocketChat/Rocket.Chat/pull/25510) by [@felipe-menelau](https://github.com/felipe-menelau)) + +- Release 4.7.2 ([#25580](https://github.com/RocketChat/Rocket.Chat/pull/25580)) +
### 👩‍💻👨‍💻 Contributors 😍 +- [@Aman-Maheshwari](https://github.com/Aman-Maheshwari) +- [@Himanshu664](https://github.com/Himanshu664) +- [@JMoVS](https://github.com/JMoVS) - [@LucasFASouza](https://github.com/LucasFASouza) +- [@Muramatsu2602](https://github.com/Muramatsu2602) +- [@aadishJ01](https://github.com/aadishJ01) +- [@aakash-gitdev](https://github.com/aakash-gitdev) - [@aswinidev](https://github.com/aswinidev) +- [@cuonghuunguyen](https://github.com/cuonghuunguyen) - [@dependabot[bot]](https://github.com/dependabot[bot]) - [@eduardofcabrera](https://github.com/eduardofcabrera) +- [@felipe-menelau](https://github.com/felipe-menelau) +- [@gerzonc](https://github.com/gerzonc) +- [@kibonusp](https://github.com/kibonusp) +- [@lingohub[bot]](https://github.com/lingohub[bot]) - [@ostjen](https://github.com/ostjen) +- [@paulobernardoaf](https://github.com/paulobernardoaf) - [@pedrogssouza](https://github.com/pedrogssouza) +- [@sidmohanty11](https://github.com/sidmohanty11) +- [@tkurz](https://github.com/tkurz) - [@ujorgeleite](https://github.com/ujorgeleite) ### 👩‍💻👨‍💻 Core Team 🤓 +- [@AllanPazRibeiro](https://github.com/AllanPazRibeiro) - [@KevLehman](https://github.com/KevLehman) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) - [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@alansikora](https://github.com/alansikora) - [@albuquerquefabio](https://github.com/albuquerquefabio) - [@amolghode1981](https://github.com/amolghode1981) +- [@cauefcr](https://github.com/cauefcr) - [@d-gubert](https://github.com/d-gubert) - [@debdutdeb](https://github.com/debdutdeb) - [@dougfabris](https://github.com/dougfabris) - [@felipe-rod123](https://github.com/felipe-rod123) - [@filipemarins](https://github.com/filipemarins) - [@gabriellsh](https://github.com/gabriellsh) +- [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) - [@guijun13](https://github.com/guijun13) +- [@jeanfbrito](https://github.com/jeanfbrito) - [@juliajforesti](https://github.com/juliajforesti) - [@matheusbsilva137](https://github.com/matheusbsilva137) - [@murtaza98](https://github.com/murtaza98) +- [@nishant23122000](https://github.com/nishant23122000) - [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) - [@renatobecker](https://github.com/renatobecker) - [@rique223](https://github.com/rique223) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) +- [@souzaramon](https://github.com/souzaramon) - [@tassoevan](https://github.com/tassoevan) - [@tiagoevanp](https://github.com/tiagoevanp) +- [@tmontini](https://github.com/tmontini) +- [@weslley543](https://github.com/weslley543) - [@yash-rajpal](https://github.com/yash-rajpal) -# 4.4.5 -`2022-05-30 · 1 🐛 · 1 👩‍💻👨‍💻` - -### Engine versions -- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` - -### 🐛 Bug fixes - - -- Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) - -### 👩‍💻👨‍💻 Core Team 🤓 - -- [@ggazzo](https://github.com/ggazzo) - -# 4.4.4 -`2022-05-20 · 1 🐛 · 1 👩‍💻👨‍💻` - -### Engine versions -- Node: `14.18.3` -- NPM: `6.14.15` -- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` - -### 🐛 Bug fixes - - -- Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) - -### 👩‍💻👨‍💻 Core Team 🤓 - -- [@ggazzo](https://github.com/ggazzo) - # 4.4.3 `2022-04-07 · 2 🐛 · 2 👩‍💻👨‍💻` @@ -5045,9 +9105,7 @@ `2022-05-26 · 1 🐛 · 1 👩‍💻👨‍💻` ### Engine versions -- Node: `14.18.3` -- NPM: `6.14.15` -- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- MongoDB: `3.4, 3.6, 4.0, 4.2` ### 🐛 Bug fixes diff --git a/_templates/package/new/package.json.ejs.t b/_templates/package/new/package.json.ejs.t index 827f44a01ad6..4eff5e1b7ed1 100644 --- a/_templates/package/new/package.json.ejs.t +++ b/_templates/package/new/package.json.ejs.t @@ -11,13 +11,14 @@ to: packages/<%= name %>/package.json "eslint": "^8.12.0", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "typescript": "~4.3.4" + "typescript": "~4.5.5" }, "scripts": { "lint": "eslint --ext .js,.jsx,.ts,.tsx .", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", "jest": "jest", - "build": "rm -rf dist && tsc -p tsconfig.json" + "build": "rm -rf dist && tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" }, "main": "./dist/index.js", "typings": "./dist/index.d.ts", @@ -25,5 +26,13 @@ to: packages/<%= name %>/package.json "/dist" ], "dependencies": { + }, + "eslintConfig": { + "extends": [ + "@rocket.chat/eslint-config" + ], + "ignorePatterns": [ + "**/dist" + ] } } diff --git a/apps/meteor/.docker-mongo/Dockerfile b/apps/meteor/.docker-mongo/Dockerfile index 2e92d78bcb0a..f8556d971d8b 100644 --- a/apps/meteor/.docker-mongo/Dockerfile +++ b/apps/meteor/.docker-mongo/Dockerfile @@ -1,4 +1,4 @@ -FROM node:14.18.3-bullseye-slim +FROM node:14.19.3-bullseye-slim LABEL maintainer="buildmaster@rocket.chat" diff --git a/apps/meteor/.docker/Dockerfile b/apps/meteor/.docker/Dockerfile index fb2cc092daa3..ce06aeb052eb 100644 --- a/apps/meteor/.docker/Dockerfile +++ b/apps/meteor/.docker/Dockerfile @@ -1,4 +1,4 @@ -FROM node:14.18.3-bullseye-slim +FROM node:14.19.3-bullseye-slim LABEL maintainer="buildmaster@rocket.chat" diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile.alpine index 0c7bd994e8f5..99aa4c2eb016 100644 --- a/apps/meteor/.docker/Dockerfile.alpine +++ b/apps/meteor/.docker/Dockerfile.alpine @@ -1,4 +1,4 @@ -FROM node:14.18.3-alpine3.15 +FROM node:14.19.3-alpine3.15 RUN apk add --no-cache ttf-dejavu @@ -12,7 +12,7 @@ RUN set -x \ && npm install --production \ # Start hack for sharp... && rm -rf npm/node_modules/sharp \ - && npm install sharp@0.29.3 \ + && npm install sharp@0.30.4 \ && mv node_modules/sharp npm/node_modules/sharp \ # End hack for sharp && cd npm \ diff --git a/apps/meteor/.docker/Dockerfile.rhel b/apps/meteor/.docker/Dockerfile.rhel index 1bbc6ec8ab9c..7ee11ce24415 100644 --- a/apps/meteor/.docker/Dockerfile.rhel +++ b/apps/meteor/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/ubi8/nodejs-12 -ENV RC_VERSION 4.8.2 +ENV RC_VERSION 5.0.0 MAINTAINER buildmaster@rocket.chat diff --git a/apps/meteor/.eslintignore b/apps/meteor/.eslintignore index cb0aed399c3b..158360ed08eb 100644 --- a/apps/meteor/.eslintignore +++ b/apps/meteor/.eslintignore @@ -3,9 +3,7 @@ data/ tests/e2e/test-failures/ packages/autoupdate/ packages/meteor-streams/ -packages/meteor-timesync/ app/emoji-emojione/generateEmojiIndex.js -app/favico/favico.js packages/rocketchat-livechat/assets/rocketchat-livechat.min.js packages/rocketchat-livechat/assets/rocket-livechat.js app/theme/client/vendor/ diff --git a/apps/meteor/.gitignore b/apps/meteor/.gitignore index f99fb3d2ccd9..e8a861e58324 100644 --- a/apps/meteor/.gitignore +++ b/apps/meteor/.gitignore @@ -78,11 +78,10 @@ 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 /data tests/e2e/test-failures/ out.txt +dist diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index daa663bcce20..292ef0526ba7 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -1,6 +1,9 @@ # Meteor packages used by this project, one per line. + # + # 'meteor add' and 'meteor remove' will edit this file for you, + # but you can also edit it by hand. rocketchat:ddp @@ -10,22 +13,22 @@ accounts-facebook@1.3.3 accounts-github@1.5.0 accounts-google@1.4.0 accounts-meteor-developer@1.5.0 -accounts-password@2.2.0 +accounts-password@2.3.1 accounts-twitter@1.5.0 blaze-html-templates check@1.3.1 ddp-rate-limiter@1.1.0 ddp-common@1.4.0 dynamic-import@0.7.2 -ecmascript@0.16.1 -typescript@4.4.1 -ejson@1.1.1 -email@2.2.0 +ecmascript@0.16.2 +typescript@4.5.4 +ejson@1.1.2 +email@2.2.1 http@2.0.0 logging@1.3.1 meteor-base@1.5.1 mobile-experience@1.1.0 -mongo@1.13.0 +mongo@1.15.0 random@1.2.0 rate-limit@1.0.9 reactive-dict@1.3.0 @@ -46,11 +49,13 @@ konecty:multiple-instances-status konecty:user-presence dispatch:run-as-user -jalik:ufs@1.0.2 -jalik:ufs-gridfs@1.0.2 + +jalik:ufs@1.0.4 +jalik:ufs-gridfs@1.0.5 +jalik:ufs-local@1.0.4 + jparker:gravatar kadira:flow-router -mizzao:timesync mrt:reactive-store mystor:device-detection rocketchat:restivus @@ -64,27 +69,25 @@ rocketchat:tap-i18n@3.0.0 underscore@1.0.10 littledata:synced-cron -edgee:slingshot -jalik:ufs-local@1.0.2 -accounts-base@2.2.1 -accounts-oauth@1.4.0 +accounts-base@2.2.3 +accounts-oauth@1.4.1 autoupdate@1.8.0 -babel-compiler@7.8.0 -google-oauth@1.4.1 +babel-compiler@7.9.0 +google-oauth@1.4.2 htmljs less matb33:collection-hooks meteorhacks:inject-initial -oauth@2.1.1 +oauth@2.1.2 oauth2@1.3.1 routepolicy@1.1.1 sha@1.0.9 templating -webapp@1.13.0 +webapp@1.13.1 webapp-hashing@1.1.0 rocketchat:oauth2-server rocketchat:i18n -rocketchat:postcss dandv:caret-position facts-base@1.0.1 -url +url@1.3.2 +standard-minifier-css diff --git a/apps/meteor/.meteor/release b/apps/meteor/.meteor/release index 0150b11e9f5b..66dd7b664724 100644 --- a/apps/meteor/.meteor/release +++ b/apps/meteor/.meteor/release @@ -1 +1 @@ -METEOR@2.5.6 +METEOR@2.7.3 diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index 7ba89bd6e6f9..2cb41ac914aa 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -1,21 +1,21 @@ -accounts-base@2.2.1 +accounts-base@2.2.3 accounts-facebook@1.3.3 accounts-github@1.5.0 accounts-google@1.4.0 accounts-meteor-developer@1.5.0 -accounts-oauth@1.4.0 -accounts-password@2.2.0 +accounts-oauth@1.4.1 +accounts-password@2.3.1 accounts-twitter@1.5.0 aldeed:simple-schema@1.5.4 -allow-deny@1.1.0 +allow-deny@1.1.1 autoupdate@1.8.0 -babel-compiler@7.8.0 -babel-runtime@1.5.0 +babel-compiler@7.9.0 +babel-runtime@1.5.1 base64@1.0.12 binary-heap@1.0.11 -blaze@2.5.0 +blaze@2.6.0 blaze-html-templates@1.2.1 -blaze-tools@1.1.2 +blaze-tools@1.1.3 boilerplate-generator@1.7.1 caching-compiler@1.2.2 caching-html-compiler@1.2.1 @@ -33,29 +33,28 @@ deps@1.0.12 diff-sequence@1.1.1 dispatch:run-as-user@1.1.1 dynamic-import@0.7.2 -ecmascript@0.16.1 +ecmascript@0.16.2 ecmascript-runtime@0.8.0 ecmascript-runtime-client@0.12.1 ecmascript-runtime-server@0.11.0 -edgee:slingshot@0.7.1 -ejson@1.1.1 -email@2.2.0 +ejson@1.1.2 +email@2.2.1 es5-shim@4.8.0 -facebook-oauth@1.10.0 +facebook-oauth@1.11.0 facts-base@1.0.1 fetch@0.1.1 geojson-utils@1.0.10 -github-oauth@1.3.2 -google-oauth@1.4.1 +github-oauth@1.4.0 +google-oauth@1.4.2 hot-code-push@1.0.4 -html-tools@1.1.2 +html-tools@1.1.3 htmljs@1.1.1 http@2.0.0 id-map@1.1.1 inter-process-messaging@0.1.1 -jalik:ufs@1.0.2 -jalik:ufs-gridfs@1.0.2 -jalik:ufs-local@1.0.2 +jalik:ufs@1.0.4 +jalik:ufs-gridfs@1.0.5 +jalik:ufs-local@1.0.4 jparker:crypto-core@0.1.0 jparker:crypto-md5@0.1.1 jparker:gravatar@0.5.1 @@ -68,35 +67,34 @@ less@3.0.2 littledata:synced-cron@1.5.1 localstorage@1.2.0 logging@1.3.1 -matb33:collection-hooks@1.1.0 +matb33:collection-hooks@1.1.2 mdg:validation-error@0.5.1 meteor@1.10.0 meteor-base@1.5.1 meteor-developer-oauth@1.3.1 meteorhacks:inject-initial@1.0.5 minifier-css@1.6.0 -minifier-js@2.7.3 -minimongo@1.7.0 -mizzao:timesync@0.3.4 +minifier-js@2.7.4 +minimongo@1.8.0 mobile-experience@1.1.0 mobile-status-bar@1.1.0 -modern-browsers@0.1.7 +modern-browsers@0.1.8 modules@0.18.0 -modules-runtime@0.12.0 -mongo@1.13.0 -mongo-decimal@0.1.2 +modules-runtime@0.13.0 +mongo@1.15.0 +mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 mrt:reactive-store@0.0.1 mystor:device-detection@0.2.0 nooitaf:colors@1.2.0 -npm-mongo@3.9.1 -oauth@2.1.1 +npm-mongo@4.3.1 +oauth@2.1.2 oauth1@1.5.0 oauth2@1.3.1 -observe-sequence@1.0.19 +observe-sequence@1.0.20 ordered-dict@1.1.0 -ostrio:cookies@2.7.0 +ostrio:cookies@2.7.2 pauli:accounts-linkedin@6.0.0 pauli:linkedin-oauth@6.0.0 promise@0.12.0 @@ -105,7 +103,7 @@ raix:handlebar-helpers@0.2.5 raix:ui-dropped-event@0.0.7 random@1.2.0 rate-limit@1.0.9 -react-fast-refresh@0.2.2 +react-fast-refresh@0.2.3 reactive-dict@1.3.0 reactive-var@1.0.11 reload@1.3.1 @@ -115,7 +113,6 @@ rocketchat:i18n@0.0.1 rocketchat:livechat@0.0.1 rocketchat:mongo-config@0.0.1 rocketchat:oauth2-server@3.0.0 -rocketchat:postcss@1.0.0 rocketchat:restivus@1.0.0 rocketchat:streamer@1.1.0 rocketchat:tap-i18n@3.0.0 @@ -126,19 +123,20 @@ session@1.2.0 sha@1.0.9 shell-server@0.5.0 simple:json-routes@2.3.1 -socket-stream-client@0.4.0 -spacebars@1.2.0 -spacebars-compiler@1.3.0 +socket-stream-client@0.5.0 +spacebars@1.3.0 +spacebars-compiler@1.3.1 +standard-minifier-css@1.8.1 standard-minifier-js@2.8.0 -templating@1.4.1 +templating@1.4.2 templating-compiler@1.4.1 -templating-runtime@1.5.0 -templating-tools@1.2.1 +templating-runtime@1.6.0 +templating-tools@1.2.2 tracker@1.2.0 twitter-oauth@1.3.0 -typescript@4.4.1 +typescript@4.5.4 ui@1.0.13 underscore@1.0.10 url@1.3.2 -webapp@1.13.0 +webapp@1.13.1 webapp-hashing@1.1.0 diff --git a/apps/meteor/.meteorignore b/apps/meteor/.meteorignore index 9b77b349e3fd..7b2dc6e71c31 100644 --- a/apps/meteor/.meteorignore +++ b/apps/meteor/.meteorignore @@ -1,3 +1,4 @@ ee/server/services coverage data +dist diff --git a/apps/meteor/.mocharc.api.js b/apps/meteor/.mocharc.api.js index 934fd1509ae1..f6c09a541e00 100644 --- a/apps/meteor/.mocharc.api.js +++ b/apps/meteor/.mocharc.api.js @@ -9,5 +9,11 @@ module.exports = { timeout: 10000, bail: true, file: 'tests/end-to-end/teardown.js', - spec: ['tests/unit/app/api/server/v1/*.spec.ts', 'tests/end-to-end/api/*.js', 'tests/end-to-end/api/*.ts', 'tests/end-to-end/apps/*.js'], + spec: [ + 'tests/unit/app/api/server/v1/**/*.spec.ts', + 'tests/end-to-end/api/**/*.js', + 'tests/end-to-end/api/**/*.ts', + 'tests/end-to-end/apps/*.js', + 'tests/end-to-end/apps/*.ts', + ], }; diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index 6a5010905612..f8118d74f7c9 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -25,6 +25,7 @@ module.exports = { exit: true, spec: [ 'ee/tests/**/*.tests.ts', + 'ee/tests/**/*.spec.ts', 'tests/unit/app/**/*.spec.ts', 'tests/unit/app/**/*.tests.js', 'tests/unit/app/**/*.tests.ts', diff --git a/apps/meteor/.scripts/start.js b/apps/meteor/.scripts/start.js deleted file mode 100644 index a29e7bb1dbf1..000000000000 --- a/apps/meteor/.scripts/start.js +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env node - -const path = require('path'); -const fs = require('fs'); -const { spawn } = require('child_process'); -const net = require('net'); - -const processes = []; - -const baseDir = path.resolve(__dirname, '..'); -const srcDir = path.resolve(baseDir); - -const isPortTaken = (port) => - new Promise((resolve, reject) => { - const tester = net - .createServer() - .once('error', (err) => (err.code === 'EADDRINUSE' ? resolve(true) : reject(err))) - .once('listening', () => tester.once('close', () => resolve(false)).close()) - .listen(port); - }); - -const waitPortRelease = (port, count = 0) => - new Promise((resolve, reject) => { - isPortTaken(port).then((taken) => { - if (!taken) { - return resolve(); - } - if (count > 60) { - return reject(); - } - console.log('Port', port, 'not released, waiting 1s...'); - setTimeout(() => { - waitPortRelease(port, ++count) - .then(resolve) - .catch(reject); - }, 1000); - }); - }); - -const appOptions = { - env: { - PORT: 3000, - ROOT_URL: 'http://localhost:3000', - }, -}; - -let killingAllProcess = false; -function killAllProcesses(mainExitCode) { - if (killingAllProcess) { - return; - } - killingAllProcess = true; - - processes.forEach((p) => { - console.log('Killing process', p.pid); - p.kill(); - }); - - waitPortRelease(appOptions.env.PORT) - .then(() => { - console.log(`Port ${appOptions.env.PORT} was released, exiting with code ${mainExitCode}`); - process.exit(mainExitCode); - }) - .catch((error) => { - console.error(`Error waiting port ${appOptions.env.PORT} to be released, exiting with code ${mainExitCode}`); - console.error(error); - process.exit(mainExitCode); - }); -} - -function startProcess(opts) { - console.log('Starting process', opts.name, opts.command, opts.params, opts.options.cwd); - const proc = spawn(opts.command, opts.params, opts.options); - processes.push(proc); - - if (opts.onData) { - proc.stdout.on('data', opts.onData); - } - - if (!opts.silent) { - proc.stdout.pipe(process.stdout); - proc.stderr.pipe(process.stderr); - } - - if (opts.logFile) { - const logStream = fs.createWriteStream(opts.logFile, { flags: 'a' }); - proc.stdout.pipe(logStream); - proc.stderr.pipe(logStream); - } - - proc.on('exit', function (code, signal) { - processes.splice(processes.indexOf(proc), 1); - - if (code != null) { - console.log(opts.name, `exited with code ${code}`); - } else { - console.log(opts.name, `exited with signal ${signal}`); - } - - killAllProcesses(code); - }); -} - -function startRocketChat() { - return new Promise((resolve) => { - const waitServerRunning = (message) => { - if (message.toString().match('SERVER RUNNING')) { - return resolve(); - } - }; - - startProcess({ - name: 'Meteor App', - command: 'node', - params: ['/tmp/build-test/bundle/main.js'], - onData: waitServerRunning, - options: { - cwd: srcDir, - env: { - ...appOptions.env, - ...process.env, - }, - }, - }); - }); -} - -async function startMicroservices() { - const waitStart = (resolve) => (message) => { - if (message.toString().match('NetworkBroker started successfully')) { - return resolve(); - } - }; - const startService = (name) => { - return new Promise((resolve) => { - const cwd = - name === 'ddp-streamer' - ? path.resolve(srcDir, '..', '..', 'ee', 'apps', name, 'dist', 'ee', 'apps', name) - : path.resolve(srcDir, 'ee', 'server', 'services', 'dist', 'ee', 'server', 'services', name); - - startProcess({ - name: `${name} service`, - command: 'node', - params: [name === 'ddp-streamer' ? 'src/service.js' : 'service.js'], - onData: waitStart(resolve), - options: { - cwd, - env: { - ...appOptions.env, - ...process.env, - PORT: 4000, - }, - }, - }); - }); - }; - - await Promise.all([ - startService('account'), - startService('authorization'), - startService('ddp-streamer'), - startService('presence'), - startService('stream-hub'), - ]); -} - -function startTests(options = []) { - const testOption = options.find((i) => i.startsWith('--test=')); - const testParam = testOption ? testOption.replace('--test=', '') : 'test'; - - console.log(`Running test "npm run ${testParam}"`); - - startProcess({ - name: 'Tests', - command: 'npm', - params: ['run', testParam], - options: { - env: { - ...process.env, - NODE_PATH: `${process.env.NODE_PATH + path.delimiter + srcDir + path.delimiter + srcDir}/node_modules`, - }, - }, - }); -} - -(async () => { - const [, , ...options] = process.argv; - - await startRocketChat(); - - if (options.includes('--enterprise')) { - await startMicroservices(); - } - - startTests(options); -})(); diff --git a/apps/meteor/.snapcraft/launchpadkey.enc b/apps/meteor/.snapcraft/launchpadkey.enc deleted file mode 100644 index af91e0e62be9..000000000000 Binary files a/apps/meteor/.snapcraft/launchpadkey.enc and /dev/null differ diff --git a/apps/meteor/.snapcraft/resources/Caddyfile b/apps/meteor/.snapcraft/resources/Caddyfile deleted file mode 100644 index 23299c1d2b7e..000000000000 --- a/apps/meteor/.snapcraft/resources/Caddyfile +++ /dev/null @@ -1,5 +0,0 @@ -_caddy-url_ -proxy / localhost:_port_ { - websocket - transparent -} diff --git a/apps/meteor/.snapcraft/resources/backupdb b/apps/meteor/.snapcraft/resources/backupdb deleted file mode 100755 index b457d325e543..000000000000 --- a/apps/meteor/.snapcraft/resources/backupdb +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -if [[ ${EUID} != 0 ]]; then - echo "[-] This task must be run with 'sudo'." - exit 1 -fi - -if $(ps x | grep "node ${SNAP}/main.js" | grep -qv "grep"); then - echo "[-] Please shutdown Rocket.Chat first to get a clean backup" - echo "[-] Use 'sudo systemctl stop snap.rocketchat-server.rocketchat-server'" -fi - -TIMESTAMP=$(date +"%Y%m%d.%H%M") -BACKUP_DIR="${SNAP_COMMON}/backup" - -if [[ ! -d ${BACKUP_DIR} ]]; then - mkdir ${BACKUP_DIR} -fi - -if [[ -d ${BACKUP_DIR}/dump ]]; then - rm -rf ${BACKUP_DIR}/dump -fi - -if [[ -f ${BACKUP_DIR}/rocketchat_backup_${TIMESTAMP}.tar.gz ]]; then - rm -f ${BACKUP_DIR}/rocketchat_backup_${TIMESTAMP}.tar.gz -fi - -if [[ -f ${BACKUP_DIR}/backup_${TIMESTAMP}.log ]]; then - rm -f ${BACKUP_DIR}/backup_${TIMESTAMP}.log -fi - -echo "[*] Creating backup file..." -mkdir ${BACKUP_DIR}/dump -echo "[*] Dumping database with \"mongodump -d parties -o ${BACKUP_DIR}/dump\"" > "${BACKUP_DIR}/backup_${TIMESTAMP}.log" -mongodump -d parties -o ${BACKUP_DIR}/dump &>> "${BACKUP_DIR}/backup_${TIMESTAMP}.log" -echo "[*] Packing archive with \"tar czvf ${BACKUP_DIR}/rocketchat_backup_${TIMESTAMP}.tar.gz ${BACKUP_DIR}/dump\"" >> "${BACKUP_DIR}/backup_${TIMESTAMP}.log" -tar czvf ${BACKUP_DIR}/rocketchat_backup_${TIMESTAMP}.tar.gz -C ${BACKUP_DIR} dump &>> "${BACKUP_DIR}/backup_${TIMESTAMP}.log" -rm -rf ${BACKUP_DIR}/dump - -echo "[+] A backup of your data can be found at ${BACKUP_DIR}/rocketchat_backup_${TIMESTAMP}.tar.gz" diff --git a/apps/meteor/.snapcraft/resources/initcaddy b/apps/meteor/.snapcraft/resources/initcaddy deleted file mode 100755 index b55d20de02b3..000000000000 --- a/apps/meteor/.snapcraft/resources/initcaddy +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash - -# Config options for Caddyfile -#options="site path port" -options="caddy-url port" - -refresh_opt_in_config() { -# replace an option inside the config file. - opt=$1 - value="$2" - if $(grep -q "_${opt}_" $Caddyfile); then - sed "s,_${opt}_,$value," $Caddyfile 2>/dev/null > ${Caddyfile}.new - mv -f ${Caddyfile}.new $Caddyfile 2>/dev/null - else - echo "Fail to update $opt in Caddyfile" - fi -} - -create_caddyfile(){ -# Copy template to config Caddyfile -cp $SNAP/bin/Caddyfile $SNAP_DATA/Caddyfile -} - -update_caddyfile(){ -# Config file path for Caddyfile -Caddyfile=$SNAP_DATA/Caddyfile - -# Iterate through the config options array -for opt in $options - do - # Use snapctl to get the value registered by the snap set command - refresh_opt_in_config $opt $(snapctl get $opt) -done -} - -caddy="$(snapctl get caddy)" -if [[ $caddy == "disable" ]]; then - echo "Caddy is not enabled, please set caddy-url= and caddy=enable" - exit 1 -fi - -create_caddyfile -update_caddyfile - -echo "Your URL was successfully configured - Please restart rocketchat and caddy services to apply configuration changes" - diff --git a/apps/meteor/.snapcraft/resources/initreplset.js b/apps/meteor/.snapcraft/resources/initreplset.js deleted file mode 100644 index 6883e248ebd6..000000000000 --- a/apps/meteor/.snapcraft/resources/initreplset.js +++ /dev/null @@ -1,13 +0,0 @@ -var ism = db.isMaster(); -if (!ism.ismaster) { - rs.initiate( - { - _id: 'rs0', - members: [ - { - _id: 0, - host: 'localhost:27017' - } - ] - }); -} diff --git a/apps/meteor/.snapcraft/resources/prepareRocketChat b/apps/meteor/.snapcraft/resources/prepareRocketChat deleted file mode 100755 index c6681eca0857..000000000000 --- a/apps/meteor/.snapcraft/resources/prepareRocketChat +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash - -curl -SLf "https://releases.rocket.chat/4.8.2/download/" -o rocket.chat.tgz - -tar xf rocket.chat.tgz --strip 1 - -cd programs/server -rm -rf npm/node_modules/meteor/emojione_emojione/node_modules/grunt-contrib-qunit - -if [[ $(uname -m) == *armv6l* ]] || [[ $(uname -m) == *armv7l* ]] -then - rm -rf npm/node_modules/sharp/vendor -fi - -export NODE_ENV=production -npm i - -# Ideally this will go away. For some reason on install its installing node-v57-linux-x64-glibc but when actually running it is looking for node-v57-linux-x64-unknown -if [[ $(uname -m) == "x86_64" ]] -then - cp -r npm/node_modules/grpc/src/node/extension_binary/node-v57-linux-x64-glibc npm/node_modules/grpc/src/node/extension_binary/node-v57-linux-x64-unknown - rm npm/node_modules/grpc/node_modules/tar/lib/.unpack.js.swp -fi - -# sharp needs execution stack removed - https://forum.snapcraft.io/t/snap-and-executable-stacks/1812 -ls -l npm/node_modules/sharp/vendor -execstack --clear-execstack npm/node_modules/sharp/vendor/lib/librsvg-2.so* - -# Having to manually remove because of latest warning -rm -rf npm/node_modules/meteor/konecty_user-presence/node_modules/colors/lib/.colors.js.swp -rm -rf node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp -rm -rf npm/node_modules/sharp/node_modules/semver/bin/.semver.js.swp -rm -rf npm/node_modules/tar/lib/.mkdir.js.swp - diff --git a/apps/meteor/.snapcraft/resources/preparecaddy b/apps/meteor/.snapcraft/resources/preparecaddy deleted file mode 100755 index 303020c803e1..000000000000 --- a/apps/meteor/.snapcraft/resources/preparecaddy +++ /dev/null @@ -1,40 +0,0 @@ -#! /bin/bash - -caddy_version="v1.0.4" - -caddy_bin="caddy" -caddy_dl_ext=".tar.gz" - -# NOTE: `uname -m` is more accurate and universal than `arch` -# See https://en.wikipedia.org/wiki/Uname -unamem="$(uname -m)" -if [[ $unamem == *aarch64* ]]; then - caddy_arch="arm64" -elif [[ $unamem == *64* ]]; then - caddy_arch="amd64" -elif [[ $unamem == *86* ]]; then - caddy_arch="386" -elif [[ $unamem == *armv5* ]]; then - caddy_arch="arm" - caddy_arm="5" -elif [[ $unamem == *armv6l* ]]; then - caddy_arch="arm" - caddy_arm="6" -elif [[ $unamem == *armv7l* ]]; then - caddy_arch="arm" - caddy_arm="7" -else - echo "Aborted, unsupported or unknown architecture: $unamem" - return 2 -fi - -caddy_arch=_linux_$caddy_arch - -echo "Downloading Caddy for $caddy_os/$caddy_arch$caddy_arm..." -caddy_file="caddy_linux_$caddy_arch${caddy_arm}_custom$caddy_dl_ext" -caddy_url="https://github.com/caddyserver/caddy/releases/download/$caddy_version/caddy_$caddy_version$caddy_arch$caddy_arm.tar.gz" -echo "$caddy_url" - -curl -L "$caddy_url" -o "$caddy_file" -tar -xzf $caddy_file -C . "$caddy_bin" -chmod +x $caddy_bin diff --git a/apps/meteor/.snapcraft/resources/preparemongo b/apps/meteor/.snapcraft/resources/preparemongo deleted file mode 100755 index 3ccfd8b35195..000000000000 --- a/apps/meteor/.snapcraft/resources/preparemongo +++ /dev/null @@ -1,22 +0,0 @@ -#! /bin/bash - -if [[ $(uname -m) == "x86_64" ]] -then - wget --backups=0 "https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.6.14.tgz" - tar -zxf ./mongodb-linux-x86_64-3.6.14.tgz --strip-components=1 -else - IFS=" " read -a links <<< $(apt-get -y --print-uris install mongodb | egrep -o "https?://[^']+") - for link in ${links[@]} - do - wget --backups=0 ${link} - done - - IFS=" " read -a deb_pkgs <<< $(ls ./ | egrep "\.deb") - for pkg in ${deb_pkgs[@]} - do - echo "Extracting ${pkg}..." - dpkg-deb -R ${pkg} ./ - done - - mv usr/bin bin -fi diff --git a/apps/meteor/.snapcraft/resources/preparenode b/apps/meteor/.snapcraft/resources/preparenode deleted file mode 100755 index d6619dd9bf4d..000000000000 --- a/apps/meteor/.snapcraft/resources/preparenode +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -node_version="v14.18.3" - -unamem="$(uname -m)" -if [[ $unamem == *aarch64* ]]; then - node_arch="arm64" -elif [[ $unamem == *64* ]]; then - node_arch="x64" -elif [[ $unamem == *86* ]]; then - node_arch="x86" -elif [[ $unamem == *armv6l* ]]; then - node_arch="armv6l" -elif [[ $unamem == *armv7l* ]]; then - node_arch="armv7l" -else - echo "Aborted, unsupported or unknown architecture: $unamem" - return 2 -fi - - -wget https://nodejs.org/dist/$node_version/node-$node_version-linux-$node_arch.tar.xz -tar xf node-$node_version-linux-$node_arch.tar.xz --strip 1 diff --git a/apps/meteor/.snapcraft/resources/restoredb b/apps/meteor/.snapcraft/resources/restoredb deleted file mode 100755 index 731cc8bbfcb0..000000000000 --- a/apps/meteor/.snapcraft/resources/restoredb +++ /dev/null @@ -1,88 +0,0 @@ -#!/bin/bash - -function warn { - echo "[!] ${1}" - echo "[*] Check ${RESTORE_DIR}/${LOG_NAME} for details." -} - -function abort { - echo "[!] ${1}" - echo "[*] Check ${RESTORE_DIR}/${LOG_NAME} for details." - echo "[-] Restore aborted!" - exit 1 -} - -if [[ ${EUID} != 0 ]]; then - echo "[-] This task must be run with 'sudo'." - exit 1 -fi - -echo "*** ATTENTION ***" -echo "* Your current database WILL BE DROPPED prior to the restore!" -echo "* Do you want to continue?" - -select yn in "Yes" "No"; do - case $yn in - Yes ) break;; - No ) exit 1;; - esac -done - -if $(ps x | grep "node ${SNAP}/main.js" | grep -qv "grep"); then - echo "[-] Please shutdown Rocket.Chat first to restore a clean backup" - echo "[-] Use 'sudo systemctl stop snap.rocketchat-server.rocketchat-server'" - echo "[-] Backup aborted!" - exit 1 -fi - -TIMESTAMP=$(date +"%Y%m%d.%H%M") -RESTORE_DIR="${SNAP_COMMON}/restore" -DATA_DIR="${RESTORE_DIR}/dump/parties" -LOG_NAME="restore_${TIMESTAMP}.log" - -if [[ ! -d ${RESTORE_DIR} ]]; then - mkdir ${RESTORE_DIR} -fi - -if [[ -d ${RESTORE_DIR}/dump ]]; then - rm -rf ${RESTORE_DIR}/dump -fi - -if [[ -f ${RESTORE_DIR}/${LOG_NAME} ]]; then - rm -f ${RESTORE_DIR}/${LOG_NAME} -fi - -BACKUP_FILE=${1} -if [[ ! -f ${BACKUP_FILE} ]]; then - echo "[-] Usage: sudo snap run rocketchat-server.restoredb ${SNAP_COMMON}/rocketchat_backup_{TIMESTAMP}.tar.gz" - exit 1 -fi - -if ! $(echo "${BACKUP_FILE}" | grep -q "${SNAP_COMMON}"); then - echo "[-] Backup file must be within ${SNAP_COMMON}." - exit 1 -fi - -echo "[*] Extracting backup file..." -echo "[*] Extracting backup file with \"tar --no-same-owner --overwrite -xzvf ${BACKUP_FILE}\"" &> "${RESTORE_DIR}/${LOG_NAME}" -cd ${RESTORE_DIR} -tar --no-same-owner --overwrite -xzvf ${BACKUP_FILE} &>> "${RESTORE_DIR}/${LOG_NAME}" \ - || abort "Failed to extract backup files to ${RESTORE_DIR}!" - -if [ $(ls -l ${DATA_DIR} | wc -l) -le 1 ]; then - abort "No restore data found within ${DATA_DIR}!" -fi -echo "[*] Restoring data..." -echo "[*] Restoring data with \"mongorestore --db parties --noIndexRestore --drop ${DATA_DIR}\"" &>> "${RESTORE_DIR}/${LOG_NAME}" -mongorestore --db parties --noIndexRestore --drop ${DATA_DIR} &>> "${RESTORE_DIR}/${LOG_NAME}" \ - || abort "Failed to execute mongorestore from ${DATA_DIR}!" -if [ $(cat ${RESTORE_DIR}/${LOG_NAME} | grep $(date +%Y) | wc -l) -lt 24 ]; then - warn "Little or no data could be restored from the backup!" -fi - -echo "[*] Preparing database..." -echo "[*] Preparing database with \"mongo parties --eval 'db.repairDatabase()' --verbose\"" &>> "${RESTORE_DIR}/${LOG_NAME}" -mongo parties --eval "db.repairDatabase()" --verbose &>> "${RESTORE_DIR}/${LOG_NAME}" \ - || abort "Failed to prepare database for usage!" - -echo "[+] Restore completed! Please with 'sudo systemctl restart snap.rocketchat-server.rocketchat-server' to verify." diff --git a/apps/meteor/.snapcraft/resources/startRocketChat b/apps/meteor/.snapcraft/resources/startRocketChat deleted file mode 100755 index 7e1cca2f0787..000000000000 --- a/apps/meteor/.snapcraft/resources/startRocketChat +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/bash - -function start_rocketchat { - echo "Checking if oplog has been enabled, and enabling if not" - LC_ALL=C mongo $SNAP/bin/initreplset.js - - echo "Checking if mongo featureCompatibilityVersion is correct, changing if not" - db_version=$(mongo --eval "printjson(db.version())" |tail -1 |tr -d '"' |cut -d. -f1,2) - db_comp_version=$(mongo --eval "printjson(db.adminCommand ({getParameter: 1, featureCompatibilityVersion: 1}))"|grep '"version" :' |cut -d: -f3 |cut -d} -f1|tr -d '[:space:]'|tr -d '"') - db_featureCompatibilityVersion="$(snapctl get db-feature-compatibility-version)" - if [[ $db_version == "3.6" ]] && [[ $db_comp_version == "3.4" ]] && [[ $db_featureCompatibilityVersion == "3.6" ]]; then - LC_ALL=C mongo --eval "printjson(db.adminCommand ({ setFeatureCompatibilityVersion: \"3.6\" }))" - fi - - ## For making fonts work for sharp - export XDG_DATA_HOME=$SNAP/usr/share - - # Font Config - export FONTCONFIG_PATH=$SNAP/etc/fonts/config.d - export FONTCONFIG_FILE=$SNAP/etc/fonts/fonts.conf - - - export DEPLOY_METHOD=snap - export NODE_ENV=production - export BABEL_CACHE_DIR=/tmp - export ROOT_URL=http://localhost - export PORT="$(snapctl get port)" - export MONGO_URL="$(snapctl get mongo-url)" - export MONGO_OPLOG_URL="$(snapctl get mongo-oplog-url)" - export Accounts_AvatarStorePath=$SNAP_COMMON/uploads - siteurl="$(snapctl get siteurl)" - if [ -n "$siteurl" ]; then - export OVERWRITE_SETTING_Site_Url=$siteurl - fi - - if ls $SNAP_COMMON/*.env >/dev/null 2>&1; then - for filename in $SNAP_COMMON/*.env; do - while read env_var; do - export "$env_var" - done < $filename - done - fi - - node $SNAP/main.js -} - -sleep_time=5 -try_times=0 - -function try_start { - - refreshing="$(snapctl get snap-refreshing)" - if [ $refreshing == "true" ]; then - exit 0 - fi - - search=$(ps --pid $(cat $SNAP_COMMON/mongod.pid) -o comm=) - - if [ $search ] - then - start_rocketchat - else - if [[ "$try_times" == 5 || "$try_times" > 5 ]]; then - echo "Was unable to connect to Mongo. Please make sure Mongo has started successfully: sudo systemctl status snap.rocketchat-server.rocketchat-mongo to view logs: sudo journalctl -u snap.rocketchat-server.rocketchat-mongo" - exit 1; - fi - - ((try_times += 1)) - ((sleep_time += 5)) - echo "Mongo is not available, can't start. Waiting ${sleep_time} seconds and trying again" - sleep $sleep_time - try_start - fi -} - -try_start diff --git a/apps/meteor/.snapcraft/resources/startmongo b/apps/meteor/.snapcraft/resources/startmongo deleted file mode 100755 index 5697c6df6dba..000000000000 --- a/apps/meteor/.snapcraft/resources/startmongo +++ /dev/null @@ -1 +0,0 @@ -env LC_ALL=C mongod --bind_ip 127.0.0.1 --pidfilepath $SNAP_COMMON/mongod.pid --smallfiles --journal --dbpath=$SNAP_COMMON --replSet rs0 diff --git a/apps/meteor/.snapcraft/snap/hooks/configure b/apps/meteor/.snapcraft/snap/hooks/configure deleted file mode 100755 index 3a2863d7de16..000000000000 --- a/apps/meteor/.snapcraft/snap/hooks/configure +++ /dev/null @@ -1,145 +0,0 @@ -#!/bin/bash - -export PATH="$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH" -export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$SNAP/lib:$SNAP/usr/lib:$SNAP/lib/x86_64-linux-gnu:$SNAP/usr/lib/x86_64-linux-gnu" -export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH - -# Obtain caddyurl value -caddyurl="$(snapctl get caddy-url)" -# Validate it -#caddyurl_regex='^https?:\/\/([0-9A-Za-z\.-]+){1,}(\.[a-z\.]{2,6})?([\/\da-z\.-]+)?$' #(supporting path) -caddyurl_regex='^https?:\/\/([0-9A-Za-z\.-]+){1,}(\.[a-z\.]{2,6})?$' -if [ -n "$caddyurl" ]; then - if ! [[ $caddyurl =~ $caddyurl_regex ]]; then - echo "\"$caddyurl\" is not a valid url" >&2 - exit 1 - fi -#else -# if [[ $siteurl =~ ^http: ]] && [[ $siteurl =~ ^http:\/\/([0-9A-Za-z\.-]+){1,}?(\.[a-z\.]{2,6})?\/([\/\da-z\.-]+){1,}$ ]]; then -# path=${siteurl#http://*/} -# site=${siteurl#"http://"} -# site=${site%%/*} -# site=http://$site -# elif [[ $siteurl =~ ^https: ]] && [[ $siteurl =~ ^https:\/\/([0-9A-Za-z\.-]+){1,}?(\.[a-z\.]{2,6})?\/([\/\da-z\.-]+){1,}$ ]]; then -# path=${siteurl#https://*/} -# site=${siteurl#"https://"} -# site=${site%%/*} -# site=https://$site -# else -# path="" -# site=$siteurl -# fi -# snapctl set path=$path -# snapctl set site=$site -fi - -# Obtain caddy value -caddy="$(snapctl get caddy)" -# Validate it -caddy_regex='((enable)|(disable))' -if ! [[ $caddy =~ $caddy_regex ]]; then - echo "\"$caddy_regex\" is not a valid, set to enable or disable" >&2 - exit 1 -else - if [[ $caddy == enable ]]; then - caddyurl="$(snapctl get caddy-url)" - if [ -z "$caddyurl" ]; then - echo "You tried to enable caddy but caddy-url is not set yet, please set up caddy-url first and then enable caddy again" >&2 - snapctl set caddy=disable - exit 1 - else - snapctl set siteurl=$caddyurl - fi - else - siteurl="$(snapctl get siteurl)" - if [ -n "$siteurl" ]; then - snapctl set siteurl= - fi - fi -fi - -# Obtain port value -port="$(snapctl get port)" -# Validate it -port_regex='^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$' -if ! [[ $port =~ $port_regex ]]; then - echo "\"$port\" is not a valid port" >&2 - exit 1 -fi - -# Obtain mongourl value -mongourl="$(snapctl get mongo-url)" -# Validate it -mongourl_regex='^mongodb:\/\/([0-9A-Za-z\.\-\_]+){1,}(([a-z\.\-\_]+){2,6})?(:[0-9]{2,5})?\/([0-9A-Za-z\_\-]+)$' -if ! [[ $mongourl =~ $mongourl_regex ]] ; then - echo "\"$mongourl\" is not a valid url" >&2 - exit 1 -fi - -# Obtain mongooplogurl value -mongooplogurl="$(snapctl get mongo-oplog-url)" -# Validate it -mongooplogurl_regex='^mongodb:\/\/([0-9A-Za-z\.\-\_]+){1,}(([a-z\.\-\_]+){2,6})?(:[0-9]{2,5})?\/local$' -if ! [[ $mongooplogurl =~ $mongooplogurl_regex ]] ; then - echo "\"$mongooplogurl\" is not a valid url" >&2 - exit 1 -fi - -# Obtain site protocol -https="$(snapctl get https)" -# Validate it -https_regex='((enable)|(disable))' -if ! [[ $https =~ $https_regex ]]; then - echo "\"$https\" should be enable or disable" >&2 - exit 1 -else - if [[ $https == enable ]] && [[ $caddyurl =~ ^http: ]]; then - echo "Error: You enabled https but your site URL starts with http, disabling https ..." - snapctl set https=disable - exit 1 - elif [[ $https == enable ]] && [[ $caddyurl =~ ^https: ]]; then - domain=${caddyurl#"https://"} - domain=${domain%%/*} - pubip=$(dig $domain |grep -A1 ";; ANSWER SECTION:" |tail -1 | awk '{print $5}') - if [ -z "$pubip" ]; then - echo "Error: Can't resove DNS query for $domain, check your DNS configuration, disabling https ..." - snapctl set https=disable - exit 1 - else - ip=$(curl ipinfo.io/ip 2>/dev/null) - if [[ $ip != $pubip ]]; then - echo "Error: Your public IP doesn't match the one resolved for caddy-url, disabling https ..." - snapctl set https=disable - exit 1 - fi - fi - elif [[ $https == enable ]] && [ -z "$caddyurl" ]; then - echo "Error: You enabled https but your site URL is empty, please set caddy-url=, disabling https ..." - snapctl set https=disable - exit 1 - fi -fi - -# Obtain backup value -backup="$(snapctl get backup-on-refresh)" -# Validate it -backup_regex='((enable)|(disable))' -if ! [[ $backup =~ $backup_regex ]] ; then - echo "\"$backup\" should be enable or disable" >&2 - exit 1 -fi - -# Obtain db featureCompatibilityVersion -db_featureCompatibilityVersion="$(snapctl get db-feature-compatibility-version)" -db_featureCompatibilityVersion_regex='^3\.[4,6]$' -if ! [[ $db_featureCompatibilityVersion =~ $db_featureCompatibilityVersion_regex ]] ; then - echo "\"$db_featureCompatibilityVersion\" should be 3.4 or 3.6" >&2 - exit 1 -else - db_version=$(mongo --eval "printjson(db.version())" |tail -1 |tr -d '"' |cut -d. -f1,2) - if [[ $db_version == "3.6" ]] && [[ $db_featureCompatibilityVersion == "3.4" ]]; then - mongo --eval "printjson(db.adminCommand ({ setFeatureCompatibilityVersion: \"3.4\" }))" - elif [[ $db_version == "3.6" ]] && [[ $db_featureCompatibilityVersion == "3.6" ]]; then - mongo --eval "printjson(db.adminCommand ({ setFeatureCompatibilityVersion: \"3.6\" }))" - fi -fi diff --git a/apps/meteor/.snapcraft/snap/hooks/install b/apps/meteor/.snapcraft/snap/hooks/install deleted file mode 100755 index c7ed881d4b82..000000000000 --- a/apps/meteor/.snapcraft/snap/hooks/install +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -# Initialize the CADDY_URL to a default -snapctl set caddy=disable - -# Initialize the PORT to a default -snapctl set port=3000 - -# Initialize the MONGO_URL to a default -snapctl set mongo-url=mongodb://localhost:27017/parties - -# Initialize the MONGO_OPLOG_URL to a default -snapctl set mongo-oplog-url=mongodb://localhost:27017/local - -# Initialize the protocol to a default -snapctl set https=disable - -# Initialize backup to a default -snapctl set backup-on-refresh=disable - -# Initialize snap-refreshing to false -snapctl set snap-refreshing=false - -# Initialize db-feature-compatibility-version to 3.6 -snapctl set db-feature-compatibility-version=3.6 - diff --git a/apps/meteor/.snapcraft/snap/hooks/post-refresh b/apps/meteor/.snapcraft/snap/hooks/post-refresh deleted file mode 100755 index ec0a988afd4c..000000000000 --- a/apps/meteor/.snapcraft/snap/hooks/post-refresh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -# Initialize the CADDY_URL to a default -caddy="$(snapctl get caddy)" -if [ -z "$caddy" ]; then - snapctl set caddy=disable -fi - -# Initialize the PORT to a default -port="$(snapctl get port)" -if [ -z "$port" ]; then - snapctl set port=3000 -fi - -# Initialize the MONGO_URL to a default -mongourl="$(snapctl get mongo-url)" -if [ -z "$mongourl" ]; then - snapctl set mongo-url=mongodb://localhost:27017/parties -fi - -# Initialize the MONGO_OPLOG_URL to a default -mongooplogurl="$(snapctl get mongo-oplog-url)" -if [ -z "$mongooplogurl" ]; then - snapctl set mongo-oplog-url=mongodb://localhost:27017/local -fi - -# Initialize the protocol to a default -https="$(snapctl get https)" -if [ -z "$https" ]; then - snapctl set https=disable -fi - -# Initialize backup to a default -backup="$(snapctl get backup-on-refresh)" -if [ -z "$backup" ]; then - snapctl set backup-on-refresh=disable -fi - -# Set back snap-refreshing to false -snapctl set snap-refreshing=false - -# Initialize db-feature-compatibility-version to a default -db_featureCompatibilityVersion="$(snapctl get db-feature-compatibility-version)" -if [ -z "$db_featureCompatibilityVersion" ]; then - snapctl set db-feature-compatibility-version=3.6 -fi - diff --git a/apps/meteor/.snapcraft/snap/hooks/pre-refresh b/apps/meteor/.snapcraft/snap/hooks/pre-refresh deleted file mode 100755 index 41d8df57a1e8..000000000000 --- a/apps/meteor/.snapcraft/snap/hooks/pre-refresh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -export PATH="$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH" -export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$SNAP/lib:$SNAP/usr/lib:$SNAP/lib/x86_64-linux-gnu:$SNAP/usr/lib/x86_64-linux-gnu" -export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH - -TIMESTAMP=$(date +"%Y%m%d.%H%M") - -# stop rocketchat -snapctl set snap-refreshing=true - -rocketchatpid=$(pgrep -f "node $SNAP/main.js") -kill -9 $rocketchatpid -if ! [ $? == 0 ]; then - echo "Failed to stop rocketchat service" > $SNAP_COMMON/refresh_${TIMESTAMP}.log -fi - -backup="$(snapctl get backup-on-refresh)" -if [ $backup == "enable" ]; then - backupdb - if ! [ $? == 0 ]; then - echo "Failed rocketchat database backup before refresh" >> $SNAP_COMMON/refresh_${TIMESTAMP}.log - fi -fi - diff --git a/apps/meteor/.snapcraft/snap/snapcraft.yaml b/apps/meteor/.snapcraft/snap/snapcraft.yaml deleted file mode 100644 index 3664bedd0fec..000000000000 --- a/apps/meteor/.snapcraft/snap/snapcraft.yaml +++ /dev/null @@ -1,112 +0,0 @@ -# -# Easiest way to work with this file, from an updated Ubuntu 16.04 LTS image -# 1. create a non-root user with sudo priv and perform following steps as non-root -# 2. `sudo apt-get update` -# 3. `sudo apt-get install snapcraft python build-essential` -# 4. `snapcraft stage` -# 5. `snapcraft snap` - -name: rocketchat-server -version: 4.8.2 -summary: Rocket.Chat server -description: Have your own Slack like online chat, built with Meteor. https://rocket.chat/ -confinement: strict -assumes: [snapd2.21] -apps: - rocketchat-server: - command: startRocketChat - daemon: simple - plugs: [network, network-bind, removable-media] - rocketchat-mongo: - command: startmongo - daemon: simple - plugs: [network, network-bind, network-observe] - rocketchat-caddy: - command: env LC_ALL=C caddy -conf=$SNAP_DATA/Caddyfile - daemon: simple - plugs: [network, network-bind] - mongo: - command: env LC_ALL=C mongo - plugs: [network] - restoredb: - command: env LC_ALL=C restoredb - plugs: [network] - backupdb: - command: env LC_ALL=C backupdb - plugs: [network] - initcaddy: - command: env LC_ALL=C initcaddy -hooks: - configure: - plugs: [network] - pre-refresh: - plugs: [network] -parts: - node: - plugin: dump - prepare: ./resources/preparenode - build-packages: - # For fibers - - python - - build-essential - - nodejs - rocketchat-server: - build-packages: - - curl - plugin: dump - prepare: ./resources/prepareRocketChat - after: [node] - source: . - stage-packages: - - execstack - - fontconfig-config - stage: - - programs - - main.js - - .node_version.txt - - etc - - usr - - star.json - mongodb: - build-packages: - - wget - source: ./ - prepare: ./resources/preparemongo - plugin: dump - stage-packages: - - libssl1.0.0 - prime: - - usr - - bin - - lib - scripts: - plugin: dump - source: resources/ - organize: - backupdb: bin/backupdb - restoredb: bin/restoredb - startmongo: bin/startmongo - startRocketChat: bin/startRocketChat - initreplset.js: bin/initreplset.js - Caddyfile: bin/Caddyfile - initcaddy: bin/initcaddy - prime: - - bin - caddy: - prepare: ./resources/preparecaddy - plugin: dump - source: ./ - prime: - - bin - organize: - caddy: bin/caddy - after: [mongodb] - hooks: - plugin: nil - stage-packages: - - dnsutils - - curl - prime: - - usr - - lib - diff --git a/apps/meteor/.storybook/main.js b/apps/meteor/.storybook/main.js index afb264d66851..71d317a497ca 100644 --- a/apps/meteor/.storybook/main.js +++ b/apps/meteor/.storybook/main.js @@ -3,8 +3,24 @@ const { resolve, relative, join } = require('path'); const webpack = require('webpack'); module.exports = { - stories: ['../app/**/*.stories.{js,tsx}', '../client/**/*.stories.{js,tsx}', '../ee/**/*.stories.{js,tsx}'], - addons: ['@storybook/addon-essentials', '@storybook/addon-interactions', '@storybook/addon-postcss'], + stories: [ + '../client/**/*.stories.{js,tsx}', + '../app/**/*.stories.{js,tsx}', + '../ee/app/**/*.stories.{js,tsx}', + '../ee/client/**/*.stories.{js,tsx}', + ], + addons: [ + '@storybook/addon-essentials', + '@storybook/addon-interactions', + { + name: '@storybook/addon-postcss', + options: { + postcssLoaderOptions: { + implementation: require('postcss'), + }, + }, + }, + ], webpackFinal: async (config) => { const cssRule = config.module.rules.find(({ test }) => test.test('index.css')); diff --git a/apps/meteor/app/2fa/server/functions/resetTOTP.ts b/apps/meteor/app/2fa/server/functions/resetTOTP.ts index 25da60ca2b1d..decce3432be5 100644 --- a/apps/meteor/app/2fa/server/functions/resetTOTP.ts +++ b/apps/meteor/app/2fa/server/functions/resetTOTP.ts @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import type { IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; import * as Mailer from '../../../mailer'; -import { Users } from '../../../models/server/raw/index'; const sendResetNotification = async function (uid: string): Promise { const user = await Users.findOneById>(uid, { @@ -45,9 +45,10 @@ const sendResetNotification = async function (uid: string): Promise { html, } as any); } catch (error) { - throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${error.message}`, { + const message = error instanceof Error ? error.message : String(error); + throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${message}`, { function: 'resetUserTOTP', - message: error.message, + message, }); } }); diff --git a/apps/meteor/app/2fa/server/lib/totp.ts b/apps/meteor/app/2fa/server/lib/totp.ts index ad831ce37029..c06a1ffc9b17 100644 --- a/apps/meteor/app/2fa/server/lib/totp.ts +++ b/apps/meteor/app/2fa/server/lib/totp.ts @@ -2,8 +2,7 @@ import { SHA256 } from 'meteor/sha'; import { Random } from 'meteor/random'; import speakeasy from 'speakeasy'; -// @ts-expect-error -import { Users } from '../../../models'; +import { Users } from '../../../models/server'; import { settings } from '../../../settings/server'; export const TOTP = { diff --git a/apps/meteor/app/action-links/server/lib/actionLinks.ts b/apps/meteor/app/action-links/server/lib/actionLinks.ts index 634e5a54de9f..4b441df64fcd 100644 --- a/apps/meteor/app/action-links/server/lib/actionLinks.ts +++ b/apps/meteor/app/action-links/server/lib/actionLinks.ts @@ -11,7 +11,7 @@ function getMessageById(messageId: IMessage['_id']): IMessage | undefined { } return Promise.await(getMessageForUser(messageId, user)); } catch (e) { - throw new Meteor.Error(e.message, 'Invalid message', { + throw new Meteor.Error(e instanceof Error ? e.message : String(e), 'Invalid message', { function: 'actionLinks.getMessage', }); } diff --git a/apps/meteor/app/analytics/client/trackEvents.js b/apps/meteor/app/analytics/client/trackEvents.js index 28c3987dc1b9..86cd69ea95a4 100644 --- a/apps/meteor/app/analytics/client/trackEvents.js +++ b/apps/meteor/app/analytics/client/trackEvents.js @@ -4,7 +4,7 @@ import { Tracker } from 'meteor/tracker'; import { settings } from '../../settings'; import { callbacks } from '../../../lib/callbacks'; -import { ChatRoom } from '../../models'; +import { ChatRoom } from '../../models/client'; function trackEvent(category, action, label) { if (window._paq) { diff --git a/apps/meteor/app/api/server/api.d.ts b/apps/meteor/app/api/server/api.d.ts index fc9519b87ea5..594f956edaea 100644 --- a/apps/meteor/app/api/server/api.d.ts +++ b/apps/meteor/app/api/server/api.d.ts @@ -9,6 +9,7 @@ import type { } from '@rocket.chat/rest-typings'; import type { IUser, IMethodConnection, IRoom } from '@rocket.chat/core-typings'; import type { ValidateFunction } from 'ajv'; +import type { Request, Response } from 'express'; import { ITwoFactorOptions } from '../../2fa/server/code'; @@ -68,21 +69,27 @@ type Options = ( } ) & { validateParams?: ValidateFunction; -}; - -type Request = { - method: 'GET' | 'POST' | 'PUT' | 'DELETE'; - url: string; - headers: Record; - body: any; + authOrAnonRequired?: true; }; type PartialThis = { readonly request: Request & { query: Record }; + readonly response: Response; +}; + +type UserInfo = IUser & { + email?: string; + settings: { + profile: {}; + preferences: unknown; + }; + avatarUrl: string; }; type ActionThis = { + readonly requestIp: string; urlParams: UrlParams; + readonly response: Response; // TODO make it unsafe readonly queryParams: TMethod extends 'GET' ? TOptions extends { validateParams: ValidateFunction } @@ -96,9 +103,12 @@ type ActionThis>; readonly request: Request; + + readonly queryOperations: TOptions extends { queryOperations: infer T } ? T : never; + /* @deprecated */ requestParams(): OperationParams; - getLoggedInUser(): IUser | undefined; + getLoggedInUser(): TOptions extends { authRequired: true } ? IUser : IUser | undefined; getPaginationItems(): { readonly offset: number; readonly count: number; @@ -110,16 +120,29 @@ type ActionThis({ object, userId }: { object: { [key: string]: unknown }; userId: string }): { [key: string]: unknown } & T; + /* @deprecated */ + isUserFromParams(): boolean; + /* @deprecated */ + getUserInfo( + me: IUser, + ): TOptions extends { authRequired: true } ? UserInfo : TOptions extends { authOrAnonRequired: true } ? UserInfo | undefined : undefined; composeRoomWithLastMessage(room: IRoom, userId: string): IRoom; } & (TOptions extends { authRequired: true } ? { readonly user: IUser; readonly userId: string; + readonly token: string; + } + : TOptions extends { authOrAnonRequired: true } + ? { + readonly user?: IUser; + readonly userId?: string; + readonly token?: string; } : { readonly user: null; - readonly userId: null; + readonly userId: undefined; + readonly token?: string; }); export type ResultFor = @@ -145,6 +168,8 @@ type Operations declare class APIClass { fieldSeparator: string; + updateRateLimiterDictionaryForRoute(route: string, rateLimiterDictionary: number): void; + limitedUserFieldsToExclude(fields: { [x: string]: unknown }, limitedUserFieldsToExclude: unknown): { [x: string]: unknown }; limitedUserFieldsToExcludeIfIsPrivilegedUser( diff --git a/apps/meteor/app/api/server/api.js b/apps/meteor/app/api/server/api.js index c88592af1e1f..17b31a3b53c6 100644 --- a/apps/meteor/app/api/server/api.js +++ b/apps/meteor/app/api/server/api.js @@ -379,6 +379,34 @@ export class APIClass extends Restivus { ...getRestPayload(this.request.body), }); + // If the endpoint requires authentication only if anonymous read is disabled, load the user info if it was provided + if (!options.authRequired && options.authOrAnonRequired) { + const { 'x-user-id': userId, 'x-auth-token': userToken } = this.request.headers; + if (userId && userToken) { + this.user = Meteor.users.findOne( + { + 'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(userToken), + '_id': userId, + }, + { + fields: getDefaultUserFields(), + }, + ); + + this.userId = this.user?._id; + } + + if (!this.user && !settings.get('Accounts_AllowAnonymousRead')) { + return { + statusCode: 401, + body: { + status: 'error', + message: 'You must be logged in to do this.', + }, + }; + } + } + const objectForRateLimitMatch = { IPAddr: this.requestIp, route: `${this.request.route}${this.request.method.toLowerCase()}`, diff --git a/apps/meteor/app/api/server/helpers/addUserToFileObj.ts b/apps/meteor/app/api/server/helpers/addUserToFileObj.ts new file mode 100644 index 000000000000..b09f1e700153 --- /dev/null +++ b/apps/meteor/app/api/server/helpers/addUserToFileObj.ts @@ -0,0 +1,19 @@ +import { IUpload, IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; + +export async function addUserToFileObj(files: IUpload[]): Promise<(IUpload & { user?: Pick })[]> { + const uids = files.map(({ userId }) => userId).filter(Boolean); + + const users = await Users.findByIds(uids, { projection: { name: 1, username: 1 } }).toArray(); + + return files.map((file) => { + const user = users.find(({ _id: userId }) => file.userId && userId === file.userId); + if (!user) { + return file; + } + return { + ...file, + user, + }; + }); +} diff --git a/apps/meteor/app/api/server/helpers/insertUserObject.ts b/apps/meteor/app/api/server/helpers/insertUserObject.ts deleted file mode 100644 index f3ea765534bf..000000000000 --- a/apps/meteor/app/api/server/helpers/insertUserObject.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Users } from '../../../models/server'; -import { API } from '../api'; - -API.helperMethods.set( - 'insertUserObject', - Meteor.bindEnvironment(function _addUserToObject({ object, userId }: { object: { [key: string]: unknown }; userId: string }) { - // Maybe `object: { [key: string]: Meteor.User }`? - const user = Users.findOneById(userId); - if (user) { - object.user = { - _id: userId, - username: user.username, - name: user.name, - }; - } - - return object; - }), -); diff --git a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts index 7e3592e765a0..25371d232e19 100644 --- a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts +++ b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts @@ -11,6 +11,13 @@ const pathAllowConf = { 'def': ['$or', '$and', '$regex'], }; +const warnFields = + process.env.NODE_ENV !== 'production' || process.env.SHOW_WARNINGS === 'true' + ? (...rest: any): void => { + console.warn(...rest, new Error().stack); + } + : new Function(); + API.helperMethods.set( 'parseJsonQuery', function _parseJsonQuery(this: { @@ -48,6 +55,7 @@ API.helperMethods.set( let fields: Record | undefined; if (this.queryParams.fields) { + warnFields('attribute fields is deprecated'); try { fields = JSON.parse(this.queryParams.fields) as Record; @@ -70,7 +78,7 @@ API.helperMethods.set( if (typeof fields === 'object') { let nonSelectableFields = Object.keys(API.v1.defaultFieldsToExclude); if (this.request.route.includes('/v1/users.')) { - const getFields = () => + const getFields = (): string[] => Object.keys( hasPermission(this.userId, 'view-full-other-user-info') ? API.v1.limitedUserFieldsToExcludeIfIsPrivilegedUser @@ -98,7 +106,8 @@ API.helperMethods.set( let query: Record = {}; if (this.queryParams.query) { - this.logger.warn('attribute query is deprecated'); + warnFields('attribute query is deprecated'); + try { query = EJSON.parse(this.queryParams.query); query = clean(query, pathAllowConf.def); diff --git a/apps/meteor/app/api/server/index.ts b/apps/meteor/app/api/server/index.ts index 3e2a60d115cb..53d0bbbad0cc 100644 --- a/apps/meteor/app/api/server/index.ts +++ b/apps/meteor/app/api/server/index.ts @@ -5,7 +5,6 @@ import './helpers/getLoggedInUser'; import './helpers/getPaginationItems'; import './helpers/getUserFromParams'; import './helpers/getUserInfo'; -import './helpers/insertUserObject'; import './helpers/isUserFromParams'; import './helpers/parseJsonQuery'; import './helpers/requestParams'; 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/lib/emailInbox.ts b/apps/meteor/app/api/server/lib/emailInbox.ts index c679c3d714ee..006dfb1b0d73 100644 --- a/apps/meteor/app/api/server/lib/emailInbox.ts +++ b/apps/meteor/app/api/server/lib/emailInbox.ts @@ -1,7 +1,7 @@ import { IEmailInbox } from '@rocket.chat/core-typings'; -import { InsertOneWriteOpResult, UpdateWriteOpResult, WithId } from 'mongodb'; +import { InsertOneResult, UpdateResult, WithId } from 'mongodb'; +import { EmailInbox } from '@rocket.chat/models'; -import { EmailInbox } from '../../../models/server/raw'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { Users } from '../../../models/server'; @@ -26,15 +26,13 @@ export const findEmailInboxes = async ({ if (!(await hasPermissionAsync(userId, 'manage-email-inbox'))) { throw new Error('error-not-allowed'); } - const cursor = EmailInbox.find(query, { + const { cursor, totalCount } = EmailInbox.findPaginated(query, { sort: sort || { name: 1 }, skip: offset, limit: count, }); - const total = await cursor.count(); - - const emailInboxes = await cursor.toArray(); + const [emailInboxes, total] = await Promise.all([cursor.toArray(), totalCount]); return { emailInboxes, @@ -53,7 +51,7 @@ export const findOneEmailInbox = async ({ userId, _id }: { userId: string; _id: export const insertOneEmailInbox = async ( userId: string, emailInboxParams: Pick, -): Promise>> => { +): Promise>> => { const obj = { ...emailInboxParams, _createdAt: new Date(), @@ -66,7 +64,7 @@ export const insertOneEmailInbox = async ( export const updateEmailInbox = async ( userId: string, emailInboxParams: Pick, -): Promise> | UpdateWriteOpResult> => { +): Promise> | UpdateResult> => { const { _id, active, name, email, description, senderInfo, department, smtp, imap } = emailInboxParams; const emailInbox = await findOneEmailInbox({ userId, _id }); diff --git a/apps/meteor/app/api/server/lib/emoji-custom.js b/apps/meteor/app/api/server/lib/emoji-custom.js deleted file mode 100644 index 1d7dde270664..000000000000 --- a/apps/meteor/app/api/server/lib/emoji-custom.js +++ /dev/null @@ -1,20 +0,0 @@ -import { EmojiCustom } from '../../../models/server/raw'; - -export async function findEmojisCustom({ query = {}, pagination: { offset, count, sort } }) { - const cursor = EmojiCustom.find(query, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - }); - - const total = await cursor.count(); - - const emojis = await cursor.toArray(); - - return { - emojis, - count: emojis.length, - offset, - total, - }; -} diff --git a/apps/meteor/app/api/server/lib/emoji-custom.ts b/apps/meteor/app/api/server/lib/emoji-custom.ts new file mode 100644 index 000000000000..b9450338503e --- /dev/null +++ b/apps/meteor/app/api/server/lib/emoji-custom.ts @@ -0,0 +1,31 @@ +import { IEmojiCustom } from '@rocket.chat/core-typings'; +import { Filter, FindOptions } from 'mongodb'; +import { EmojiCustom } from '@rocket.chat/models'; + +export async function findEmojisCustom({ + query = {}, + pagination: { offset, count, sort }, +}: { + query: Filter; + pagination: { offset: number; count: number; sort: FindOptions['sort'] }; +}): Promise<{ + emojis: IEmojiCustom[]; + count: number; + offset: any; + total: number; +}> { + const { cursor, totalCount } = EmojiCustom.findPaginated(query, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + }); + + const [emojis, total] = await Promise.all([cursor.toArray(), totalCount]); + + return { + emojis, + count: emojis.length, + offset, + total, + }; +} diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.js b/apps/meteor/app/api/server/lib/getUploadFormData.js deleted file mode 100644 index 39b0e7436c13..000000000000 --- a/apps/meteor/app/api/server/lib/getUploadFormData.js +++ /dev/null @@ -1,37 +0,0 @@ -import busboy from 'busboy'; - -export const getUploadFormData = async ({ request }) => - new Promise((resolve, reject) => { - const bb = busboy({ headers: request.headers, defParamCharset: 'utf8' }); - - const fields = {}; - - bb.on('file', (fieldname, file, { filename, encoding, mimeType: mimetype }) => { - const fileData = []; - - console.log(file); - file.on('data', (data) => fileData.push(data)); - - file.on('end', () => { - if (fields.hasOwnProperty(fieldname)) { - return reject('Just 1 file is allowed'); - } - - fields[fieldname] = { - file, - filename, - encoding, - mimetype, - fileBuffer: Buffer.concat(fileData), - }; - }); - }); - - bb.on('field', (fieldname, value) => { - fields[fieldname] = value; - }); - - bb.on('finish', () => resolve(fields)); - - request.pipe(bb); - }); diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.ts b/apps/meteor/app/api/server/lib/getUploadFormData.ts new file mode 100644 index 000000000000..f20075823bb5 --- /dev/null +++ b/apps/meteor/app/api/server/lib/getUploadFormData.ts @@ -0,0 +1,84 @@ +import { Readable } from 'stream'; + +import { Meteor } from 'meteor/meteor'; +import type { Request } from 'express'; +import busboy from 'busboy'; +import { ValidateFunction } from 'ajv'; + +type UploadResult = { + file: Readable; + filename: string; + encoding: string; + mimetype: string; + fileBuffer: Buffer; +}; + +export const getUploadFormData = async < + T extends string, + K extends Record = Record, + V extends ValidateFunction = ValidateFunction, +>( + { request }: { request: Request }, + options: { + field?: T; + validate?: V; + } = {}, +): Promise<[UploadResult, K, T]> => + new Promise((resolve, reject) => { + const bb = busboy({ headers: request.headers, defParamCharset: 'utf8' }); + const fields = Object.create(null) as K; + + let uploadedFile: UploadResult | undefined; + + let assetName: T | undefined; + + bb.on( + 'file', + ( + fieldname: string, + file: Readable, + { filename, encoding, mimeType: mimetype }: { filename: string; encoding: string; mimeType: string }, + ) => { + const fileData: Uint8Array[] = []; + + file.on('data', (data: any) => fileData.push(data)); + + file.on('end', () => { + if (uploadedFile) { + return reject('Just 1 file is allowed'); + } + if (options.field && fieldname !== options.field) { + return reject(new Meteor.Error('invalid-field')); + } + uploadedFile = { + file, + filename, + encoding, + mimetype, + fileBuffer: Buffer.concat(fileData), + }; + + assetName = fieldname as T; + }); + }, + ); + + bb.on('field', (fieldname: keyof K, value: K[keyof K]) => { + fields[fieldname] = value; + }); + + bb.on('finish', () => { + if (!uploadedFile || !assetName) { + return reject('No file uploaded'); + } + if (options.validate === undefined) { + return resolve([uploadedFile, fields, assetName]); + } + if (!options.validate(fields)) { + return reject(`Invalid fields${options.validate.errors?.join(', ')}`); + } + return resolve([uploadedFile, fields, assetName]); + }); + + request.pipe(bb); + }); diff --git a/apps/meteor/app/api/server/lib/integrations.ts b/apps/meteor/app/api/server/lib/integrations.ts index c785984380e5..517b64ad3440 100644 --- a/apps/meteor/app/api/server/lib/integrations.ts +++ b/apps/meteor/app/api/server/lib/integrations.ts @@ -1,6 +1,6 @@ import type { IIntegration, IUser } from '@rocket.chat/core-typings'; +import { Integrations } from '@rocket.chat/models'; -import { Integrations } from '../../../models/server/raw'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; const hasIntegrationsPermission = async (userId: string, integration: IIntegration): Promise => { diff --git a/apps/meteor/app/api/server/lib/messages.js b/apps/meteor/app/api/server/lib/messages.js deleted file mode 100644 index d06eef7f0671..000000000000 --- a/apps/meteor/app/api/server/lib/messages.js +++ /dev/null @@ -1,142 +0,0 @@ -import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; -import { Rooms, Messages, Users } from '../../../models/server/raw'; -import { getValue } from '../../../settings/server/raw'; - -export async function findMentionedMessages({ uid, roomId, pagination: { offset, count, sort } }) { - const room = await Rooms.findOneById(roomId); - if (!(await canAccessRoomAsync(room, { _id: uid }))) { - throw new Error('error-not-allowed'); - } - const user = await Users.findOneById(uid, { fields: { username: 1 } }); - if (!user) { - throw new Error('invalid-user'); - } - - const cursor = await Messages.findVisibleByMentionAndRoomId(user.username, roomId, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - }); - - const total = await cursor.count(); - - const messages = await cursor.toArray(); - - return { - messages, - count: messages.length, - offset, - total, - }; -} - -export async function findStarredMessages({ uid, roomId, pagination: { offset, count, sort } }) { - const room = await Rooms.findOneById(roomId); - if (!(await canAccessRoomAsync(room, { _id: uid }))) { - throw new Error('error-not-allowed'); - } - const user = await Users.findOneById(uid, { fields: { username: 1 } }); - if (!user) { - throw new Error('invalid-user'); - } - - const cursor = await Messages.findStarredByUserAtRoom(uid, roomId, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - }); - - const total = await cursor.count(); - - const messages = await cursor.toArray(); - - return { - messages, - count: messages.length, - offset, - total, - }; -} - -export async function findSnippetedMessageById({ uid, messageId }) { - if (!(await getValue('Message_AllowSnippeting'))) { - throw new Error('error-not-allowed'); - } - - if (!uid) { - throw new Error('invalid-user'); - } - - const snippet = await Messages.findOne({ _id: messageId, snippeted: true }); - - if (!snippet) { - throw new Error('invalid-message'); - } - - const room = await Rooms.findOneById(snippet.rid); - - if (!room) { - throw new Error('invalid-message'); - } - - if (!(await canAccessRoomAsync(room, { _id: uid }))) { - throw new Error('error-not-allowed'); - } - - return { - message: snippet, - }; -} - -export async function findSnippetedMessages({ uid, roomId, pagination: { offset, count, sort } }) { - if (!(await getValue('Message_AllowSnippeting'))) { - throw new Error('error-not-allowed'); - } - const room = await Rooms.findOneById(roomId); - - if (!(await canAccessRoomAsync(room, { _id: uid }))) { - throw new Error('error-not-allowed'); - } - - const cursor = await Messages.findSnippetedByRoom(roomId, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - }); - - const total = await cursor.count(); - - const messages = await cursor.toArray(); - - return { - messages, - count: messages.length, - offset, - total, - }; -} - -export async function findDiscussionsFromRoom({ uid, roomId, text, pagination: { offset, count, sort } }) { - const room = await Rooms.findOneById(roomId); - - if (!(await canAccessRoomAsync(room, { _id: uid }))) { - throw new Error('error-not-allowed'); - } - - const cursor = Messages.findDiscussionsByRoomAndText(roomId, text, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - }); - - const total = await cursor.count(); - - const messages = await cursor.toArray(); - - return { - messages, - count: messages.length, - offset, - total, - }; -} diff --git a/apps/meteor/app/api/server/lib/messages.ts b/apps/meteor/app/api/server/lib/messages.ts new file mode 100644 index 000000000000..50f3df5e7688 --- /dev/null +++ b/apps/meteor/app/api/server/lib/messages.ts @@ -0,0 +1,189 @@ +import type { FindOptions } from 'mongodb'; +import type { IMessage, IUser } from '@rocket.chat/core-typings'; +import { Rooms, Messages, Users } from '@rocket.chat/models'; + +import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; +import { getValue } from '../../../settings/server/raw'; + +export async function findMentionedMessages({ + uid, + roomId, + pagination: { offset, count, sort }, +}: { + uid: string; + roomId: string; + pagination: { offset: number; count: number; sort: FindOptions['sort'] }; +}): Promise<{ + messages: IMessage[]; + count: number; + offset: number; + total: number; +}> { + const room = await Rooms.findOneById(roomId); + if (!room || !(await canAccessRoomAsync(room, { _id: uid }))) { + throw new Error('error-not-allowed'); + } + const user: IUser | null = await Users.findOneById(uid, { fields: { username: 1 } }); + if (!user) { + throw new Error('invalid-user'); + } + + const { cursor, totalCount } = Messages.findVisibleByMentionAndRoomId(user.username, roomId, { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + }); + + const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); + + return { + messages, + count: messages.length, + offset, + total, + }; +} + +export async function findStarredMessages({ + uid, + roomId, + pagination: { offset, count, sort }, +}: { + uid: string; + roomId: string; + pagination: { offset: number; count: number; sort: FindOptions['sort'] }; +}): Promise<{ + messages: IMessage[]; + count: number; + offset: any; + total: number; +}> { + const room = await Rooms.findOneById(roomId); + if (!room || !(await canAccessRoomAsync(room, { _id: uid }))) { + throw new Error('error-not-allowed'); + } + const user = await Users.findOneById(uid, { fields: { username: 1 } }); + if (!user) { + throw new Error('invalid-user'); + } + + const { cursor, totalCount } = Messages.findStarredByUserAtRoom(uid, roomId, { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + }); + + const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); + + return { + messages, + count: messages.length, + offset, + total, + }; +} + +export async function findSnippetedMessageById({ uid, messageId }: { uid: string; messageId: string }): Promise { + if (!(await getValue('Message_AllowSnippeting'))) { + throw new Error('error-not-allowed'); + } + + if (!uid) { + throw new Error('invalid-user'); + } + + const snippet = await Messages.findOne({ _id: messageId, snippeted: true }); + + if (!snippet) { + throw new Error('invalid-message'); + } + + const room = await Rooms.findOneById(snippet.rid); + + if (!room) { + throw new Error('invalid-message'); + } + + if (!(await canAccessRoomAsync(room, { _id: uid }))) { + throw new Error('error-not-allowed'); + } + + return snippet; +} + +export async function findSnippetedMessages({ + uid, + roomId, + pagination: { offset, count, sort }, +}: { + uid: string; + roomId: string; + pagination: { offset: number; count: number; sort: FindOptions['sort'] }; +}): Promise<{ + messages: IMessage[]; + count: number; + offset: number; + total: number; +}> { + if (!(await getValue('Message_AllowSnippeting'))) { + throw new Error('error-not-allowed'); + } + const room = await Rooms.findOneById(roomId); + + if (!room || !(await canAccessRoomAsync(room, { _id: uid }))) { + throw new Error('error-not-allowed'); + } + + const { cursor, totalCount } = Messages.findSnippetedByRoom(roomId, { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + }); + + const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); + + return { + messages, + count: messages.length, + offset, + total, + }; +} + +export async function findDiscussionsFromRoom({ + uid, + roomId, + text, + pagination: { offset, count, sort }, +}: { + uid: string; + roomId: string; + text: string; + pagination: { offset: number; count: number; sort: FindOptions['sort'] }; +}): Promise<{ + messages: IMessage[]; + count: number; + offset: number; + total: number; +}> { + const room = await Rooms.findOneById(roomId); + + if (!room || !(await canAccessRoomAsync(room, { _id: uid }))) { + throw new Error('error-not-allowed'); + } + + const { cursor, totalCount } = await Messages.findDiscussionsByRoomAndText(roomId, text, { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + }); + + const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); + + return { + messages, + count: messages.length, + offset, + total, + }; +} diff --git a/apps/meteor/app/api/server/lib/rooms.js b/apps/meteor/app/api/server/lib/rooms.js deleted file mode 100644 index 0e402dec3ffe..000000000000 --- a/apps/meteor/app/api/server/lib/rooms.js +++ /dev/null @@ -1,156 +0,0 @@ -import { hasPermissionAsync, hasAtLeastOnePermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { Rooms } from '../../../models/server/raw'; -import { Subscriptions } from '../../../models/server'; -import { adminFields } from '../../../../lib/rooms/adminFields'; - -export async function findAdminRooms({ uid, filter, types = [], pagination: { offset, count, sort } }) { - if (!(await hasPermissionAsync(uid, 'view-room-administration'))) { - throw new Error('error-not-authorized'); - } - const name = filter && filter.trim(); - const discussion = types && types.includes('discussions'); - const includeTeams = types && types.includes('teams'); - const showOnlyTeams = types.length === 1 && types.includes('teams'); - const typesToRemove = ['discussions', 'teams']; - const showTypes = Array.isArray(types) ? types.filter((type) => !typesToRemove.includes(type)) : []; - const options = { - fields: adminFields, - sort: sort || { default: -1, name: 1 }, - skip: offset, - limit: count, - }; - - let cursor; - if (name && showTypes.length) { - cursor = Rooms.findByNameContainingAndTypes(name, showTypes, discussion, includeTeams, showOnlyTeams, options); - } else if (showTypes.length) { - cursor = Rooms.findByTypes(showTypes, discussion, includeTeams, showOnlyTeams, options); - } else { - cursor = Rooms.findByNameContaining(name, discussion, includeTeams, showOnlyTeams, options); - } - - const total = await cursor.count(); - - const rooms = await cursor.toArray(); - - return { - rooms, - count: rooms.length, - offset, - total, - }; -} - -export async function findAdminRoom({ uid, rid }) { - if (!(await hasPermissionAsync(uid, 'view-room-administration'))) { - throw new Error('error-not-authorized'); - } - - return Rooms.findOneById(rid, { fields: adminFields }); -} - -export async function findChannelAndPrivateAutocomplete({ uid, selector }) { - const options = { - fields: { - _id: 1, - fname: 1, - name: 1, - t: 1, - avatarETag: 1, - }, - limit: 10, - sort: { - name: 1, - }, - }; - - const userRoomsIds = Subscriptions.cachedFindByUserId(uid, { fields: { rid: 1 } }) - .fetch() - .map((item) => item.rid); - - const rooms = await Rooms.findRoomsWithoutDiscussionsByRoomIds(selector.name, userRoomsIds, options).toArray(); - - return { - items: rooms, - }; -} - -export async function findAdminRoomsAutocomplete({ uid, selector }) { - if (!(await hasAtLeastOnePermissionAsync(uid, ['view-room-administration', 'can-audit']))) { - throw new Error('error-not-authorized'); - } - const options = { - fields: { - _id: 1, - fname: 1, - name: 1, - t: 1, - avatarETag: 1, - }, - limit: 10, - sort: { - name: 1, - }, - }; - - const rooms = await Rooms.findRoomsByNameOrFnameStarting(selector.name, options).toArray(); - - return { - items: rooms, - }; -} - -export async function findChannelAndPrivateAutocompleteWithPagination({ uid, selector, pagination: { offset, count, sort } }) { - const userRoomsIds = Subscriptions.cachedFindByUserId(uid, { fields: { rid: 1 } }) - .fetch() - .map((item) => item.rid); - - const options = { - fields: { - _id: 1, - fname: 1, - name: 1, - t: 1, - avatarETag: 1, - }, - sort: sort || { name: 1 }, - skip: offset, - limit: count, - }; - - const cursor = await Rooms.findRoomsWithoutDiscussionsByRoomIds(selector.name, userRoomsIds, options); - - const total = await cursor.count(); - const rooms = await cursor.toArray(); - - return { - items: rooms, - total, - }; -} - -export async function findRoomsAvailableForTeams({ uid, name }) { - const options = { - fields: { - _id: 1, - fname: 1, - name: 1, - t: 1, - avatarETag: 1, - }, - limit: 10, - sort: { - name: 1, - }, - }; - - const userRooms = Subscriptions.findByUserIdAndRoles(uid, ['owner'], { fields: { rid: 1 } }) - .fetch() - .map((item) => item.rid); - - const rooms = await Rooms.findChannelAndGroupListWithoutTeamsByNameStartingByOwner(uid, name, userRooms, options).toArray(); - - return { - items: rooms, - }; -} diff --git a/apps/meteor/app/api/server/lib/rooms.ts b/apps/meteor/app/api/server/lib/rooms.ts new file mode 100644 index 000000000000..2c189cdd3fd9 --- /dev/null +++ b/apps/meteor/app/api/server/lib/rooms.ts @@ -0,0 +1,189 @@ +import { IRoom, ISubscription } from '@rocket.chat/core-typings'; +import { Rooms } from '@rocket.chat/models'; + +import { hasPermissionAsync, hasAtLeastOnePermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { Subscriptions } from '../../../models/server'; +import { adminFields } from '../../../../lib/rooms/adminFields'; + +export async function findAdminRooms({ + uid, + filter, + types = [], + pagination: { offset, count, sort }, +}: { + uid: string; + filter: string; + types: string[]; + pagination: { offset: number; count: number; sort: [string, number][] }; +}): Promise<{ + rooms: IRoom[]; + count: number; + offset: number; + total: number; +}> { + if (!(await hasPermissionAsync(uid, 'view-room-administration'))) { + throw new Error('error-not-authorized'); + } + const name = filter?.trim(); + const discussion = types?.includes('discussions'); + const includeTeams = types?.includes('teams'); + const showOnlyTeams = types.length === 1 && types.includes('teams'); + const typesToRemove = ['discussions', 'teams']; + const showTypes = Array.isArray(types) ? types.filter((type) => !typesToRemove.includes(type)) : []; + const options = { + fields: adminFields, + sort: sort || { default: -1, name: 1 }, + skip: offset, + limit: count, + }; + + let result; + if (name && showTypes.length) { + result = Rooms.findByNameContainingAndTypes(name, showTypes, discussion, includeTeams, showOnlyTeams, options); + } else if (showTypes.length) { + result = Rooms.findByTypes(showTypes, discussion, includeTeams, showOnlyTeams, options); + } else { + result = Rooms.findByNameContaining(name, discussion, includeTeams, showOnlyTeams, options); + } + + const { cursor, totalCount } = result; + + const [rooms, total] = await Promise.all([cursor.toArray(), totalCount]); + + return { + rooms, + count: rooms.length, + offset, + total, + }; +} + +export async function findAdminRoom({ uid, rid }: { uid: string; rid: string }): Promise { + if (!(await hasPermissionAsync(uid, 'view-room-administration'))) { + throw new Error('error-not-authorized'); + } + + return Rooms.findOneById(rid, { fields: adminFields }); +} + +export async function findChannelAndPrivateAutocomplete({ uid, selector }: { uid: string; selector: { name: string } }): Promise<{ + items: IRoom[]; +}> { + const options = { + fields: { + _id: 1, + fname: 1, + name: 1, + t: 1, + avatarETag: 1, + }, + limit: 10, + sort: { + name: 1, + }, + }; + + const userRoomsIds = Subscriptions.cachedFindByUserId(uid, { fields: { rid: 1 } }) + .fetch() + .map((item: Pick) => item.rid); + + const rooms = await Rooms.findRoomsWithoutDiscussionsByRoomIds(selector.name, userRoomsIds, options).toArray(); + + return { + items: rooms, + }; +} + +export async function findAdminRoomsAutocomplete({ uid, selector }: { uid: string; selector: { name: string } }): Promise<{ + items: IRoom[]; +}> { + if (!(await hasAtLeastOnePermissionAsync(uid, ['view-room-administration', 'can-audit']))) { + throw new Error('error-not-authorized'); + } + const options = { + fields: { + _id: 1, + fname: 1, + name: 1, + t: 1, + avatarETag: 1, + }, + limit: 10, + sort: { + name: 1, + }, + }; + + const rooms = await Rooms.findRoomsByNameOrFnameStarting(selector.name, options).toArray(); + + return { + items: rooms, + }; +} + +export async function findChannelAndPrivateAutocompleteWithPagination({ + uid, + selector, + pagination: { offset, count, sort }, +}: { + uid: string; + selector: { name: string }; + pagination: { offset: number; count: number; sort: [string, number][] }; +}): Promise<{ + items: IRoom[]; + total: number; +}> { + const userRoomsIds = Subscriptions.cachedFindByUserId(uid, { fields: { rid: 1 } }) + .fetch() + .map((item: Pick) => item.rid); + + const options = { + fields: { + _id: 1, + fname: 1, + name: 1, + t: 1, + avatarETag: 1, + }, + sort: sort || { name: 1 }, + skip: offset, + limit: count, + }; + + const { cursor, totalCount } = Rooms.findPaginatedRoomsWithoutDiscussionsByRoomIds(selector.name, userRoomsIds, options); + + const [rooms, total] = await Promise.all([cursor.toArray(), totalCount]); + + return { + items: rooms, + total, + }; +} + +export async function findRoomsAvailableForTeams({ uid, name }: { uid: string; name: string }): Promise<{ + items: IRoom[]; +}> { + const options = { + fields: { + _id: 1, + fname: 1, + name: 1, + t: 1, + avatarETag: 1, + }, + limit: 10, + sort: { + name: 1, + }, + }; + + const userRooms = ( + Subscriptions.findByUserIdAndRoles(uid, ['owner'], { fields: { rid: 1 } }).fetch() as Pick[] + ).map((item) => item.rid); + + const rooms = await Rooms.findChannelAndGroupListWithoutTeamsByNameStartingByOwner(uid, name, userRooms, options).toArray(); + + return { + items: rooms, + }; +} diff --git a/apps/meteor/app/api/server/lib/users.js b/apps/meteor/app/api/server/lib/users.js deleted file mode 100644 index 1e7eb9a7a242..000000000000 --- a/apps/meteor/app/api/server/lib/users.js +++ /dev/null @@ -1,95 +0,0 @@ -import { escapeRegExp } from '@rocket.chat/string-helpers'; - -import { Users } from '../../../models/server/raw'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; - -export async function findUsersToAutocomplete({ uid, selector }) { - if (!(await hasPermissionAsync(uid, 'view-outside-room'))) { - return { items: [] }; - } - const exceptions = selector.exceptions || []; - const conditions = selector.conditions || {}; - const options = { - projection: { - name: 1, - username: 1, - nickname: 1, - status: 1, - avatarETag: 1, - }, - sort: { - username: 1, - }, - limit: 10, - }; - - const users = await Users.findActiveByUsernameOrNameRegexWithExceptionsAndConditions( - new RegExp(escapeRegExp(selector.term), 'i'), - exceptions, - conditions, - options, - ).toArray(); - - return { - items: users, - }; -} - -/** - * Returns a new query object with the inclusive fields only - * @param {Object} query search query for matching rows - */ -export function getInclusiveFields(query) { - const newQuery = {}; - - for (const [key, value] of Object.entries(query)) { - if (value === 1) { - newQuery[key] = value; - } - } - - return newQuery; -} - -/** - * get the default fields if **fields** are empty (`{}`) or `undefined`/`null` - * @param {Object|null|undefined} fields the fields from parsed jsonQuery - */ -export function getNonEmptyFields(fields) { - const defaultFields = { - name: 1, - username: 1, - emails: 1, - roles: 1, - status: 1, - active: 1, - avatarETag: 1, - lastLogin: 1, - }; - - if (!fields || Object.keys(fields).length === 0) { - return defaultFields; - } - - return { ...defaultFields, ...fields }; -} - -/** - * get the default query if **query** is empty (`{}`) or `undefined`/`null` - * @param {Object|null|undefined} query the query from parsed jsonQuery - */ -export function getNonEmptyQuery(query) { - const defaultQuery = { - $or: [ - { 'emails.address': { $regex: '', $options: 'i' } }, - { username: { $regex: '', $options: 'i' } }, - { name: { $regex: '', $options: 'i' } }, - ], - }; - - if (!query || Object.keys(query).length === 0) { - return defaultQuery; - } - - return { ...defaultQuery, ...query }; -} diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts new file mode 100644 index 000000000000..d683df20f192 --- /dev/null +++ b/apps/meteor/app/api/server/lib/users.ts @@ -0,0 +1,112 @@ +import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { IUser } from '@rocket.chat/core-typings'; +import { Filter } from 'mongodb'; +import { Users } from '@rocket.chat/models'; +import type { Mongo } from 'meteor/mongo'; + +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; + +type UserAutoComplete = Required>; +export async function findUsersToAutocomplete({ + uid, + selector, +}: { + uid: string; + selector: { + exceptions: string[]; + conditions: Filter; + term: string; + }; +}): Promise<{ + items: UserAutoComplete[]; +}> { + if (!(await hasPermissionAsync(uid, 'view-outside-room'))) { + return { items: [] }; + } + const exceptions = selector.exceptions || []; + const conditions = selector.conditions || {}; + const options = { + projection: { + name: 1, + username: 1, + nickname: 1, + status: 1, + avatarETag: 1, + }, + sort: { + username: 1, + }, + limit: 10, + }; + + const users = await Users.findActiveByUsernameOrNameRegexWithExceptionsAndConditions( + new RegExp(escapeRegExp(selector.term), 'i'), + exceptions, + conditions, + options, + ).toArray(); + + return { + items: users, + }; +} + +/** + * Returns a new query object with the inclusive fields only + * @param {Object} query search query for matching rows + */ +export function getInclusiveFields(query: { [k: string]: 1 }): {} { + const newQuery = Object.create(null); + + for (const [key, value] of Object.entries(query)) { + if (value === 1) { + newQuery[key] = value; + } + } + + return newQuery; +} + +/** + * get the default fields if **fields** are empty (`{}`) or `undefined`/`null` + * @param {Object|null|undefined} fields the fields from parsed jsonQuery + */ +export function getNonEmptyFields(fields: { [k: string]: 1 | 0 }): { [k: string]: 1 } { + const defaultFields = { + name: 1, + username: 1, + emails: 1, + roles: 1, + status: 1, + active: 1, + avatarETag: 1, + lastLogin: 1, + type: 1, + } as const; + + if (!fields || Object.keys(fields).length === 0) { + return defaultFields; + } + + return { ...defaultFields, ...fields }; +} + +/** + * get the default query if **query** is empty (`{}`) or `undefined`/`null` + * @param {Object|null|undefined} query the query from parsed jsonQuery + */ +export function getNonEmptyQuery(query: Mongo.Query | undefined | null, canSeeAllUserInfo?: boolean): Mongo.Query { + const defaultQuery: Mongo.Query = { + $or: [{ username: { $regex: '', $options: 'i' } }, { name: { $regex: '', $options: 'i' } }], + }; + + if (canSeeAllUserInfo) { + defaultQuery.$or?.push({ 'emails.address': { $regex: '', $options: 'i' } }); + } + + if (!query || Object.keys(query).length === 0) { + return defaultQuery; + } + + return { ...defaultQuery, ...query }; +} diff --git a/apps/meteor/app/api/server/lib/webdav.ts b/apps/meteor/app/api/server/lib/webdav.ts index 7f3701a7d695..db0c47a64e2d 100644 --- a/apps/meteor/app/api/server/lib/webdav.ts +++ b/apps/meteor/app/api/server/lib/webdav.ts @@ -1,6 +1,5 @@ import type { IWebdavAccount } from '@rocket.chat/core-typings'; - -import { WebdavAccounts } from '../../../models/server/raw'; +import { WebdavAccounts } from '@rocket.chat/models'; export async function findWebdavAccountsByUserId({ uid }: { uid: string }): Promise { return WebdavAccounts.findWithUserId(uid, { diff --git a/apps/meteor/app/api/server/middlewares/authentication.ts b/apps/meteor/app/api/server/middlewares/authentication.ts new file mode 100644 index 000000000000..268000ef4cb8 --- /dev/null +++ b/apps/meteor/app/api/server/middlewares/authentication.ts @@ -0,0 +1,33 @@ +import { Request, Response, NextFunction } from 'express'; + +import { Users } from '../../../models/server'; +import { oAuth2ServerAuth } from '../../../oauth2-server-config/server/oauth/oauth2-server'; + +export type AuthenticationMiddlewareConfig = { + rejectUnauthorized: boolean; +}; + +export const defaultAuthenticationMiddlewareConfig = { + rejectUnauthorized: true, +}; + +export function authenticationMiddleware(config: AuthenticationMiddlewareConfig = defaultAuthenticationMiddlewareConfig) { + return (req: Request, res: Response, next: NextFunction): void => { + const { 'x-user-id': userId, 'x-auth-token': authToken } = req.headers; + + if (userId && authToken) { + req.user = Users.findOneByIdAndLoginToken(userId, authToken); + } else { + req.user = oAuth2ServerAuth(req)?.user; + } + + if (config.rejectUnauthorized && !req.user) { + res.status(401).send('Unauthorized'); + return; + } + + req.userId = req?.user?._id; + + next(); + }; +} diff --git a/apps/meteor/app/api/server/v1/assets.js b/apps/meteor/app/api/server/v1/assets.js deleted file mode 100644 index c232d3c0ff2c..000000000000 --- a/apps/meteor/app/api/server/v1/assets.js +++ /dev/null @@ -1,60 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { RocketChatAssets } from '../../../assets/server'; -import { API } from '../api'; -import { getUploadFormData } from '../lib/getUploadFormData'; - -API.v1.addRoute( - 'assets.setAsset', - { authRequired: true }, - { - post() { - const { refreshAllClients, ...files } = Promise.await( - getUploadFormData({ - request: this.request, - }), - ); - - const assetsKeys = Object.keys(RocketChatAssets.assets); - - const [assetName] = Object.keys(files); - - const isValidAsset = assetsKeys.includes(assetName); - if (!isValidAsset) { - throw new Meteor.Error('error-invalid-asset', 'Invalid asset'); - } - - Meteor.runAsUser(this.userId, () => { - const { [assetName]: asset } = files; - - Meteor.call('setAsset', asset.fileBuffer, asset.mimetype, assetName); - if (refreshAllClients) { - Meteor.call('refreshClients'); - } - }); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'assets.unsetAsset', - { authRequired: true }, - { - post() { - const { assetName, refreshAllClients } = this.bodyParams; - const isValidAsset = Object.keys(RocketChatAssets.assets).includes(assetName); - if (!isValidAsset) { - throw new Meteor.Error('error-invalid-asset', 'Invalid asset'); - } - Meteor.runAsUser(this.userId, () => { - Meteor.call('unsetAsset', assetName); - if (refreshAllClients) { - Meteor.call('refreshClients'); - } - }); - return API.v1.success(); - }, - }, -); diff --git a/apps/meteor/app/api/server/v1/assets.ts b/apps/meteor/app/api/server/v1/assets.ts new file mode 100644 index 000000000000..655d4fa49e23 --- /dev/null +++ b/apps/meteor/app/api/server/v1/assets.ts @@ -0,0 +1,56 @@ +import { Meteor } from 'meteor/meteor'; +import { isAssetsUnsetAssetProps } from '@rocket.chat/rest-typings'; + +import { RocketChatAssets } from '../../../assets/server'; +import { API } from '../api'; +import { getUploadFormData } from '../lib/getUploadFormData'; + +API.v1.addRoute( + 'assets.setAsset', + { authRequired: true }, + { + async post() { + const [asset, { refreshAllClients }, assetName] = await getUploadFormData({ + request: this.request, + }); + + const assetsKeys = Object.keys(RocketChatAssets.assets); + + const isValidAsset = assetsKeys.includes(assetName); + if (!isValidAsset) { + throw new Meteor.Error('error-invalid-asset', 'Invalid asset'); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('setAsset', asset.fileBuffer, asset.mimetype, assetName); + if (refreshAllClients) { + Meteor.call('refreshClients'); + } + }); + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'assets.unsetAsset', + { + authRequired: true, + validateParams: isAssetsUnsetAssetProps, + }, + { + post() { + const { assetName, refreshAllClients } = this.bodyParams; + const isValidAsset = Object.keys(RocketChatAssets.assets).includes(assetName); + if (!isValidAsset) { + throw new Meteor.Error('error-invalid-asset', 'Invalid asset'); + } + Meteor.call('unsetAsset', assetName); + if (refreshAllClients) { + Meteor.call('refreshClients'); + } + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/channels.js b/apps/meteor/app/api/server/v1/channels.js index b5137b487223..0ba1e876094a 100644 --- a/apps/meteor/app/api/server/v1/channels.js +++ b/apps/meteor/app/api/server/v1/channels.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import _ from 'underscore'; +import { Integrations, Uploads, Messages as MessagesRaw, Rooms as RoomsRaw, Subscriptions as SubscriptionsRaw } from '@rocket.chat/models'; import { Rooms, Subscriptions, Messages, Users } from '../../../models/server'; -import { Integrations, Uploads } from '../../../models/server/raw'; import { canAccessRoom, hasPermission, hasAtLeastOnePermission } from '../../../authorization/server'; import { mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; @@ -11,6 +11,7 @@ import { API } from '../api'; import { settings } from '../../../settings/server'; import { Team } from '../../../../server/sdk'; import { findUsersOfRoom } from '../../../../server/lib/findUsersOfRoom'; +import { addUserToFileObj } from '../helpers/addUserToFileObj'; // Returns the channel IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property function findChannelByIdOrName({ params, checkedArchived = true, userId }) { @@ -271,17 +272,11 @@ API.v1.addRoute( 'channels.files', { authRequired: true }, { - get() { + async get() { const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false, }); - const addUserObjectToEveryObject = (file) => { - if (file.userId) { - file = this.insertUserObject({ object: file, userId: file.userId }); - } - return file; - }; if (!canAccessRoom(findResult, { _id: this.userId })) { return API.v1.unauthorized(); @@ -292,20 +287,20 @@ API.v1.addRoute( const ourQuery = Object.assign({}, query, { rid: findResult._id }); - const files = Promise.await( - Uploads.find(ourQuery, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - fields, - }).toArray(), - ); + const { cursor, totalCount } = Uploads.findPaginated(ourQuery, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + projection: fields, + }); + + const [files, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ - files: files.map(addUserObjectToEveryObject), + files: await addUserToFileObj(files), count: files.length, offset, - total: Promise.await(Uploads.find(ourQuery).count()), + total, }); }, }, @@ -315,7 +310,7 @@ API.v1.addRoute( 'channels.getIntegrations', { authRequired: true }, { - get() { + async get() { if ( !hasAtLeastOnePermission(this.userId, [ 'manage-outgoing-integrations', @@ -351,15 +346,15 @@ API.v1.addRoute( const { sort, fields: projection, query } = this.parseJsonQuery(); ourQuery = Object.assign(mountIntegrationQueryBasedOnPermissions(this.userId), query, ourQuery); - const cursor = Integrations.find(ourQuery, { + + const { cursor, totalCount } = Integrations.findPaginated(ourQuery, { sort: sort || { _createdAt: 1 }, skip: offset, limit: count, projection, }); - const integrations = Promise.await(cursor.toArray()); - const total = Promise.await(cursor.count()); + const [integrations, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ integrations, @@ -413,66 +408,64 @@ API.v1.addRoute( 'channels.list', { authRequired: true }, { - get: { - // This is defined as such only to provide an example of how the routes can be defined :X - action() { - const { offset, count } = this.getPaginationItems(); - const { sort, fields, query } = this.parseJsonQuery(); - const hasPermissionToSeeAllPublicChannels = hasPermission(this.userId, 'view-c-room'); - - const ourQuery = { ...query, t: 'c' }; - - if (!hasPermissionToSeeAllPublicChannels) { - if (!hasPermission(this.userId, 'view-joined-room')) { - return API.v1.unauthorized(); - } - const roomIds = Subscriptions.findByUserIdAndType(this.userId, 'c', { - fields: { rid: 1 }, - }) - .fetch() - .map((s) => s.rid); - ourQuery._id = { $in: roomIds }; - } + async get() { + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + const hasPermissionToSeeAllPublicChannels = hasPermission(this.userId, 'view-c-room'); - // teams filter - I would love to have a way to apply this filter @ db level :( - const ids = Subscriptions.cachedFindByUserId(this.userId, { fields: { rid: 1 } }) + const ourQuery = { ...query, t: 'c' }; + + if (!hasPermissionToSeeAllPublicChannels) { + if (!hasPermission(this.userId, 'view-joined-room')) { + return API.v1.unauthorized(); + } + const roomIds = Subscriptions.findByUserIdAndType(this.userId, 'c', { + fields: { rid: 1 }, + }) .fetch() - .map((item) => item.rid); + .map((s) => s.rid); + ourQuery._id = { $in: roomIds }; + } + + // teams filter - I would love to have a way to apply this filter @ db level :( + const ids = Subscriptions.cachedFindByUserId(this.userId, { fields: { rid: 1 } }) + .fetch() + .map((item) => item.rid); - ourQuery.$or = [ - { - teamId: { - $exists: false, - }, + ourQuery.$or = [ + { + teamId: { + $exists: false, }, - { - teamId: { - $exists: true, - }, - _id: { - $in: ids, - }, + }, + { + teamId: { + $exists: true, }, - ]; - - const cursor = Rooms.find(ourQuery, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - fields, - }); + _id: { + $in: ids, + }, + }, + ]; - const total = cursor.count(); + const { cursor, totalCount } = RoomsRaw.findPaginated(ourQuery, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + projection: fields, + }); - const rooms = cursor.fetch(); + const [channels, total] = await Promise.all([ + cursor.map((room) => this.composeRoomWithLastMessage(room, this.userId)).toArray(), + totalCount, + ]); - return API.v1.success({ - channels: rooms.map((room) => this.composeRoomWithLastMessage(room, this.userId)), - count: rooms.length, - offset, - total, - }); - }, + return API.v1.success({ + channels, + count: channels.length, + offset, + total, + }); }, }, ); @@ -481,26 +474,34 @@ API.v1.addRoute( 'channels.list.joined', { authRequired: true }, { - get() { + async get() { const { offset, count } = this.getPaginationItems(); const { sort, fields } = this.parseJsonQuery(); - // TODO: CACHE: Add Breacking notice since we removed the query param - const cursor = Rooms.findBySubscriptionTypeAndUserId('c', this.userId, { + const subs = await SubscriptionsRaw.findByUserIdAndTypes(this.userId, ['c'], { projection: { rid: 1 } }).toArray(); + const rids = subs.map(({ rid }) => rid).filter(Boolean); + + if (rids.length === 0) { + return API.v1.notFound(); + } + + const { cursor, totalCount } = RoomsRaw.findPaginatedByTypeAndIds('c', rids, { sort: sort || { name: 1 }, skip: offset, limit: count, - fields, + projection: fields, }); - const totalCount = cursor.count(); - const rooms = cursor.fetch(); + const [channels, total] = await Promise.all([ + cursor.map((room) => this.composeRoomWithLastMessage(room, this.userId)).toArray(), + totalCount, + ]); return API.v1.success({ - channels: rooms.map((room) => this.composeRoomWithLastMessage(room, this.userId)), + channels, offset, - count: rooms.length, - total: totalCount, + count: channels.length, + total, }); }, }, @@ -510,7 +511,7 @@ API.v1.addRoute( 'channels.members', { authRequired: true }, { - get() { + async get() { const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false, @@ -532,7 +533,7 @@ API.v1.addRoute( ); const { status, filter } = this.queryParams; - const cursor = findUsersOfRoom({ + const { cursor, totalCount } = findUsersOfRoom({ rid: findResult._id, ...(status && { status: { $in: status } }), skip, @@ -541,8 +542,7 @@ API.v1.addRoute( ...(sort?.username && { sort: { username: sort.username } }), }); - const total = cursor.count(); - const members = cursor.fetch(); + const [members, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ members, @@ -917,7 +917,7 @@ API.v1.addRoute( 'channels.anonymousread', { authRequired: false }, { - get() { + async get() { const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false, @@ -933,15 +933,14 @@ API.v1.addRoute( }); } - const cursor = Messages.find(ourQuery, { + const { cursor, totalCount } = MessagesRaw.findPaginated(ourQuery, { sort: sort || { ts: -1 }, skip: offset, limit: count, - fields, + projection: fields, }); - const total = cursor.count(); - const messages = cursor.fetch(); + const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ messages: normalizeMessagesForUser(messages, this.userId), diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index e1ab4da8e8d9..fab7c8f5e362 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -18,9 +18,10 @@ import { isChannelsSetReadOnlyProps, isChannelsDeleteProps, } from '@rocket.chat/rest-typings'; +import { Messages } from '@rocket.chat/models'; -import { Rooms, Subscriptions, Messages } from '../../../models/server'; -import { hasPermission, hasAllPermission } from '../../../authorization/server'; +import { Rooms, Subscriptions } from '../../../models/server'; +import { hasPermission } from '../../../authorization/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { API } from '../api'; import { Team } from '../../../../server/sdk'; @@ -129,9 +130,9 @@ API.v1.addRoute( }, { get() { - const { roomId, unreads, oldest, latest, showThreadMessages, inclusive } = this.queryParams; + const { unreads, oldest, latest, showThreadMessages, inclusive, ...params } = this.queryParams; const findResult = findChannelByIdOrName({ - params: { roomId }, + params, checkedArchived: false, }); @@ -184,13 +185,13 @@ API.v1.addRoute( }, { post() { - const { roomId, joinCode } = this.bodyParams; - const findResult = findChannelByIdOrName({ params: { roomId } }); + const { joinCode, ...params } = this.bodyParams; + const findResult = findChannelByIdOrName({ params }); Meteor.call('joinRoom', findResult._id, joinCode); return API.v1.success({ - channel: findChannelByIdOrName({ params: { roomId }, userId: this.userId }), + channel: findChannelByIdOrName({ params, userId: this.userId }), }); }, }, @@ -204,15 +205,15 @@ API.v1.addRoute( }, { post() { - const { roomId /* userId */ } = this.bodyParams; - const findResult = findChannelByIdOrName({ params: { roomId } }); + const { ...params /* userId */ } = this.bodyParams; + const findResult = findChannelByIdOrName({ params }); const user = this.getUserFromParams(); Meteor.call('removeUserFromRoom', { rid: findResult._id, username: user.username }); return API.v1.success({ - channel: findChannelByIdOrName({ params: { roomId }, userId: this.userId }), + channel: findChannelByIdOrName({ params, userId: this.userId }), }); }, }, @@ -226,15 +227,15 @@ API.v1.addRoute( }, { post() { - const { roomId } = this.bodyParams; - const findResult = findChannelByIdOrName({ params: { roomId } }); + const { ...params } = this.bodyParams; + const findResult = findChannelByIdOrName({ params }); Meteor.runAsUser(this.userId, () => { Meteor.call('leaveRoom', findResult._id); }); return API.v1.success({ - channel: findChannelByIdOrName({ params: { roomId }, userId: this.userId }), + channel: findChannelByIdOrName({ params, userId: this.userId }), }); }, }, @@ -247,7 +248,7 @@ API.v1.addRoute( validateParams: isChannelsMessagesProps, }, { - get() { + async get() { const { roomId } = this.queryParams; const findResult = findChannelByIdOrName({ params: { roomId }, @@ -269,15 +270,15 @@ API.v1.addRoute( return API.v1.unauthorized(); } - const cursor = Messages.find(ourQuery, { + // @ts-expect-error recursive types are causing issues here + const { cursor, totalCount } = Messages.findPaginated(ourQuery, { sort: sort || { ts: -1 }, skip: offset, limit: count, - fields, + projection: fields, }); - const total = cursor.count(); - const messages = cursor.fetch(); + const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ messages: normalizeMessagesForUser(messages, this.userId), @@ -297,10 +298,10 @@ API.v1.addRoute( }, { post() { - const { roomId } = this.bodyParams; + const { ...params } = this.bodyParams; const findResult = findChannelByIdOrName({ - params: { roomId }, + params, checkedArchived: false, }); @@ -329,9 +330,7 @@ API.v1.addRoute( }, { post() { - const { roomId } = this.bodyParams; - - const findResult = findChannelByIdOrName({ params: { roomId } }); + const findResult = findChannelByIdOrName({ params: this.bodyParams }); if (findResult.ro === this.bodyParams.readOnly) { return API.v1.failure('The channel read only setting is the same as what it would be changed to.'); @@ -340,7 +339,7 @@ API.v1.addRoute( Meteor.call('saveRoomSettings', findResult._id, 'readOnly', this.bodyParams.readOnly); return API.v1.success({ - channel: findChannelByIdOrName({ params: { roomId }, userId: this.userId }), + channel: findChannelByIdOrName({ params: this.bodyParams, userId: this.userId }), }); }, }, @@ -354,9 +353,9 @@ API.v1.addRoute( }, { post() { - const { roomId, announcement } = this.bodyParams; + const { announcement, ...params } = this.bodyParams; - const findResult = findChannelByIdOrName({ params: { roomId } }); + const findResult = findChannelByIdOrName({ params }); Meteor.call('saveRoomSettings', findResult._id, 'roomAnnouncement', announcement); @@ -379,23 +378,19 @@ API.v1.addRoute( const { offset, count } = this.getPaginationItems(); const { sort } = this.parseJsonQuery(); - const mentions = Meteor.runAsUser(this.userId, () => - Meteor.call('getUserMentionsByChannel', { - roomId, - options: { - sort: sort || { ts: 1 }, - skip: offset, - limit: count, - }, - }), - ); - - const allMentions = Meteor.runAsUser(this.userId, () => - Meteor.call('getUserMentionsByChannel', { - roomId, - options: {}, - }), - ); + const mentions = Meteor.call('getUserMentionsByChannel', { + roomId, + options: { + sort: sort || { ts: 1 }, + skip: offset, + limit: count, + }, + }); + + const allMentions = Meteor.call('getUserMentionsByChannel', { + roomId, + options: {}, + }); return API.v1.success({ mentions, @@ -415,9 +410,9 @@ API.v1.addRoute( }, { get() { - const { roomId } = this.queryParams; + const { ...params } = this.queryParams; - const findResult = findChannelByIdOrName({ params: { roomId } }); + const findResult = findChannelByIdOrName({ params }); const moderators = Subscriptions.findByRoomIdAndRoles(findResult._id, ['moderator'], { fields: { u: 1 }, @@ -460,7 +455,7 @@ API.v1.addRoute( }, { async post() { - if (!hasAllPermission(this.userId, ['create-team', 'edit-room'])) { + if (!hasPermission(this.userId, 'create-team')) { return API.v1.unauthorized(); } @@ -470,6 +465,10 @@ API.v1.addRoute( return API.v1.failure('The parameter "channelId" or "channelName" is required'); } + if (!hasPermission(this.userId, 'edit-room', channelId)) { + return API.v1.unauthorized(); + } + const room = findChannelByIdOrName({ params: { roomId: channelId, diff --git a/apps/meteor/app/api/server/v1/chat.js b/apps/meteor/app/api/server/v1/chat.js index 76629a85349b..73f4b6931ddc 100644 --- a/apps/meteor/app/api/server/v1/chat.js +++ b/apps/meteor/app/api/server/v1/chat.js @@ -1,8 +1,9 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; +import { Messages as MessagesRaw } from '@rocket.chat/models'; -import { Messages } from '../../../models'; +import { Messages } from '../../../models/server'; import { canAccessRoom, canAccessRoomId, roomAccessAttributes, hasPermission } from '../../../authorization/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { processWebhookMessage } from '../../../lib/server'; @@ -446,7 +447,7 @@ API.v1.addRoute( 'chat.getDeletedMessages', { authRequired: true }, { - get() { + async get() { const { roomId, since } = this.queryParams; const { offset, count } = this.getPaginationItems(); @@ -459,19 +460,18 @@ API.v1.addRoute( } else if (isNaN(Date.parse(since))) { throw new Meteor.Error('The "since" query parameter must be a valid date.'); } - const cursor = Messages.trashFindDeletedAfter( + + const { cursor, totalCount } = MessagesRaw.trashFindPaginatedDeletedAfter( new Date(since), { rid: roomId }, { skip: offset, limit: count, - fields: { _id: 1 }, + projection: { _id: 1 }, }, ); - const total = cursor.count(); - - const messages = cursor.fetch(); + const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ messages, @@ -487,7 +487,7 @@ API.v1.addRoute( 'chat.getPinnedMessages', { authRequired: true }, { - get() { + async get() { const { roomId } = this.queryParams; const { offset, count } = this.getPaginationItems(); @@ -499,17 +499,15 @@ API.v1.addRoute( throw new Meteor.Error('error-not-allowed', 'Not allowed'); } - const cursor = Messages.findPinnedByRoom(roomId, { + const { cursor, totalCount } = MessagesRaw.findPaginatedPinnedByRoom(roomId, { skip: offset, limit: count, }); - const total = cursor.count(); - - const messages = cursor.fetch(); + const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ - messages, + messages: normalizeMessagesForUser(messages, this.userId), count: messages.length, offset, total, @@ -522,14 +520,15 @@ API.v1.addRoute( 'chat.getThreadsList', { authRequired: true }, { - get() { + async get() { const { rid, type, text } = this.queryParams; + check(rid, String); + check(type, Match.Maybe(String)); + check(text, Match.Maybe(String)); + const { offset, count } = this.getPaginationItems(); const { sort, fields, query } = this.parseJsonQuery(); - if (!rid) { - throw new Meteor.Error('The required "rid" query param is missing.'); - } if (!settings.get('Threads_enabled')) { throw new Meteor.Error('error-not-allowed', 'Threads Disabled'); } @@ -547,20 +546,18 @@ API.v1.addRoute( msg: new RegExp(escapeRegExp(text), 'i'), }; - const threadQuery = { ...query, ...typeThread, rid, tcount: { $exists: true } }; - const cursor = Messages.find(threadQuery, { + const threadQuery = { ...query, ...typeThread, rid: room._id, tcount: { $exists: true } }; + const { cursor, totalCount } = MessagesRaw.findPaginated(threadQuery, { sort: sort || { tlm: -1 }, skip: offset, limit: count, - fields, + projection: fields, }); - const total = cursor.count(); - - const threads = cursor.fetch(); + const [threads, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ - threads, + threads: normalizeMessagesForUser(threads, this.userId), count: threads.length, offset, total, @@ -613,7 +610,7 @@ API.v1.addRoute( 'chat.getThreadMessages', { authRequired: true }, { - get() { + async get() { const { tmid } = this.queryParams; const { query, fields, sort } = this.parseJsonQuery(); const { offset, count } = this.getPaginationItems(); @@ -634,19 +631,17 @@ API.v1.addRoute( if (!canAccessRoom(room, user)) { throw new Meteor.Error('error-not-allowed', 'Not Allowed'); } - const cursor = Messages.find( + const { cursor, totalCount } = MessagesRaw.findPaginated( { ...query, tmid }, { sort: sort || { ts: 1 }, skip: offset, limit: count, - fields, + projection: fields, }, ); - const total = cursor.count(); - - const messages = cursor.fetch(); + const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ messages, diff --git a/apps/meteor/app/api/server/v1/commands.js b/apps/meteor/app/api/server/v1/commands.js deleted file mode 100644 index 9a5cde50ef00..000000000000 --- a/apps/meteor/app/api/server/v1/commands.js +++ /dev/null @@ -1,346 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Random } from 'meteor/random'; -import objectPath from 'object-path'; - -import { slashCommands } from '../../../utils/server'; -import { Messages } from '../../../models/server'; -import { canAccessRoomId } from '../../../authorization/server'; -import { API } from '../api'; - -API.v1.addRoute( - 'commands.get', - { authRequired: true }, - { - get() { - const params = this.queryParams; - - if (typeof params.command !== 'string') { - return API.v1.failure('The query param "command" must be provided.'); - } - - const cmd = slashCommands.commands[params.command.toLowerCase()]; - - if (!cmd) { - return API.v1.failure(`There is no command in the system by the name of: ${params.command}`); - } - - return API.v1.success({ command: cmd }); - }, - }, -); - -// TODO: replace with something like client/lib/minimongo -const processQueryOptionsOnResult = (result, options = {}) => { - if (result === undefined || result === null) { - return undefined; - } - - if (Array.isArray(result)) { - if (options.sort) { - result = result.sort((a, b) => { - let r = 0; - for (const field in options.sort) { - if (options.sort.hasOwnProperty(field)) { - const direction = options.sort[field]; - let valueA; - let valueB; - if (field.indexOf('.') > -1) { - valueA = objectPath.get(a, field); - valueB = objectPath.get(b, field); - } else { - valueA = a[field]; - valueB = b[field]; - } - if (valueA > valueB) { - r = direction; - break; - } - if (valueA < valueB) { - r = -direction; - break; - } - } - } - return r; - }); - } - - if (typeof options.skip === 'number') { - result.splice(0, options.skip); - } - - if (typeof options.limit === 'number' && options.limit !== 0) { - result.splice(options.limit); - } - } - - if (!options.fields) { - options.fields = {}; - } - - const fieldsToRemove = []; - const fieldsToGet = []; - - 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 (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'); - } - - const pickFields = (obj, fields) => { - const picked = {}; - fields.forEach((field) => { - if (field.indexOf('.') !== -1) { - objectPath.set(picked, field, objectPath.get(obj, field)); - } else { - picked[field] = obj[field]; - } - }); - return picked; - }; - - 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); - } - - return null; - }); - } else { - if (fieldsToRemove.length > 0) { - return Object.fromEntries(Object.entries(result).filter(([key]) => !fieldsToRemove.includes(key))); - } - - if (fieldsToGet.length > 0) { - return pickFields(result, fieldsToGet); - } - } - } - - return result; -}; - -API.v1.addRoute( - 'commands.list', - { authRequired: true }, - { - get() { - const { offset, count } = this.getPaginationItems(); - const { sort, fields, query } = this.parseJsonQuery(); - - let commands = Object.values(slashCommands.commands); - - if (query && 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, - }); - - return API.v1.success({ - commands, - offset, - count: commands.length, - total: totalCount, - }); - }, - }, -); - -// Expects a body of: { command: 'gimme', params: 'any string value', roomId: 'value', triggerId: 'value' } -API.v1.addRoute( - 'commands.run', - { authRequired: true }, - { - 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.'); - } - - if (body.params && typeof body.params !== 'string') { - return API.v1.failure('The parameters for the command must be a single string.'); - } - - if (typeof body.roomId !== 'string') { - return API.v1.failure("The room's id where to execute this command must be provided and be a string."); - } - - if (body.tmid && typeof body.tmid !== 'string') { - return API.v1.failure('The tmid parameter when provided must be a string.'); - } - - const cmd = body.command.toLowerCase(); - if (!slashCommands.commands[cmd]) { - return API.v1.failure('The command provided does not exist (or is disabled).'); - } - - if (!canAccessRoomId(body.roomId, user._id)) { - return API.v1.unauthorized(); - } - - const params = body.params ? body.params : ''; - const message = { - _id: Random.id(), - rid: body.roomId, - msg: `/${cmd} ${params}`, - }; - - 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; - } - - const { triggerId } = body; - - const result = Meteor.runAsUser(user._id, () => slashCommands.run(cmd, params, message, triggerId)); - - return API.v1.success({ result }); - }, - }, -); - -API.v1.addRoute( - 'commands.preview', - { authRequired: true }, - { - // Expects these query params: command: 'giphy', params: 'mine', roomId: 'value' - get() { - const query = this.queryParams; - const user = this.getLoggedInUser(); - - if (typeof query.command !== 'string') { - return API.v1.failure('You must provide a command to get the previews from.'); - } - - if (query.params && typeof query.params !== 'string') { - return API.v1.failure('The parameters for the command must be a single string.'); - } - - if (typeof query.roomId !== 'string') { - return API.v1.failure("The room's id where the previews are being displayed must be provided and be a string."); - } - - const cmd = query.command.toLowerCase(); - if (!slashCommands.commands[cmd]) { - return API.v1.failure('The command provided does not exist (or is disabled).'); - } - - if (!canAccessRoomId(query.roomId, user._id)) { - return API.v1.unauthorized(); - } - - const params = query.params ? query.params : ''; - - let preview; - Meteor.runAsUser(user._id, () => { - 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.'); - } - - if (body.params && typeof body.params !== 'string') { - return API.v1.failure('The parameters for the command must be a single string.'); - } - - if (typeof body.roomId !== 'string') { - return API.v1.failure("The room's id where the preview is being executed in must be provided and be a string."); - } - - if (typeof body.previewItem === 'undefined') { - return API.v1.failure('The preview item being executed must be provided.'); - } - - if (!body.previewItem.id || !body.previewItem.type || typeof body.previewItem.value === 'undefined') { - return API.v1.failure('The preview item being executed is in the wrong format.'); - } - - if (body.tmid && typeof body.tmid !== 'string') { - return API.v1.failure('The tmid parameter when provided must be a string.'); - } - - if (body.triggerId && typeof body.triggerId !== 'string') { - return API.v1.failure('The triggerId parameter when provided must be a string.'); - } - - const cmd = body.command.toLowerCase(); - if (!slashCommands.commands[cmd]) { - return API.v1.failure('The command provided does not exist (or is disabled).'); - } - - if (!canAccessRoomId(body.roomId, user._id)) { - return API.v1.unauthorized(); - } - - const params = body.params ? body.params : ''; - const message = { - rid: body.roomId, - }; - - 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, - ); - }); - - return API.v1.success(); - }, - }, -); diff --git a/apps/meteor/app/api/server/v1/commands.ts b/apps/meteor/app/api/server/v1/commands.ts new file mode 100644 index 000000000000..e2659e17ac3d --- /dev/null +++ b/apps/meteor/app/api/server/v1/commands.ts @@ -0,0 +1,334 @@ +import { Meteor } from 'meteor/meteor'; +import { Random } from 'meteor/random'; +import objectPath from 'object-path'; + +import { slashCommands } from '../../../utils/server'; +import { Messages } from '../../../models/server'; +import { canAccessRoomId } from '../../../authorization/server'; +import { API } from '../api'; + +API.v1.addRoute( + 'commands.get', + { authRequired: true }, + { + get() { + const params = this.queryParams; + + if (typeof params.command !== 'string') { + return API.v1.failure('The query param "command" must be provided.'); + } + + const cmd = slashCommands.commands[params.command.toLowerCase()]; + + if (!cmd) { + return API.v1.failure(`There is no command in the system by the name of: ${params.command}`); + } + + return API.v1.success({ command: cmd }); + }, + }, +); + +/* @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 []; + } + + if (Array.isArray(result)) { + if (options.sort) { + result = result.sort((a, b) => { + let r = 0; + for (const field in options.sort) { + if (options.sort.hasOwnProperty(field)) { + const direction = options.sort[field]; + let valueA; + let valueB; + if (field.indexOf('.') > -1) { + valueA = objectPath.get(a, field); + valueB = objectPath.get(b, field); + } else { + valueA = a[field]; + valueB = b[field]; + } + if (valueA > valueB) { + r = direction; + break; + } + if (valueA < valueB) { + r = -direction; + break; + } + } + } + return r; + }); + } + + if (typeof options.skip === 'number') { + result.splice(0, options.skip); + } + + if (typeof options.limit === 'number' && options.limit !== 0) { + result.splice(options.limit); + } + } + + const fieldsToRemove: F[] = []; + const fieldsToGet: F[] = []; + + 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 (fieldsToGet.length > 0 && fieldsToGet.indexOf('_id' as F) === -1) { + fieldsToGet.push('_id' as F); + } + + 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 as Pick; + }; + + if (fieldsToRemove.length > 0 && fieldsToGet.length > 0) { + console.warn("Can't mix remove and get fields"); + fieldsToRemove.splice(0, fieldsToRemove.length); + } + + if (fieldsToRemove.length > 0 || fieldsToGet.length > 0) { + return result.map((record) => { + if (fieldsToRemove.length > 0) { + return Object.fromEntries(Object.entries(record).filter(([key]) => !fieldsToRemove.includes(key as F))) as Pick; + } + + return pickFields(record, fieldsToGet); + }); + } + + return result; +}; + +API.v1.addRoute( + 'commands.list', + { authRequired: true }, + { + get() { + const { offset, count } = this.getPaginationItems(); + const { sort, query } = this.parseJsonQuery(); + + let commands = Object.values(slashCommands.commands); + + if (query?.command) { + commands = commands.filter((command) => command.command === query.command); + } + + const totalCount = commands.length; + + return API.v1.success({ + commands: processQueryOptionsOnResult(commands, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + }), + offset, + count: commands.length, + total: totalCount, + }); + }, + }, +); + +// Expects a body of: { command: 'gimme', params: 'any string value', roomId: 'value', triggerId: 'value' } +API.v1.addRoute( + 'commands.run', + { authRequired: true }, + { + post() { + const body = this.bodyParams; + + if (typeof body.command !== 'string') { + return API.v1.failure('You must provide a command to run.'); + } + + if (body.params && typeof body.params !== 'string') { + return API.v1.failure('The parameters for the command must be a single string.'); + } + + if (typeof body.roomId !== 'string') { + return API.v1.failure("The room's id where to execute this command must be provided and be a string."); + } + + if (body.tmid && typeof body.tmid !== 'string') { + return API.v1.failure('The tmid parameter when provided must be a string.'); + } + + const cmd = body.command.toLowerCase(); + if (!slashCommands.commands[cmd]) { + return API.v1.failure('The command provided does not exist (or is disabled).'); + } + + if (!canAccessRoomId(body.roomId, this.userId)) { + return API.v1.unauthorized(); + } + + const params = body.params ? body.params : ''; + if (typeof body.tmid === 'string') { + const thread = Messages.findOneById(body.tmid); + if (!thread || thread.rid !== body.roomId) { + return API.v1.failure('Invalid thread.'); + } + } + + const message = { + _id: Random.id(), + rid: body.roomId, + msg: `/${cmd} ${params}`, + ...(body.tmid && { tmid: body.tmid }), + }; + + const { triggerId } = body; + + const result = slashCommands.run(cmd, params, message, triggerId); + + return API.v1.success({ result }); + }, + }, +); + +API.v1.addRoute( + 'commands.preview', + { authRequired: true }, + { + // Expects these query params: command: 'giphy', params: 'mine', roomId: 'value' + async get() { + const query = this.queryParams; + const user = this.getLoggedInUser(); + + if (typeof query.command !== 'string') { + return API.v1.failure('You must provide a command to get the previews from.'); + } + + if (query.params && typeof query.params !== 'string') { + return API.v1.failure('The parameters for the command must be a single string.'); + } + + if (typeof query.roomId !== 'string') { + return API.v1.failure("The room's id where the previews are being displayed must be provided and be a string."); + } + + const cmd = query.command.toLowerCase(); + if (!slashCommands.commands[cmd]) { + return API.v1.failure('The command provided does not exist (or is disabled).'); + } + + if (!canAccessRoomId(query.roomId, user._id)) { + return API.v1.unauthorized(); + } + + const params = query.params ? query.params : ''; + + 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; + + if (typeof body.command !== 'string') { + return API.v1.failure('You must provide a command to run the preview item on.'); + } + + if (body.params && typeof body.params !== 'string') { + return API.v1.failure('The parameters for the command must be a single string.'); + } + + if (typeof body.roomId !== 'string') { + return API.v1.failure("The room's id where the preview is being executed in must be provided and be a string."); + } + + if (typeof body.previewItem === 'undefined') { + return API.v1.failure('The preview item being executed must be provided.'); + } + + if (!body.previewItem.id || !body.previewItem.type || typeof body.previewItem.value === 'undefined') { + return API.v1.failure('The preview item being executed is in the wrong format.'); + } + + if (body.tmid && typeof body.tmid !== 'string') { + return API.v1.failure('The tmid parameter when provided must be a string.'); + } + + if (body.triggerId && typeof body.triggerId !== 'string') { + return API.v1.failure('The triggerId parameter when provided must be a string.'); + } + + const cmd = body.command.toLowerCase(); + if (!slashCommands.commands[cmd]) { + return API.v1.failure('The command provided does not exist (or is disabled).'); + } + + if (!canAccessRoomId(body.roomId, this.userId)) { + return API.v1.unauthorized(); + } + + const { params = '' } = body; + if (body.tmid) { + const thread = Messages.findOneById(body.tmid); + if (!thread || thread.rid !== body.roomId) { + return API.v1.failure('Invalid thread.'); + } + } + + 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..d68a1fa5b24f --- /dev/null +++ b/apps/meteor/app/api/server/v1/custom-sounds.ts @@ -0,0 +1,28 @@ +import { CustomSounds } from '@rocket.chat/models'; + +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, totalCount } = CustomSounds.findPaginated(query, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + }); + + const [sounds, total] = await Promise.all([cursor.toArray(), totalCount]); + + 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.js deleted file mode 100644 index b53583a4dc0b..000000000000 --- a/apps/meteor/app/api/server/v1/custom-user-status.js +++ /dev/null @@ -1,108 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; - -import { CustomUserStatus } from '../../../models'; -import { API } from '../api'; -import { findCustomUserStatus } from '../lib/custom-user-status'; - -API.v1.addRoute( - 'custom-user-status.list', - { authRequired: true }, - { - get() { - const { offset, count } = this.getPaginationItems(); - const { sort, query } = this.parseJsonQuery(); - - return API.v1.success( - Promise.await( - findCustomUserStatus({ - query, - pagination: { - offset, - count, - sort, - }, - }), - ), - ); - }, - }, -); - -API.v1.addRoute( - 'custom-user-status.create', - { authRequired: true }, - { - post() { - check(this.bodyParams, { - name: String, - statusType: Match.Maybe(String), - }); - - const userStatusData = { - name: this.bodyParams.name, - statusType: this.bodyParams.statusType, - }; - - Meteor.runAsUser(this.userId, () => { - Meteor.call('insertOrUpdateUserStatus', userStatusData); - }); - - return API.v1.success({ - customUserStatus: CustomUserStatus.findOneByName(userStatusData.name), - }); - }, - }, -); - -API.v1.addRoute( - 'custom-user-status.delete', - { authRequired: true }, - { - post() { - const { customUserStatusId } = this.bodyParams; - if (!customUserStatusId) { - return API.v1.failure('The "customUserStatusId" params is required!'); - } - - Meteor.runAsUser(this.userId, () => Meteor.call('deleteCustomUserStatus', customUserStatusId)); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'custom-user-status.update', - { authRequired: true }, - { - post() { - check(this.bodyParams, { - _id: String, - name: String, - statusType: Match.Maybe(String), - }); - - const userStatusData = { - _id: this.bodyParams._id, - name: this.bodyParams.name, - statusType: this.bodyParams.statusType, - }; - - const customUserStatus = CustomUserStatus.findOneById(userStatusData._id); - - // Ensure the message exists - if (!customUserStatus) { - return API.v1.failure(`No custom user status found with the id of "${userStatusData._id}".`); - } - - Meteor.runAsUser(this.userId, () => { - Meteor.call('insertOrUpdateUserStatus', userStatusData); - }); - - return API.v1.success({ - customUserStatus: CustomUserStatus.findOneById(userStatusData._id), - }); - }, - }, -); diff --git a/apps/meteor/app/api/server/v1/custom-user-status.ts b/apps/meteor/app/api/server/v1/custom-user-status.ts new file mode 100644 index 000000000000..0a2b086f5e2b --- /dev/null +++ b/apps/meteor/app/api/server/v1/custom-user-status.ts @@ -0,0 +1,116 @@ +import { Meteor } from 'meteor/meteor'; +import { Match, check } from 'meteor/check'; +import { CustomUserStatus } from '@rocket.chat/models'; + +import { API } from '../api'; + +API.v1.addRoute( + 'custom-user-status.list', + { authRequired: true }, + { + async get() { + const { offset, count } = this.getPaginationItems(); + const { sort, query } = this.parseJsonQuery(); + + const { cursor, totalCount } = CustomUserStatus.findPaginated(query, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + }); + + const [statuses, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + statuses, + count: statuses.length, + offset, + total, + }); + }, + }, +); + +API.v1.addRoute( + 'custom-user-status.create', + { authRequired: true }, + { + async post() { + check(this.bodyParams, { + name: String, + statusType: Match.Maybe(String), + }); + + const userStatusData = { + name: this.bodyParams.name, + statusType: this.bodyParams.statusType, + }; + + 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, + }); + }, + }, +); + +API.v1.addRoute( + 'custom-user-status.delete', + { authRequired: true }, + { + post() { + const { customUserStatusId } = this.bodyParams; + if (!customUserStatusId) { + return API.v1.failure('The "customUserStatusId" params is required!'); + } + + Meteor.call('deleteCustomUserStatus', customUserStatusId); + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'custom-user-status.update', + { authRequired: true }, + { + async post() { + check(this.bodyParams, { + _id: String, + name: String, + statusType: Match.Maybe(String), + }); + + const userStatusData = { + _id: this.bodyParams._id, + name: this.bodyParams.name, + statusType: this.bodyParams.statusType, + }; + + const customUserStatusToUpdate = await CustomUserStatus.findOneById(userStatusData._id); + + // Ensure the message exists + if (!customUserStatusToUpdate) { + return API.v1.failure(`No custom user status found with the id of "${userStatusData._id}".`); + } + + 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, + }); + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/e2e.js b/apps/meteor/app/api/server/v1/e2e.js deleted file mode 100644 index f079e75b8119..000000000000 --- a/apps/meteor/app/api/server/v1/e2e.js +++ /dev/null @@ -1,183 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { API } from '../api'; - -API.v1.addRoute( - 'e2e.fetchMyKeys', - { authRequired: true }, - { - get() { - let result; - Meteor.runAsUser(this.userId, () => { - result = Meteor.call('e2e.fetchMyKeys'); - }); - - return API.v1.success(result); - }, - }, -); - -API.v1.addRoute( - 'e2e.getUsersOfRoomWithoutKey', - { authRequired: true }, - { - get() { - const { rid } = this.queryParams; - - let result; - Meteor.runAsUser(this.userId, () => { - result = Meteor.call('e2e.getUsersOfRoomWithoutKey', rid); - }); - - return API.v1.success(result); - }, - }, -); - -/** - * @openapi - * /api/v1/e2e.setRoomKeyID: - * post: - * description: Sets the end-to-end encryption key ID for a room - * security: - * - autenticated: {} - * requestBody: - * description: A tuple containing the room ID and the key ID - * content: - * application/json: - * schema: - * type: object - * properties: - * rid: - * type: string - * keyID: - * type: string - * responses: - * 200: - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiSuccessV1' - * default: - * description: Unexpected error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiFailureV1' - */ -API.v1.addRoute( - 'e2e.setRoomKeyID', - { authRequired: true }, - { - post() { - const { rid, keyID } = this.bodyParams; - - Meteor.runAsUser(this.userId, () => { - API.v1.success(Meteor.call('e2e.setRoomKeyID', rid, keyID)); - }); - - return API.v1.success(); - }, - }, -); - -/** - * @openapi - * /api/v1/e2e.setUserPublicAndPrivateKeys: - * post: - * description: Sets the end-to-end encryption keys for the authenticated user - * security: - * - autenticated: {} - * requestBody: - * description: A tuple containing the public and the private keys - * content: - * application/json: - * schema: - * type: object - * properties: - * public_key: - * type: string - * private_key: - * type: string - * responses: - * 200: - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiSuccessV1' - * default: - * description: Unexpected error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiFailureV1' - */ -API.v1.addRoute( - 'e2e.setUserPublicAndPrivateKeys', - { authRequired: true }, - { - post() { - const { public_key, private_key } = this.bodyParams; - - Meteor.runAsUser(this.userId, () => { - API.v1.success( - Meteor.call('e2e.setUserPublicAndPrivateKeys', { - public_key, - private_key, - }), - ); - }); - - return API.v1.success(); - }, - }, -); - -/** - * @openapi - * /api/v1/e2e.updateGroupKey: - * post: - * description: Updates the end-to-end encryption key for a user on a room - * security: - * - autenticated: {} - * requestBody: - * description: A tuple containing the user ID, the room ID, and the key - * content: - * application/json: - * schema: - * type: object - * properties: - * uid: - * type: string - * rid: - * type: string - * key: - * type: string - * responses: - * 200: - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiSuccessV1' - * default: - * description: Unexpected error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiFailureV1' - */ -API.v1.addRoute( - 'e2e.updateGroupKey', - { authRequired: true }, - { - post() { - const { uid, rid, key } = this.bodyParams; - - Meteor.runAsUser(this.userId, () => { - API.v1.success(Meteor.call('e2e.updateGroupKey', rid, uid, key)); - }); - - return API.v1.success(); - }, - }, -); diff --git a/apps/meteor/app/api/server/v1/e2e.ts b/apps/meteor/app/api/server/v1/e2e.ts new file mode 100644 index 000000000000..13939b1caf0d --- /dev/null +++ b/apps/meteor/app/api/server/v1/e2e.ts @@ -0,0 +1,197 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { Meteor } from 'meteor/meteor'; +import { + ise2eGetUsersOfRoomWithoutKeyParamsGET, + ise2eSetRoomKeyIDParamsPOST, + ise2eSetUserPublicAndPrivateKeysParamsPOST, + ise2eUpdateGroupKeyParamsPOST, +} from '@rocket.chat/rest-typings'; +import { IUser } from '@rocket.chat/core-typings'; + +import { API } from '../api'; + +API.v1.addRoute( + 'e2e.fetchMyKeys', + { + authRequired: true, + }, + { + get() { + const result: { + public_key: string; + private_key: string; + } = Meteor.call('e2e.fetchMyKeys'); + + return API.v1.success(result); + }, + }, +); + +API.v1.addRoute( + 'e2e.getUsersOfRoomWithoutKey', + { + authRequired: true, + validateParams: ise2eGetUsersOfRoomWithoutKeyParamsGET, + }, + { + get() { + const { rid } = this.queryParams; + + const result: { + users: IUser[]; + } = Meteor.call('e2e.getUsersOfRoomWithoutKey', rid); + + return API.v1.success(result); + }, + }, +); + +/** + * @openapi + * /api/v1/e2e.setRoomKeyID: + * post: + * description: Sets the end-to-end encryption key ID for a room + * security: + * - autenticated: {} + * requestBody: + * description: A tuple containing the room ID and the key ID + * content: + * application/json: + * schema: + * type: object + * properties: + * rid: + * type: string + * keyID: + * type: string + * responses: + * 200: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiSuccessV1' + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ + +API.v1.addRoute( + 'e2e.setRoomKeyID', + { + authRequired: true, + validateParams: ise2eSetRoomKeyIDParamsPOST, + }, + { + post() { + const { rid, keyID } = this.bodyParams; + + Meteor.call('e2e.setRoomKeyID', rid, keyID); + + return API.v1.success(); + }, + }, +); + +/** + * @openapi + * /api/v1/e2e.setUserPublicAndPrivateKeys: + * post: + * description: Sets the end-to-end encryption keys for the authenticated user + * security: + * - autenticated: {} + * requestBody: + * description: A tuple containing the public and the private keys + * content: + * application/json: + * schema: + * type: object + * properties: + * public_key: + * type: string + * private_key: + * type: string + * responses: + * 200: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiSuccessV1' + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ +API.v1.addRoute( + 'e2e.setUserPublicAndPrivateKeys', + { + authRequired: true, + validateParams: ise2eSetUserPublicAndPrivateKeysParamsPOST, + }, + { + post() { + const { public_key, private_key } = Meteor.call('e2e.fetchMyKeys'); + + Meteor.call('e2e.setUserPublicAndPrivateKeys', { + public_key, + private_key, + }); + + return API.v1.success(); + }, + }, +); + +/** + * @openapi + * /api/v1/e2e.updateGroupKey: + * post: + * description: Updates the end-to-end encryption key for a user on a room + * security: + * - autenticated: {} + * requestBody: + * description: A tuple containing the user ID, the room ID, and the key + * content: + * application/json: + * schema: + * type: object + * properties: + * uid: + * type: string + * rid: + * type: string + * key: + * type: string + * responses: + * 200: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiSuccessV1' + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ +API.v1.addRoute( + 'e2e.updateGroupKey', + { + authRequired: true, + validateParams: ise2eUpdateGroupKeyParamsPOST, + }, + { + post() { + const { uid, rid, key } = this.bodyParams; + + Meteor.call('e2e.updateGroupKey', rid, uid, key); + + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/email-inbox.ts b/apps/meteor/app/api/server/v1/email-inbox.ts index 1e6392a8aad0..e94370acf3f3 100644 --- a/apps/meteor/app/api/server/v1/email-inbox.ts +++ b/apps/meteor/app/api/server/v1/email-inbox.ts @@ -1,9 +1,9 @@ import { check, Match } from 'meteor/check'; +import { EmailInbox } from '@rocket.chat/models'; import { API } from '../api'; import { insertOneEmailInbox, findEmailInboxes, findOneEmailInbox, updateEmailInbox } from '../lib/emailInbox'; import { hasPermission } from '../../../authorization/server/functions/hasPermission'; -import { EmailInbox } from '../../../models/server/raw'; import Users from '../../../models/server/models/Users'; import { sendTestEmailToInbox } from '../../../../server/features/EmailInbox/EmailInbox_Outgoing'; diff --git a/apps/meteor/app/api/server/v1/emoji-custom.js b/apps/meteor/app/api/server/v1/emoji-custom.js deleted file mode 100644 index 6a5fb2260e1b..000000000000 --- a/apps/meteor/app/api/server/v1/emoji-custom.js +++ /dev/null @@ -1,161 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { EmojiCustom } from '../../../models/server/raw'; -import { API } from '../api'; -import { getUploadFormData } from '../lib/getUploadFormData'; -import { findEmojisCustom } from '../lib/emoji-custom'; -import { Media } from '../../../../server/sdk'; - -API.v1.addRoute( - 'emoji-custom.list', - { authRequired: true }, - { - get() { - const { query } = this.parseJsonQuery(); - const { updatedSince } = this.queryParams; - let updatedSinceDate; - if (updatedSince) { - if (isNaN(Date.parse(updatedSince))) { - throw new Meteor.Error('error-roomId-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); - } else { - updatedSinceDate = new Date(updatedSince); - } - return API.v1.success({ - emojis: { - update: Promise.await(EmojiCustom.find({ ...query, _updatedAt: { $gt: updatedSinceDate } }).toArray()), - remove: Promise.await(EmojiCustom.trashFindDeletedAfter(updatedSinceDate).toArray()), - }, - }); - } - - return API.v1.success({ - emojis: { - update: Promise.await(EmojiCustom.find(query).toArray()), - remove: [], - }, - }); - }, - }, -); - -API.v1.addRoute( - 'emoji-custom.all', - { authRequired: true }, - { - get() { - const { offset, count } = this.getPaginationItems(); - const { sort, query } = this.parseJsonQuery(); - - return API.v1.success( - Promise.await( - findEmojisCustom({ - query, - pagination: { - offset, - count, - sort, - }, - }), - ), - ); - }, - }, -); - -API.v1.addRoute( - 'emoji-custom.create', - { authRequired: true }, - { - post() { - const { emoji, ...fields } = Promise.await( - getUploadFormData({ - request: this.request, - }), - ); - - if (!emoji) { - throw new Meteor.Error('invalid-field'); - } - - const isUploadable = Promise.await(Media.isImage(emoji.fileBuffer)); - if (!isUploadable) { - throw new Meteor.Error('emoji-is-not-image', "Emoji file provided cannot be uploaded since it's not an image"); - } - - const [, extension] = emoji.mimetype.split('/'); - fields.extension = extension; - - fields.newFile = true; - fields.aliases = fields.aliases || ''; - - Meteor.runAsUser(this.userId, () => { - Meteor.call('insertOrUpdateEmoji', fields); - Meteor.call('uploadEmojiCustom', emoji.fileBuffer, emoji.mimetype, fields); - }); - }, - }, -); - -API.v1.addRoute( - 'emoji-custom.update', - { authRequired: true }, - { - post() { - const { emoji, ...fields } = Promise.await( - getUploadFormData({ - request: this.request, - }), - ); - - if (!fields._id) { - throw new Meteor.Error('The required "_id" query param is missing.'); - } - - const emojiToUpdate = Promise.await(EmojiCustom.findOneById(fields._id)); - if (!emojiToUpdate) { - throw new Meteor.Error('Emoji not found.'); - } - - fields.previousName = emojiToUpdate.name; - fields.previousExtension = emojiToUpdate.extension; - fields.aliases = fields.aliases || ''; - fields.newFile = Boolean(emoji?.fileBuffer.length); - - if (fields.newFile) { - const isUploadable = Promise.await(Media.isImage(emoji.fileBuffer)); - if (!isUploadable) { - throw new Meteor.Error('emoji-is-not-image', "Emoji file provided cannot be uploaded since it's not an image"); - } - - const [, extension] = emoji.mimetype.split('/'); - fields.extension = extension; - } else { - fields.extension = emojiToUpdate.extension; - } - - Meteor.runAsUser(this.userId, () => { - Meteor.call('insertOrUpdateEmoji', fields); - if (fields.newFile) { - Meteor.call('uploadEmojiCustom', emoji.fileBuffer, emoji.mimetype, fields); - } - }); - }, - }, -); - -API.v1.addRoute( - 'emoji-custom.delete', - { authRequired: true }, - { - post() { - const { emojiId } = this.bodyParams; - if (!emojiId) { - return API.v1.failure('The "emojiId" params is required!'); - } - - Meteor.runAsUser(this.userId, () => Meteor.call('deleteEmojiCustom', emojiId)); - - return API.v1.success(); - }, - }, -); diff --git a/apps/meteor/app/api/server/v1/emoji-custom.ts b/apps/meteor/app/api/server/v1/emoji-custom.ts new file mode 100644 index 000000000000..e0027765352e --- /dev/null +++ b/apps/meteor/app/api/server/v1/emoji-custom.ts @@ -0,0 +1,169 @@ +import { Meteor } from 'meteor/meteor'; +import { EmojiCustom } from '@rocket.chat/models'; + +import { API } from '../api'; +import { getUploadFormData } from '../lib/getUploadFormData'; +import { findEmojisCustom } from '../lib/emoji-custom'; +import { Media } from '../../../../server/sdk'; +import { SystemLogger } from '../../../../server/lib/logger/system'; + +API.v1.addRoute( + 'emoji-custom.list', + { authRequired: true }, + { + async get() { + const { query } = this.parseJsonQuery(); + const { updatedSince } = this.queryParams; + if (updatedSince) { + const updatedSinceDate = new Date(updatedSince); + if (isNaN(Date.parse(updatedSince))) { + throw new Meteor.Error('error-roomId-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); + } + const [update, remove] = await Promise.all([ + EmojiCustom.find({ ...query, _updatedAt: { $gt: updatedSinceDate } }).toArray(), + EmojiCustom.trashFindDeletedAfter(updatedSinceDate).toArray(), + ]); + return API.v1.success({ + emojis: { + update, + remove, + }, + }); + } + + return API.v1.success({ + emojis: { + update: await EmojiCustom.find(query).toArray(), + remove: [], + }, + }); + }, + }, +); + +API.v1.addRoute( + 'emoji-custom.all', + { authRequired: true }, + { + async get() { + const { offset, count } = this.getPaginationItems(); + const { sort, query } = this.parseJsonQuery(); + + return API.v1.success( + await findEmojisCustom({ + query, + pagination: { + offset, + count, + sort, + }, + }), + ); + }, + }, +); + +API.v1.addRoute( + 'emoji-custom.create', + { authRequired: true }, + { + async post() { + const [emoji, fields] = await getUploadFormData( + { + request: this.request, + }, + { field: 'emoji' }, + ); + + const isUploadable = await Media.isImage(emoji.fileBuffer); + if (!isUploadable) { + throw new Meteor.Error('emoji-is-not-image', "Emoji file provided cannot be uploaded since it's not an image"); + } + + const [, extension] = emoji.mimetype.split('/'); + fields.extension = extension; + + try { + Meteor.call('insertOrUpdateEmoji', { + ...fields, + newFile: true, + aliases: fields.aliases || '', + }); + Meteor.call('uploadEmojiCustom', emoji.fileBuffer, emoji.mimetype, { + ...fields, + newFile: true, + aliases: fields.aliases || '', + }); + } catch (e) { + SystemLogger.error(e); + return API.v1.failure(); + } + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'emoji-custom.update', + { authRequired: true }, + { + async post() { + const [emoji, fields] = await getUploadFormData( + { + request: this.request, + }, + { field: 'emoji' }, + ); + + if (!fields._id) { + throw new Meteor.Error('The required "_id" query param is missing.'); + } + + const emojiToUpdate = await EmojiCustom.findOneById(fields._id); + if (!emojiToUpdate) { + throw new Meteor.Error('Emoji not found.'); + } + + fields.previousName = emojiToUpdate.name; + fields.previousExtension = emojiToUpdate.extension; + fields.aliases = fields.aliases || ''; + const newFile = Boolean(emoji?.fileBuffer.length); + + if (fields.newFile) { + const isUploadable = await Media.isImage(emoji.fileBuffer); + if (!isUploadable) { + throw new Meteor.Error('emoji-is-not-image', "Emoji file provided cannot be uploaded since it's not an image"); + } + + const [, extension] = emoji.mimetype.split('/'); + fields.extension = extension; + } else { + fields.extension = emojiToUpdate.extension; + } + + Meteor.call('insertOrUpdateEmoji', { ...fields, newFile }); + if (fields.newFile) { + Meteor.call('uploadEmojiCustom', emoji.fileBuffer, emoji.mimetype, { ...fields, newFile }); + } + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'emoji-custom.delete', + { authRequired: true }, + { + post() { + const { emojiId } = this.bodyParams; + if (!emojiId) { + return API.v1.failure('The "emojiId" params is required!'); + } + + Meteor.call('deleteEmojiCustom', emojiId); + + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/groups.js b/apps/meteor/app/api/server/v1/groups.js index 4a715ad65d2c..94a690be732a 100644 --- a/apps/meteor/app/api/server/v1/groups.js +++ b/apps/meteor/app/api/server/v1/groups.js @@ -1,10 +1,10 @@ import _ from 'underscore'; import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; +import { Integrations, Messages as MessagesRaw, Uploads, Rooms as RoomsRaw, Subscriptions as SubscriptionsRaw } from '@rocket.chat/models'; import { mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; import { Subscriptions, Rooms, Messages, Users } from '../../../models/server'; -import { Integrations, Uploads } from '../../../models/server/raw'; import { hasPermission, hasAtLeastOnePermission, @@ -16,6 +16,7 @@ import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMes import { API } from '../api'; import { Team } from '../../../../server/sdk'; import { findUsersOfRoom } from '../../../../server/lib/findUsersOfRoom'; +import { addUserToFileObj } from '../helpers/addUserToFileObj'; // Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property export function findPrivateGroupByIdOrName({ params, userId, checkedArchived = true }) { @@ -333,38 +334,32 @@ API.v1.addRoute( 'groups.files', { authRequired: true }, { - get() { + async get() { const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false, }); - const addUserObjectToEveryObject = (file) => { - if (file.userId) { - file = this.insertUserObject({ object: file, userId: file.userId }); - } - return file; - }; const { offset, count } = this.getPaginationItems(); const { sort, fields, query } = this.parseJsonQuery(); const ourQuery = Object.assign({}, query, { rid: findResult.rid }); - const files = Promise.await( - Uploads.find(ourQuery, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - fields, - }).toArray(), - ); + const { cursor, totalCount } = Uploads.findPaginated(ourQuery, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + projection: fields, + }); + + const [files, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ - files: files.map(addUserObjectToEveryObject), + files: await addUserToFileObj(files), count: files.length, offset, - total: Promise.await(Uploads.find(ourQuery).count()), + total, }); }, }, @@ -374,7 +369,7 @@ API.v1.addRoute( 'groups.getIntegrations', { authRequired: true }, { - get() { + async get() { if ( !hasAtLeastOnePermission(this.userId, [ 'manage-outgoing-integrations', @@ -408,15 +403,14 @@ API.v1.addRoute( const ourQuery = Object.assign(mountIntegrationQueryBasedOnPermissions(this.userId), query, { channel: { $in: channelsToSearch }, }); - const cursor = Integrations.find(ourQuery, { + const { cursor, totalCount } = Integrations.findPaginated(ourQuery, { sort: sort || { _createdAt: 1 }, skip: offset, limit: count, projection, }); - const integrations = Promise.await(cursor.toArray()); - const total = Promise.await(cursor.count()); + const [integrations, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ integrations, @@ -580,26 +574,34 @@ API.v1.addRoute( 'groups.list', { authRequired: true }, { - get() { + async get() { const { offset, count } = this.getPaginationItems(); const { sort, fields } = this.parseJsonQuery(); - // TODO: CACHE: Add Breacking notice since we removed the query param - const cursor = Rooms.findBySubscriptionTypeAndUserId('p', this.userId, { + const subs = await SubscriptionsRaw.findByUserIdAndTypes(this.userId, ['p'], { projection: { rid: 1 } }).toArray(); + const rids = subs.map(({ rid }) => rid).filter(Boolean); + + if (rids.length === 0) { + return API.v1.notFound(); + } + + const { cursor, totalCount } = RoomsRaw.findPaginatedByTypeAndIds('p', rids, { sort: sort || { name: 1 }, skip: offset, limit: count, - fields, + projection: fields, }); - const totalCount = cursor.count(); - const rooms = cursor.fetch(); + const [groups, total] = await Promise.all([ + cursor.map((room) => this.composeRoomWithLastMessage(room, this.userId)).toArray(), + totalCount, + ]); return API.v1.success({ - groups: rooms.map((room) => this.composeRoomWithLastMessage(room, this.userId)), + groups, offset, - count: rooms.length, - total: totalCount, + count: groups.length, + total, }); }, }, @@ -609,7 +611,7 @@ API.v1.addRoute( 'groups.listAll', { authRequired: true }, { - get() { + async get() { if (!hasPermission(this.userId, 'view-room-administration')) { return API.v1.unauthorized(); } @@ -617,21 +619,23 @@ API.v1.addRoute( const { sort, fields, query } = this.parseJsonQuery(); const ourQuery = Object.assign({}, query, { t: 'p' }); - const cursor = Rooms.find(ourQuery, { + const { cursor, totalCount } = RoomsRaw.findPaginated(ourQuery, { sort: sort || { name: 1 }, skip: offset, limit: count, - fields, + projection: fields, }); - const totalCount = cursor.count(); - const rooms = cursor.fetch(); + const [rooms, total] = await Promise.all([ + cursor.map((room) => this.composeRoomWithLastMessage(room, this.userId)).toArray(), + totalCount, + ]); return API.v1.success({ - groups: rooms.map((room) => this.composeRoomWithLastMessage(room, this.userId)), + groups: rooms, offset, count: rooms.length, - total: totalCount, + total, }); }, }, @@ -641,7 +645,7 @@ API.v1.addRoute( 'groups.members', { authRequired: true }, { - get() { + async get() { const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, @@ -663,7 +667,7 @@ API.v1.addRoute( ); const { status, filter } = this.queryParams; - const cursor = findUsersOfRoom({ + const { cursor, totalCount } = findUsersOfRoom({ rid: findResult.rid, ...(status && { status: { $in: status } }), skip, @@ -672,8 +676,7 @@ API.v1.addRoute( ...(sort?.username && { sort: { username: sort.username } }), }); - const total = cursor.count(); - const members = cursor.fetch(); + const [members, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ members, @@ -689,7 +692,7 @@ API.v1.addRoute( 'groups.messages', { authRequired: true }, { - get() { + async get() { const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, @@ -699,18 +702,20 @@ API.v1.addRoute( const ourQuery = Object.assign({}, query, { rid: findResult.rid }); - const messages = Messages.find(ourQuery, { + const { cursor, totalCount } = MessagesRaw.findPaginated(ourQuery, { sort: sort || { ts: -1 }, skip: offset, limit: count, - fields, - }).fetch(); + projection: fields, + }); + + const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ messages: normalizeMessagesForUser(messages, this.userId), count: messages.length, offset, - total: Messages.find(ourQuery).count(), + total, }); }, }, diff --git a/apps/meteor/app/api/server/v1/im.ts b/apps/meteor/app/api/server/v1/im.ts index 76542f1e5582..a749a2da9b14 100644 --- a/apps/meteor/app/api/server/v1/im.ts +++ b/apps/meteor/app/api/server/v1/im.ts @@ -1,19 +1,27 @@ /** * 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 type { IMessage, IRoom, ISubscription, IUpload } from '@rocket.chat/core-typings'; +import { + isDmDeleteProps, + isDmFileProps, + isDmMemberProps, + isDmMessagesProps, + isDmCreateProps, + isDmHistoryProps, +} from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; +import { Subscriptions, Uploads, Messages, Rooms, Users } from '@rocket.chat/models'; -import { Users } from '../../../models/server'; -import { Subscriptions, Uploads, Messages, Rooms, Settings } from '../../../models/server/raw'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { hasPermission } from '../../../authorization/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { API } from '../api'; import { getRoomByNameOrIdWithOptionToJoin } from '../../../lib/server/functions/getRoomByNameOrIdWithOptionToJoin'; import { createDirectMessage } from '../../../../server/methods/createDirectMessage'; +import { addUserToFileObj } from '../helpers/addUserToFileObj'; +import { settings } from '../../../settings/server'; interface IImFilesObject extends IUpload { userId: string; @@ -210,26 +218,17 @@ API.v1.addRoute( const ourQuery = query ? { rid: room._id, ...query } : { rid: room._id }; - const files = ( - await Uploads.find(ourQuery, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - projection: fields, - }).toArray() - ).map((file): IImFilesObject | (IImFilesObject & { user: Pick }) => { - if (file.userId) { - return this.insertUserObject }>({ - object: { ...file }, - userId: file.userId, - }); - } - return file; + const { cursor, totalCount } = Uploads.findPaginated(ourQuery, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + projection: fields, }); - const total = await Uploads.find(ourQuery).count(); + const [files, total] = await Promise.all([cursor.toArray(), totalCount]); + return API.v1.success({ - files, + files: await addUserToFileObj(files), count: files.length, offset, total, @@ -240,7 +239,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(); @@ -307,15 +306,16 @@ API.v1.addRoute( const options = { sort: { username: sort?.username ? sort.username : 1 }, - projection: { _id: 1, username: 1, name: 1, status: 1, statusText: 1, utcOffset: 1 }, + projection: { _id: 1, username: 1, name: 1, status: 1, statusText: 1, utcOffset: 1, federated: 1 }, skip: offset, limit: count, }; - const cursor = Users.findByActiveUsersExcept(filter, [], options, null, [extraQuery]); + const searchFields = settings.get('Accounts_SearchFields').trim().split(','); + + const { cursor, totalCount } = Users.findPaginatedByActiveUsersExcept(filter, [], options, searchFields, [extraQuery]); - const members = cursor.fetch(); - const total = cursor.count(); + const [members, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ members, @@ -347,18 +347,21 @@ API.v1.addRoute( const ourQuery = { rid: room._id, ...query }; const sortObj = { ts: sort?.ts ?? -1 }; - const messages = await Messages.find(ourQuery, { + + const { cursor, totalCount } = Messages.findPaginated(ourQuery, { sort: sortObj, skip: offset, limit: count, - ...(fields && { fields }), - }).toArray(); + ...(fields && { projection: fields }), + }); + + const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ messages: normalizeMessagesForUser(messages, this.userId), count: messages.length, offset, - total: await Messages.find(ourQuery).count(), + total, }); }, }, @@ -369,13 +372,7 @@ API.v1.addRoute( { authRequired: true }, { async get() { - const settings = await Settings.findOne( - { _id: 'API_Enable_Direct_Message_History_EndPoint' }, - { - projection: { _id: 1, value: 1 }, - }, - ); - if (settings?.value !== true) { + if (settings.get('API_Enable_Direct_Message_History_EndPoint') !== true) { throw new Meteor.Error('error-endpoint-disabled', 'This endpoint is disabled', { route: '/api/v1/im.messages.others', }); @@ -390,7 +387,7 @@ API.v1.addRoute( throw new Meteor.Error('error-roomid-param-not-provided', 'The parameter "roomId" is required'); } - const room = await Rooms.findOneById(roomId, { projection: { _id: 1, t: 1 } }); + const room = await Rooms.findOneById>(roomId, { projection: { _id: 1, t: 1 } }); if (!room || room?.t !== 'd') { throw new Meteor.Error('error-room-not-found', `No direct message room found by the id of: ${roomId}`); } @@ -399,17 +396,18 @@ API.v1.addRoute( const { sort, fields, query } = this.parseJsonQuery(); const ourQuery = Object.assign({}, query, { rid: room._id }); - const msgs = await Messages.find(ourQuery, { + const { cursor, totalCount } = Messages.findPaginated(ourQuery, { sort: sort || { ts: -1 }, skip: offset, limit: count, - fields, - }).toArray(); + projection: fields, + }); + + const [msgs, total] = await Promise.all([cursor.toArray(), totalCount]); if (!msgs) { throw new Meteor.Error('error-no-messages', 'No messages found'); } - const total = await Messages.find(ourQuery).count(); return API.v1.success({ messages: normalizeMessagesForUser(msgs, this.userId), @@ -435,7 +433,7 @@ API.v1.addRoute( .map((item) => item.rid) .toArray(); - const rooms = Rooms.find( + const { cursor, totalCount } = Rooms.findPaginated( { type: 'd', _id: { $in: subscriptions } }, { sort, @@ -443,10 +441,12 @@ API.v1.addRoute( limit: count, projection: fields, }, - ).map((room: IRoom) => this.composeRoomWithLastMessage(room, this.userId)); + ); - const total = await rooms.count(); - const ims = await rooms.toArray(); + const [ims, total] = await Promise.all([ + cursor.map((room: IRoom) => this.composeRoomWithLastMessage(room, this.userId)).toArray(), + totalCount, + ]); return API.v1.success({ ims, @@ -470,22 +470,26 @@ API.v1.addRoute( const { offset, count }: { offset: number; count: number } = this.getPaginationItems(); const { sort, fields, query } = this.parseJsonQuery(); - const ourQuery = { ...query, t: 'd' }; + const { cursor, totalCount } = Rooms.findPaginated( + { ...query, t: 'd' }, + { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + projection: fields, + }, + ); - const rooms = await Rooms.find(ourQuery, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - projection: fields, - }) - .map((room: IRoom) => this.composeRoomWithLastMessage(room, this.userId)) - .toArray(); + const [rooms, total] = await Promise.all([ + cursor.map((room: IRoom) => this.composeRoomWithLastMessage(room, this.userId)).toArray(), + totalCount, + ]); return API.v1.success({ ims: rooms, offset, count: rooms.length, - total: await Rooms.find(ourQuery).count(), + total, }); }, }, diff --git a/apps/meteor/app/api/server/v1/import.js b/apps/meteor/app/api/server/v1/import.js deleted file mode 100644 index b681b480d9d5..000000000000 --- a/apps/meteor/app/api/server/v1/import.js +++ /dev/null @@ -1,185 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { API } from '../api'; -import { hasPermission } from '../../../authorization/server'; -import { Imports } from '../../../models/server'; -import { Importers } from '../../../importer/server'; - -API.v1.addRoute( - 'uploadImportFile', - { authRequired: true }, - { - post() { - const { binaryContent, contentType, fileName, importerKey } = this.bodyParams; - - return API.v1.success(Meteor.call('uploadImportFile', binaryContent, contentType, fileName, importerKey)); - }, - }, -); - -API.v1.addRoute( - 'downloadPublicImportFile', - { authRequired: true }, - { - post() { - const { fileUrl, importerKey } = this.bodyParams; - - Meteor.runAsUser(this.userId, () => { - API.v1.success(Meteor.call('downloadPublicImportFile', fileUrl, importerKey)); - }); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'startImport', - { authRequired: true }, - { - post() { - const { input } = this.bodyParams; - - Meteor.runAsUser(this.userId, () => { - API.v1.success(Meteor.call('startImport', input)); - }); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'getImportFileData', - { authRequired: true }, - { - get() { - let result; - Meteor.runAsUser(this.userId, () => { - result = Meteor.call('getImportFileData'); - }); - - return API.v1.success(result); - }, - }, -); - -API.v1.addRoute( - 'getImportProgress', - { authRequired: true }, - { - get() { - let result; - Meteor.runAsUser(this.userId, () => { - result = Meteor.call('getImportProgress'); - }); - - return API.v1.success(result); - }, - }, -); - -API.v1.addRoute( - 'getLatestImportOperations', - { authRequired: true }, - { - get() { - let result; - Meteor.runAsUser(this.userId, () => { - result = Meteor.call('getLatestImportOperations'); - }); - - return API.v1.success(result); - }, - }, -); - -API.v1.addRoute( - 'downloadPendingFiles', - { authRequired: true }, - { - post() { - if (!this.userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'downloadPendingFiles', - }); - } - - if (!hasPermission(this.userId, 'run-import')) { - throw new Meteor.Error('not_authorized'); - } - - const importer = Importers.get('pending-files'); - if (!importer) { - throw new Meteor.Error('error-importer-not-defined', 'The Pending File Importer was not found.', { - method: 'downloadPendingFiles', - }); - } - - importer.instance = new importer.importer(importer); // eslint-disable-line new-cap - const count = importer.instance.prepareFileCount(); - - return API.v1.success({ - success: true, - count, - }); - }, - }, -); - -API.v1.addRoute( - 'downloadPendingAvatars', - { authRequired: true }, - { - post() { - if (!this.userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'downloadPendingAvatars', - }); - } - - if (!hasPermission(this.userId, 'run-import')) { - throw new Meteor.Error('not_authorized'); - } - - const importer = Importers.get('pending-avatars'); - if (!importer) { - throw new Meteor.Error('error-importer-not-defined', 'The Pending File Importer was not found.', { - method: 'downloadPendingAvatars', - }); - } - - importer.instance = new importer.importer(importer); // eslint-disable-line new-cap - const count = importer.instance.prepareFileCount(); - - return API.v1.success({ - success: true, - count, - }); - }, - }, -); - -API.v1.addRoute( - 'getCurrentImportOperation', - { authRequired: true }, - { - get() { - if (!this.userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'getCurrentImportOperation', - }); - } - - if (!hasPermission(this.userId, 'run-import')) { - throw new Meteor.Error('not_authorized'); - } - - const operation = Imports.findLastImport(); - return API.v1.success({ - success: true, - operation, - }); - }, - }, -); diff --git a/apps/meteor/app/api/server/v1/import.ts b/apps/meteor/app/api/server/v1/import.ts new file mode 100644 index 000000000000..8245f5edfcb2 --- /dev/null +++ b/apps/meteor/app/api/server/v1/import.ts @@ -0,0 +1,178 @@ +import { Meteor } from 'meteor/meteor'; +import { + isUploadImportFileParamsPOST, + isDownloadPublicImportFileParamsPOST, + isStartImportParamsPOST, + isGetImportFileDataParamsGET, + isGetImportProgressParamsGET, + isGetLatestImportOperationsParamsGET, + isDownloadPendingFilesParamsPOST, + isDownloadPendingAvatarsParamsPOST, + isGetCurrentImportOperationParamsGET, +} from '@rocket.chat/rest-typings'; + +import { API } from '../api'; +import { Imports } from '../../../models/server'; +import { Importers } from '../../../importer/server'; +import { executeUploadImportFile } from '../../../importer/server/methods/uploadImportFile'; + +API.v1.addRoute( + 'uploadImportFile', + { + authRequired: true, + validateParams: isUploadImportFileParamsPOST, + }, + { + post() { + const { binaryContent, contentType, fileName, importerKey } = this.bodyParams; + + return API.v1.success(executeUploadImportFile(this.userId, binaryContent, contentType, fileName, importerKey)); + }, + }, +); + +API.v1.addRoute( + 'downloadPublicImportFile', + { + authRequired: true, + validateParams: isDownloadPublicImportFileParamsPOST, + }, + { + post() { + const { fileUrl, importerKey } = this.bodyParams; + + Meteor.call('downloadPublicImportFile', fileUrl, importerKey); + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'startImport', + { + authRequired: true, + validateParams: isStartImportParamsPOST, + }, + { + post() { + const { input } = this.bodyParams; + + Meteor.call('startImport', input); + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'getImportFileData', + { + authRequired: true, + validateParams: isGetImportFileDataParamsGET, + }, + { + get() { + const result = Meteor.call('getImportFileData'); + return API.v1.success(result); + }, + }, +); + +API.v1.addRoute( + 'getImportProgress', + { + authRequired: true, + validateParams: isGetImportProgressParamsGET, + }, + { + get() { + const result = Meteor.call('getImportProgress'); + return API.v1.success(result); + }, + }, +); + +API.v1.addRoute( + 'getLatestImportOperations', + { + authRequired: true, + validateParams: isGetLatestImportOperationsParamsGET, + }, + { + get() { + const result = Meteor.call('getLatestImportOperations'); + return API.v1.success(result); + }, + }, +); + +API.v1.addRoute( + 'downloadPendingFiles', + { + authRequired: true, + validateParams: isDownloadPendingFilesParamsPOST, + permissionsRequired: ['run-import'], + }, + { + post() { + const importer = Importers.get('pending-files'); + if (!importer) { + throw new Meteor.Error('error-importer-not-defined', 'The Pending File Importer was not found.', { + method: 'downloadPendingFiles', + }); + } + + importer.instance = new importer.importer(importer); // eslint-disable-line new-cap + const count = importer.instance.prepareFileCount(); + + return API.v1.success({ + count, + }); + }, + }, +); + +API.v1.addRoute( + 'downloadPendingAvatars', + { + authRequired: true, + validateParams: isDownloadPendingAvatarsParamsPOST, + permissionsRequired: ['run-import'], + }, + { + post() { + const importer = Importers.get('pending-avatars'); + if (!importer) { + throw new Meteor.Error('error-importer-not-defined', 'The Pending File Importer was not found.', { + method: 'downloadPendingAvatars', + }); + } + + importer.instance = new importer.importer(importer); // eslint-disable-line new-cap + const count = importer.instance.prepareFileCount(); + + return API.v1.success({ + count, + }); + }, + }, +); + +API.v1.addRoute( + 'getCurrentImportOperation', + { + authRequired: true, + validateParams: isGetCurrentImportOperationParamsGET, + permissionsRequired: ['run-import'], + }, + { + get() { + const operation = Imports.findLastImport(); + return API.v1.success({ + success: true, + operation, + }); + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/instances.ts b/apps/meteor/app/api/server/v1/instances.ts index 6826d6733743..204f1ceb9440 100644 --- a/apps/meteor/app/api/server/v1/instances.ts +++ b/apps/meteor/app/api/server/v1/instances.ts @@ -1,9 +1,9 @@ import type { IInstanceStatus } from '@rocket.chat/core-typings'; +import { InstanceStatus } from '@rocket.chat/models'; import { getInstanceConnection } from '../../../../server/stream/streamBroadcast'; import { hasPermission } from '../../../authorization/server'; import { API } from '../api'; -import { InstanceStatus } from '../../../models/server/raw'; API.v1.addRoute( 'instances.get', diff --git a/apps/meteor/app/api/server/v1/integrations.ts b/apps/meteor/app/api/server/v1/integrations.ts index 61413e87af3a..4f3127e58fbb 100644 --- a/apps/meteor/app/api/server/v1/integrations.ts +++ b/apps/meteor/app/api/server/v1/integrations.ts @@ -8,9 +8,10 @@ import { isIntegrationsGetProps, isIntegrationsUpdateProps, } from '@rocket.chat/rest-typings'; +import { Integrations, IntegrationHistory } from '@rocket.chat/models'; +import type { Filter } from 'mongodb'; import { hasAtLeastOnePermission } from '../../../authorization/server'; -import { Integrations, IntegrationHistory } from '../../../models/server/raw'; import { API } from '../api'; import { mountIntegrationHistoryQueryBasedOnPermissions, @@ -55,15 +56,14 @@ API.v1.addRoute( const { sort, fields: projection, query } = this.parseJsonQuery(); const ourQuery = Object.assign(mountIntegrationHistoryQueryBasedOnPermissions(userId, id), query); - const cursor = IntegrationHistory.find(ourQuery, { + const { cursor, totalCount } = IntegrationHistory.findPaginated(ourQuery, { sort: sort || { _updatedAt: -1 }, skip: offset, limit: count, projection, }); - const history = await cursor.toArray(); - const total = await cursor.count(); + const [history, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ history, @@ -80,7 +80,7 @@ API.v1.addRoute( 'integrations.list', { authRequired: true }, { - get() { + async get() { if ( !hasAtLeastOnePermission(this.userId, [ 'manage-outgoing-integrations', @@ -95,17 +95,16 @@ API.v1.addRoute( const { offset, count } = this.getPaginationItems(); const { sort, fields: projection, query } = this.parseJsonQuery(); - const ourQuery = Object.assign(mountIntegrationQueryBasedOnPermissions(this.userId), query); - const cursor = Integrations.find(ourQuery, { + const ourQuery = Object.assign(mountIntegrationQueryBasedOnPermissions(this.userId), query) as Filter; + + const { cursor, totalCount } = Integrations.findPaginated(ourQuery, { sort: sort || { ts: -1 }, skip: offset, limit: count, projection, }); - const total = Promise.await(cursor.count()); - - const integrations = Promise.await(cursor.toArray()); + const [integrations, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ integrations, diff --git a/apps/meteor/app/api/server/v1/invites.js b/apps/meteor/app/api/server/v1/invites.js deleted file mode 100644 index 907585a818bc..000000000000 --- a/apps/meteor/app/api/server/v1/invites.js +++ /dev/null @@ -1,76 +0,0 @@ -import { API } from '../api'; -import { findOrCreateInvite } from '../../../invites/server/functions/findOrCreateInvite'; -import { removeInvite } from '../../../invites/server/functions/removeInvite'; -import { listInvites } from '../../../invites/server/functions/listInvites'; -import { useInviteToken } from '../../../invites/server/functions/useInviteToken'; -import { validateInviteToken } from '../../../invites/server/functions/validateInviteToken'; - -API.v1.addRoute( - 'listInvites', - { authRequired: true }, - { - get() { - const result = Promise.await(listInvites(this.userId)); - return API.v1.success(result); - }, - }, -); - -API.v1.addRoute( - 'findOrCreateInvite', - { authRequired: true }, - { - post() { - const { rid, days, maxUses } = this.bodyParams; - const result = Promise.await(findOrCreateInvite(this.userId, { rid, days, maxUses })); - - return API.v1.success(result); - }, - }, -); - -API.v1.addRoute( - 'removeInvite/:_id', - { authRequired: true }, - { - delete() { - const { _id } = this.urlParams; - const result = Promise.await(removeInvite(this.userId, { _id })); - - return API.v1.success(result); - }, - }, -); - -API.v1.addRoute( - 'useInviteToken', - { authRequired: true }, - { - post() { - const { token } = this.bodyParams; - // eslint-disable-next-line react-hooks/rules-of-hooks - const result = Promise.await(useInviteToken(this.userId, token)); - - return API.v1.success(result); - }, - }, -); - -API.v1.addRoute( - 'validateInviteToken', - { authRequired: false }, - { - post() { - const { token } = this.bodyParams; - - let valid = true; - try { - Promise.await(validateInviteToken(token)); - } catch (e) { - valid = false; - } - - return API.v1.success({ valid }); - }, - }, -); diff --git a/apps/meteor/app/api/server/v1/invites.ts b/apps/meteor/app/api/server/v1/invites.ts new file mode 100644 index 000000000000..8ac3a2b8eddb --- /dev/null +++ b/apps/meteor/app/api/server/v1/invites.ts @@ -0,0 +1,84 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +import { IInvite } from '@rocket.chat/core-typings'; +import { isFindOrCreateInviteParams, isUseInviteTokenProps, isValidateInviteTokenProps } from '@rocket.chat/rest-typings'; + +import { API } from '../api'; +import { findOrCreateInvite } from '../../../invites/server/functions/findOrCreateInvite'; +import { removeInvite } from '../../../invites/server/functions/removeInvite'; +import { listInvites } from '../../../invites/server/functions/listInvites'; +import { useInviteToken } from '../../../invites/server/functions/useInviteToken'; +import { validateInviteToken } from '../../../invites/server/functions/validateInviteToken'; + +API.v1.addRoute( + 'listInvites', + { + authRequired: true, + }, + { + async get() { + const result = await listInvites(this.userId); + return API.v1.success(result); + }, + }, +); + +API.v1.addRoute( + 'findOrCreateInvite', + { + authRequired: true, + validateParams: isFindOrCreateInviteParams, + }, + { + async post() { + const { rid, days, maxUses } = this.bodyParams; + + return API.v1.success((await findOrCreateInvite(this.userId, { rid, days, maxUses })) as IInvite); + }, + }, +); + +API.v1.addRoute( + 'removeInvite/:_id', + { authRequired: true }, + { + async delete() { + const { _id } = this.urlParams; + + return API.v1.success(await removeInvite(this.userId, { _id })); + }, + }, +); + +API.v1.addRoute( + 'useInviteToken', + { + authRequired: true, + validateParams: isUseInviteTokenProps, + }, + { + async post() { + const { token } = this.bodyParams; + // eslint-disable-next-line react-hooks/rules-of-hooks + + return API.v1.success(await useInviteToken(this.userId, token)); + }, + }, +); + +API.v1.addRoute( + 'validateInviteToken', + { + authRequired: false, + validateParams: isValidateInviteTokenProps, + }, + { + async post() { + const { token } = this.bodyParams; + try { + return API.v1.success({ valid: Boolean(await validateInviteToken(token)) }); + } catch (_) { + return API.v1.success({ valid: false }); + } + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/misc.js b/apps/meteor/app/api/server/v1/misc.js deleted file mode 100644 index 77b8519c3818..000000000000 --- a/apps/meteor/app/api/server/v1/misc.js +++ /dev/null @@ -1,471 +0,0 @@ -import crypto from 'crypto'; - -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { EJSON } from 'meteor/ejson'; -import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; -import { escapeHTML } from '@rocket.chat/string-helpers'; - -import { hasPermission } from '../../../authorization/server'; -import { Users } from '../../../models/server'; -import { settings } from '../../../settings/server'; -import { API } from '../api'; -import { getDefaultUserFields } from '../../../utils/server/functions/getDefaultUserFields'; -import { getURL } from '../../../utils/lib/getURL'; -import { getLogs } from '../../../../server/stream/stdout'; -import { SystemLogger } from '../../../../server/lib/logger/system'; - -/** - * @openapi - * /api/v1/me: - * get: - * description: Gets user data of the authenticated user - * security: - * - authenticated: [] - * responses: - * 200: - * description: The user data of the authenticated user - * content: - * application/json: - * schema: - * allOf: - * - $ref: '#/components/schemas/ApiSuccessV1' - * - type: object - * properties: - * name: - * type: string - * username: - * type: string - * nickname: - * type: string - * emails: - * type: array - * items: - * type: object - * properties: - * address: - * type: string - * verified: - * type: boolean - * email: - * type: string - * status: - * $ref: '#/components/schemas/UserStatus' - * statusDefault: - * $ref: '#/components/schemas/UserStatus' - * statusText: - * $ref: '#/components/schemas/UserStatus' - * statusConnection: - * $ref: '#/components/schemas/UserStatus' - * bio: - * type: string - * avatarOrigin: - * type: string - * enum: [none, local, upload, url] - * utcOffset: - * type: number - * language: - * type: string - * settings: - * type: object - * properties: - * preferences: - * type: object - * enableAutoAway: - * type: boolean - * idleTimeLimit: - * type: number - * roles: - * type: array - * active: - * type: boolean - * defaultRoom: - * type: string - * customFields: - * type: array - * requirePasswordChange: - * type: boolean - * requirePasswordChangeReason: - * type: string - * services: - * type: object - * properties: - * github: - * type: object - * gitlab: - * type: object - * tokenpass: - * type: object - * blockstack: - * type: object - * password: - * type: object - * properties: - * exists: - * type: boolean - * totp: - * type: object - * properties: - * enabled: - * type: boolean - * email2fa: - * type: object - * properties: - * enabled: - * type: boolean - * statusLivechat: - * type: string - * enum: [available, 'not-available'] - * banners: - * type: array - * items: - * type: object - * properties: - * id: - * type: string - * title: - * type: string - * text: - * type: string - * textArguments: - * type: array - * items: {} - * modifiers: - * type: array - * items: - * type: string - * infoUrl: - * type: string - * oauth: - * type: object - * properties: - * authorizedClients: - * type: array - * items: - * type: string - * _updatedAt: - * type: string - * format: date-time - * avatarETag: - * type: string - * default: - * description: Unexpected error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiFailureV1' - */ -API.v1.addRoute( - 'me', - { authRequired: true }, - { - get() { - const fields = getDefaultUserFields(); - const user = Users.findOneById(this.userId, { fields }); - - // The password hash shouldn't be leaked but the client may need to know if it exists. - if (user?.services?.password?.bcrypt) { - user.services.password.exists = true; - delete user.services.password.bcrypt; - } - - return API.v1.success(this.getUserInfo(user)); - }, - }, -); - -let onlineCache = 0; -let onlineCacheDate = 0; -const cacheInvalid = 60000; // 1 minute -API.v1.addRoute( - 'shield.svg', - { authRequired: false, rateLimiterOptions: { numRequestsAllowed: 60, intervalTimeInMS: 60000 } }, - { - get() { - const { type, icon } = this.queryParams; - let { channel, name } = this.queryParams; - if (!settings.get('API_Enable_Shields')) { - throw new Meteor.Error('error-endpoint-disabled', 'This endpoint is disabled', { - route: '/api/v1/shield.svg', - }); - } - - const types = settings.get('API_Shield_Types'); - if ( - type && - types !== '*' && - !types - .split(',') - .map((t) => t.trim()) - .includes(type) - ) { - throw new Meteor.Error('error-shield-disabled', 'This shield type is disabled', { - route: '/api/v1/shield.svg', - }); - } - const hideIcon = icon === 'false'; - if (hideIcon && (!name || !name.trim())) { - return API.v1.failure('Name cannot be empty when icon is hidden'); - } - - let text; - let backgroundColor = '#4c1'; - switch (type) { - case 'online': - if (Date.now() - onlineCacheDate > cacheInvalid) { - onlineCache = Users.findUsersNotOffline().count(); - onlineCacheDate = Date.now(); - } - - text = `${onlineCache} ${TAPi18n.__('Online')}`; - break; - case 'channel': - if (!channel) { - return API.v1.failure('Shield channel is required for type "channel"'); - } - - text = `#${channel}`; - break; - case 'user': - if (settings.get('API_Shield_user_require_auth') && !this.getLoggedInUser()) { - return API.v1.failure('You must be logged in to do this.'); - } - const user = this.getUserFromParams(); - - // Respect the server's choice for using their real names or not - if (user.name && settings.get('UI_Use_Real_Name')) { - text = `${user.name}`; - } else { - text = `@${user.username}`; - } - - switch (user.status) { - case 'online': - backgroundColor = '#1fb31f'; - break; - case 'away': - backgroundColor = '#dc9b01'; - break; - case 'busy': - backgroundColor = '#bc2031'; - break; - case 'offline': - backgroundColor = '#a5a1a1'; - } - break; - default: - text = TAPi18n.__('Join_Chat').toUpperCase(); - } - - const iconSize = hideIcon ? 7 : 24; - const leftSize = name ? name.length * 6 + 7 + iconSize : iconSize; - const rightSize = text.length * 6 + 20; - const width = leftSize + rightSize; - const height = 20; - - channel = escapeHTML(channel); - text = escapeHTML(text); - name = escapeHTML(name); - - return { - headers: { 'Content-Type': 'image/svg+xml;charset=utf-8' }, - body: ` - - - - - - - - - - - - - - ${hideIcon ? '' : ``} - - ${ - name - ? `${name} - ${name}` - : '' - } - ${text} - ${text} - - - ` - .trim() - .replace(/\>[\s]+\<'), - }; - }, - }, -); - -API.v1.addRoute( - 'spotlight', - { authRequired: true }, - { - get() { - check(this.queryParams, { - query: String, - }); - - const { query } = this.queryParams; - - const result = Meteor.runAsUser(this.userId, () => Meteor.call('spotlight', query)); - - return API.v1.success(result); - }, - }, -); - -API.v1.addRoute( - 'directory', - { authRequired: true }, - { - get() { - const { offset, count } = this.getPaginationItems(); - const { sort, query } = this.parseJsonQuery(); - - const { text, type, workspace = 'local' } = query; - - if (sort && Object.keys(sort).length > 1) { - return API.v1.failure('This method support only one "sort" parameter'); - } - const sortBy = sort ? Object.keys(sort)[0] : undefined; - const sortDirection = sort && Object.values(sort)[0] === 1 ? 'asc' : 'desc'; - - const result = Meteor.runAsUser(this.userId, () => - Meteor.call('browseChannels', { - text, - type, - workspace, - sortBy, - sortDirection, - offset: Math.max(0, offset), - limit: Math.max(0, count), - }), - ); - - if (!result) { - return API.v1.failure('Please verify the parameters'); - } - return API.v1.success({ - result: result.results, - count: result.results.length, - offset, - total: result.total, - }); - }, - }, -); - -/** - * @openapi - * /api/v1/stdout.queue: - * get: - * description: Retrieves last 1000 lines of server logs - * security: - * - authenticated: ['view-logs'] - * responses: - * 200: - * description: The user data of the authenticated user - * content: - * application/json: - * schema: - * allOf: - * - $ref: '#/components/schemas/ApiSuccessV1' - * - type: object - * properties: - * queue: - * type: array - * items: - * type: object - * properties: - * id: - * type: string - * string: - * type: string - * ts: - * type: string - * format: date-time - * default: - * description: Unexpected error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiFailureV1' - */ -API.v1.addRoute( - 'stdout.queue', - { authRequired: true }, - { - get() { - if (!hasPermission(this.userId, 'view-logs')) { - return API.v1.unauthorized(); - } - return API.v1.success({ queue: getLogs() }); - }, - }, -); - -const mountResult = ({ id, error, result }) => ({ - message: EJSON.stringify({ - msg: 'result', - id, - error, - result, - }), -}); - -const methodCall = () => ({ - post() { - check(this.bodyParams, { - message: String, - }); - - const { method, params, id } = EJSON.parse(this.bodyParams.message); - - const connectionId = - this.token || - crypto - .createHash('md5') - .update(this.requestIp + this.request.headers['user-agent']) - .digest('hex'); - - const rateLimiterInput = { - userId: this.userId, - clientAddress: this.requestIp, - type: 'method', - name: method, - connectionId, - }; - - try { - DDPRateLimiter._increment(rateLimiterInput); - const rateLimitResult = DDPRateLimiter._check(rateLimiterInput); - if (!rateLimitResult.allowed) { - throw new Meteor.Error('too-many-requests', DDPRateLimiter.getErrorMessage(rateLimitResult), { - timeToReset: rateLimitResult.timeToReset, - }); - } - - const result = Meteor.call(method, ...params); - return API.v1.success(mountResult({ id, result })); - } catch (error) { - SystemLogger.error(`Exception while invoking method ${method}`, error.message); - if (settings.get('Log_Level') === '2') { - Meteor._debug(`Exception while invoking method ${method}`, error); - } - return API.v1.success(mountResult({ id, error })); - } - }, -}); - -// had to create two different endpoints for authenticated and non-authenticated calls -// because restivus does not provide 'this.userId' if 'authRequired: false' -API.v1.addRoute('method.call/:method', { authRequired: true, rateLimiterOptions: false }, methodCall()); -API.v1.addRoute('method.callAnon/:method', { authRequired: false, rateLimiterOptions: false }, methodCall()); diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts new file mode 100644 index 000000000000..38e884bd6600 --- /dev/null +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -0,0 +1,585 @@ +import crypto from 'crypto'; + +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { EJSON } from 'meteor/ejson'; +import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; +import { escapeHTML } from '@rocket.chat/string-helpers'; +import { + isShieldSvgProps, + isSpotlightProps, + isDirectoryProps, + isMethodCallProps, + isMethodCallAnonProps, + isMeteorCall, +} from '@rocket.chat/rest-typings'; +import { IUser } from '@rocket.chat/core-typings'; + +import { hasPermission } from '../../../authorization/server'; +import { Users } from '../../../models/server'; +import { settings } from '../../../settings/server'; +import { API } from '../api'; +import { getDefaultUserFields } from '../../../utils/server/functions/getDefaultUserFields'; +import { getURL } from '../../../utils/lib/getURL'; +import { getLogs } from '../../../../server/stream/stdout'; +import { SystemLogger } from '../../../../server/lib/logger/system'; + +/** + * @openapi + * /api/v1/me: + * get: + * description: Gets user data of the authenticated user + * security: + * - authenticated: [] + * responses: + * 200: + * description: The user data of the authenticated user + * content: + * application/json: + * schema: + * allOf: + * - $ref: '#/components/schemas/ApiSuccessV1' + * - type: object + * properties: + * name: + * type: string + * username: + * type: string + * nickname: + * type: string + * emails: + * type: array + * items: + * type: object + * properties: + * address: + * type: string + * verified: + * type: boolean + * email: + * type: string + * status: + * $ref: '#/components/schemas/UserStatus' + * statusDefault: + * $ref: '#/components/schemas/UserStatus' + * statusText: + * $ref: '#/components/schemas/UserStatus' + * statusConnection: + * $ref: '#/components/schemas/UserStatus' + * bio: + * type: string + * avatarOrigin: + * type: string + * enum: [none, local, upload, url] + * utcOffset: + * type: number + * language: + * type: string + * settings: + * type: object + * properties: + * preferences: + * type: object + * enableAutoAway: + * type: boolean + * idleTimeLimit: + * type: number + * roles: + * type: array + * active: + * type: boolean + * defaultRoom: + * type: string + * customFields: + * type: array + * requirePasswordChange: + * type: boolean + * requirePasswordChangeReason: + * type: string + * services: + * type: object + * properties: + * github: + * type: object + * gitlab: + * type: object + * password: + * type: object + * properties: + * exists: + * type: boolean + * totp: + * type: object + * properties: + * enabled: + * type: boolean + * email2fa: + * type: object + * properties: + * enabled: + * type: boolean + * statusLivechat: + * type: string + * enum: [available, 'not-available'] + * banners: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * title: + * type: string + * text: + * type: string + * textArguments: + * type: array + * items: {} + * modifiers: + * type: array + * items: + * type: string + * infoUrl: + * type: string + * oauth: + * type: object + * properties: + * authorizedClients: + * type: array + * items: + * type: string + * _updatedAt: + * type: string + * format: date-time + * avatarETag: + * type: string + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ +API.v1.addRoute( + 'me', + { authRequired: true }, + { + async get() { + const fields = getDefaultUserFields(); + const { services, ...user } = Users.findOneById(this.userId, { fields }) as IUser; + + return API.v1.success( + this.getUserInfo({ + ...user, + ...(services && { + services: { + ...services, + password: { + // The password hash shouldn't be leaked but the client may need to know if it exists. + exists: Boolean(services?.password?.bcrypt), + } as any, + }, + }), + }), + ); + }, + }, +); + +let onlineCache = 0; +let onlineCacheDate = 0; +const cacheInvalid = 60000; // 1 minute + +API.v1.addRoute( + 'shield.svg', + { + authRequired: false, + rateLimiterOptions: { + numRequestsAllowed: 60, + intervalTimeInMS: 60000, + }, + validateParams: isShieldSvgProps, + }, + { + get() { + const { type, icon } = this.queryParams; + let { channel, name } = this.queryParams; + if (!settings.get('API_Enable_Shields')) { + throw new Meteor.Error('error-endpoint-disabled', 'This endpoint is disabled', { + route: '/api/v1/shield.svg', + }); + } + + const types = settings.get('API_Shield_Types'); + if ( + type && + types !== '*' && + !types + .split(',') + .map((t: string) => t.trim()) + .includes(type) + ) { + throw new Meteor.Error('error-shield-disabled', 'This shield type is disabled', { + route: '/api/v1/shield.svg', + }); + } + const hideIcon = icon === 'false'; + if (hideIcon && (!name || !name.trim())) { + return API.v1.failure('Name cannot be empty when icon is hidden'); + } + + let text; + let backgroundColor = '#4c1'; + switch (type) { + case 'online': + if (Date.now() - onlineCacheDate > cacheInvalid) { + onlineCache = Users.findUsersNotOffline().count(); + onlineCacheDate = Date.now(); + } + + text = `${onlineCache} ${TAPi18n.__('Online')}`; + break; + case 'channel': + if (!channel) { + return API.v1.failure('Shield channel is required for type "channel"'); + } + + text = `#${channel}`; + break; + case 'user': + if (settings.get('API_Shield_user_require_auth') && !this.getLoggedInUser()) { + return API.v1.failure('You must be logged in to do this.'); + } + const user = this.getUserFromParams(); + + // Respect the server's choice for using their real names or not + if (user.name && settings.get('UI_Use_Real_Name')) { + text = `${user.name}`; + } else { + text = `@${user.username}`; + } + + switch (user.status) { + case 'online': + backgroundColor = '#1fb31f'; + break; + case 'away': + backgroundColor = '#dc9b01'; + break; + case 'busy': + backgroundColor = '#bc2031'; + break; + case 'offline': + backgroundColor = '#a5a1a1'; + } + break; + default: + text = TAPi18n.__('Join_Chat').toUpperCase(); + } + + const iconSize = hideIcon ? 7 : 24; + const leftSize = name ? name.length * 6 + 7 + iconSize : iconSize; + const rightSize = text.length * 6 + 20; + const width = leftSize + rightSize; + const height = 20; + + channel = escapeHTML(channel); + text = escapeHTML(text); + name = escapeHTML(name); + + return { + headers: { 'Content-Type': 'image/svg+xml;charset=utf-8' }, + body: ` + + + + + + + + + + + + + + ${hideIcon ? '' : ``} + + ${ + name + ? `${name} + ${name}` + : '' + } + ${text} + ${text} + + + ` + .trim() + .replace(/\>[\s]+\<'), + } as any; + }, + }, +); + +API.v1.addRoute( + 'spotlight', + { + authRequired: true, + validateParams: isSpotlightProps, + }, + { + get() { + const { query } = this.queryParams; + + const result = Meteor.call('spotlight', query); + + return API.v1.success(result); + }, + }, +); + +API.v1.addRoute( + 'directory', + { + authRequired: true, + validateParams: isDirectoryProps, + }, + { + get() { + const { offset, count } = this.getPaginationItems(); + const { sort, query } = this.parseJsonQuery(); + + const { text, type, workspace = 'local' } = query; + + if (sort && Object.keys(sort).length > 1) { + return API.v1.failure('This method support only one "sort" parameter'); + } + const sortBy = sort ? Object.keys(sort)[0] : undefined; + const sortDirection = sort && Object.values(sort)[0] === 1 ? 'asc' : 'desc'; + + const result = Meteor.call('browseChannels', { + text, + type, + workspace, + sortBy, + sortDirection, + offset: Math.max(0, offset), + limit: Math.max(0, count), + }); + + if (!result) { + return API.v1.failure('Please verify the parameters'); + } + return API.v1.success({ + result: result.results, + count: result.results.length, + offset, + total: result.total, + }); + }, + }, +); + +/** + * @openapi + * /api/v1/stdout.queue: + * get: + * description: Retrieves last 1000 lines of server logs + * security: + * - authenticated: ['view-logs'] + * responses: + * 200: + * description: The user data of the authenticated user + * content: + * application/json: + * schema: + * allOf: + * - $ref: '#/components/schemas/ApiSuccessV1' + * - type: object + * properties: + * queue: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * string: + * type: string + * ts: + * type: string + * format: date-time + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ +API.v1.addRoute( + 'stdout.queue', + { authRequired: true }, + { + get() { + if (!hasPermission(this.userId, 'view-logs')) { + return API.v1.unauthorized(); + } + return API.v1.success({ queue: getLogs() }); + }, + }, +); + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/interface-name-prefix + interface Endpoints { + 'method.call/:method': { + POST: (params: { method: string; args: any[] }) => any; + }; + 'method.callAnon/:method': { + POST: (params: { method: string; args: any[] }) => any; + }; + } +} + +const mountResult = ({ + id, + error, + result, +}: { + id: string; + error?: unknown; + result?: unknown; +}): { + message: string; +} => ({ + message: EJSON.stringify({ + msg: 'result', + id, + error: error as any, + result: result as any, + }), +}); + +// had to create two different endpoints for authenticated and non-authenticated calls +// because restivus does not provide 'this.userId' if 'authRequired: false' +API.v1.addRoute( + 'method.call/:method', + { + authRequired: true, + rateLimiterOptions: false, + validateParams: isMeteorCall, + }, + { + post() { + check(this.bodyParams, { + message: String, + }); + + const data = EJSON.parse(this.bodyParams.message); + + if (!isMethodCallProps(data)) { + return API.v1.failure('Invalid method call'); + } + + const { method, params, id } = data; + + const connectionId = + this.token || + crypto + .createHash('md5') + .update(this.requestIp + this.request.headers['user-agent']) + .digest('hex'); + + const rateLimiterInput = { + userId: this.userId, + clientAddress: this.requestIp, + type: 'method', + name: method, + connectionId, + }; + + try { + DDPRateLimiter._increment(rateLimiterInput); + const rateLimitResult = DDPRateLimiter._check(rateLimiterInput); + if (!rateLimitResult.allowed) { + throw new Meteor.Error('too-many-requests', DDPRateLimiter.getErrorMessage(rateLimitResult), { + timeToReset: rateLimitResult.timeToReset, + }); + } + + const result = Meteor.call(method, ...params); + return API.v1.success(mountResult({ id, result })); + } catch (error) { + if (error instanceof Error) SystemLogger.error(`Exception while invoking method ${method}`, error.message); + else SystemLogger.error(`Exception while invoking method ${method}`, error); + + if (settings.get('Log_Level') === '2') { + Meteor._debug(`Exception while invoking method ${method}`, error); + } + return API.v1.success(mountResult({ id, error })); + } + }, + }, +); +API.v1.addRoute( + 'method.callAnon/:method', + { + authRequired: false, + rateLimiterOptions: false, + validateParams: isMeteorCall, + }, + { + post() { + check(this.bodyParams, { + message: String, + }); + + const data = EJSON.parse(this.bodyParams.message); + + if (!isMethodCallAnonProps(data)) { + return API.v1.failure('Invalid method call'); + } + + const { method, params, id } = data; + + const connectionId = + this.token || + crypto + .createHash('md5') + .update(this.requestIp + this.request.headers['user-agent']) + .digest('hex'); + + const rateLimiterInput = { + userId: this.userId || undefined, + clientAddress: this.requestIp, + type: 'method', + name: method, + connectionId, + }; + + try { + DDPRateLimiter._increment(rateLimiterInput); + const rateLimitResult = DDPRateLimiter._check(rateLimiterInput); + if (!rateLimitResult.allowed) { + throw new Meteor.Error('too-many-requests', DDPRateLimiter.getErrorMessage(rateLimitResult), { + timeToReset: rateLimitResult.timeToReset, + }); + } + + const result = Meteor.call(method, ...params); + return API.v1.success(mountResult({ id, result })); + } catch (error) { + if (error instanceof Error) SystemLogger.error(`Exception while invoking method ${method}`, error.message); + else SystemLogger.error(`Exception while invoking method ${method}`, error); + + if (settings.get('Log_Level') === '2') { + Meteor._debug(`Exception while invoking method ${method}`, error); + } + return API.v1.success(mountResult({ id, error })); + } + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/oauthapps.ts b/apps/meteor/app/api/server/v1/oauthapps.ts index 50d9549dac67..a9d48884595d 100644 --- a/apps/meteor/app/api/server/v1/oauthapps.ts +++ b/apps/meteor/app/api/server/v1/oauthapps.ts @@ -1,7 +1,7 @@ import { isOauthAppsGetParams } from '@rocket.chat/rest-typings'; +import { OAuthApps } from '@rocket.chat/models'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { OAuthApps } from '../../../models/server/raw'; import { API } from '../api'; API.v1.addRoute( diff --git a/apps/meteor/app/api/server/v1/permissions.ts b/apps/meteor/app/api/server/v1/permissions.ts index f820c733c4b7..d03fab78e7fb 100644 --- a/apps/meteor/app/api/server/v1/permissions.ts +++ b/apps/meteor/app/api/server/v1/permissions.ts @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; import type { IPermission } from '@rocket.chat/core-typings'; import { isBodyParamsValidPermissionUpdate } from '@rocket.chat/rest-typings'; +import { Permissions, Roles } from '@rocket.chat/models'; import { hasPermission } from '../../../authorization/server'; import { API } from '../api'; -import { Permissions, Roles } from '../../../models/server/raw'; API.v1.addRoute( 'permissions.listAll', diff --git a/apps/meteor/app/api/server/v1/push.ts b/apps/meteor/app/api/server/v1/push.ts index 1dc649cb5adf..9ed60260d422 100644 --- a/apps/meteor/app/api/server/v1/push.ts +++ b/apps/meteor/app/api/server/v1/push.ts @@ -1,13 +1,13 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { Match, check } from 'meteor/check'; +import { Messages } from '@rocket.chat/models'; import { appTokensCollection } from '../../../push/server'; import { API } from '../api'; import PushNotification from '../../../push-notifications/server/lib/PushNotification'; import { canAccessRoom } from '../../../authorization/server/functions/canAccessRoom'; import { Users, Rooms } from '../../../models/server'; -import { Messages } from '../../../models/server/raw'; API.v1.addRoute( 'push.token', @@ -38,6 +38,7 @@ API.v1.addRoute( Meteor.call('raix:push-update', { id: deviceId, token: { [type]: value }, + authToken: this.request.headers['x-auth-token'], appName, userId: this.userId, }), diff --git a/apps/meteor/app/api/server/v1/roles.ts b/apps/meteor/app/api/server/v1/roles.ts index 9785741326d7..252421d1a9b6 100644 --- a/apps/meteor/app/api/server/v1/roles.ts +++ b/apps/meteor/app/api/server/v1/roles.ts @@ -8,13 +8,14 @@ import { isRoleUpdateProps, } from '@rocket.chat/rest-typings'; import type { IRole } from '@rocket.chat/core-typings'; +import { Roles } from '@rocket.chat/models'; import { Users } from '../../../models/server'; import { API } from '../api'; -import { getUsersInRole, hasRole } from '../../../authorization/server'; +import { hasRole } from '../../../authorization/server'; +import { getUsersInRolePaginated } from '../../../authorization/server/functions/getUsersInRole'; import { settings } from '../../../settings/server/index'; import { api } from '../../../../server/sdk/api'; -import { Roles } from '../../../models/server/raw'; import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { hasAnyRoleAsync } from '../../../authorization/server/functions/hasRole'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -175,14 +176,16 @@ API.v1.addRoute( apiDeprecationLogger.warn(`Querying roles by name is deprecated and will be removed on the next major release of Rocket.Chat`); } - const users = await getUsersInRole(roleData._id, roomId, { + const { cursor, totalCount } = await getUsersInRolePaginated(roleData._id, roomId, { limit: count as number, sort: { username: 1 }, skip: offset as number, projection, }); - return API.v1.success({ users: await users.toArray(), total: await users.count() }); + const [users, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ users, total }); }, }, ); diff --git a/apps/meteor/app/api/server/v1/rooms.js b/apps/meteor/app/api/server/v1/rooms.js index 5d722a6e9fc4..ee059908eda0 100644 --- a/apps/meteor/app/api/server/v1/rooms.js +++ b/apps/meteor/app/api/server/v1/rooms.js @@ -1,7 +1,8 @@ import { Meteor } from 'meteor/meteor'; +import { Rooms as RoomsRaw } from '@rocket.chat/models'; import { FileUpload } from '../../../file-upload'; -import { Rooms, Messages } from '../../../models'; +import { Rooms, Messages } from '../../../models/server'; import { API } from '../api'; import { findAdminRooms, @@ -85,10 +86,13 @@ API.v1.addRoute( return API.v1.unauthorized(); } - const { file, ...fields } = Promise.await( - getUploadFormData({ - request: this.request, - }), + const [file, fields] = Promise.await( + getUploadFormData( + { + request: this.request, + }, + { field: 'file' }, + ), ); if (!file) { @@ -290,7 +294,7 @@ API.v1.addRoute( 'rooms.getDiscussions', { authRequired: true }, { - get() { + async get() { const room = findRoomByIdOrName({ params: this.requestParams() }); const { offset, count } = this.getPaginationItems(); const { sort, fields, query } = this.parseJsonQuery(); @@ -301,18 +305,20 @@ API.v1.addRoute( const ourQuery = Object.assign(query, { prid: room._id }); - const discussions = Rooms.find(ourQuery, { + const { cursor, totalCount } = RoomsRaw.findPaginated(ourQuery, { sort: sort || { fname: 1 }, skip: offset, limit: count, - fields, - }).fetch(); + projection: fields, + }); + + const [discussions, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ discussions, count: discussions.length, offset, - total: Rooms.find(ourQuery).count(), + total, }); }, }, diff --git a/apps/meteor/app/api/server/v1/settings.ts b/apps/meteor/app/api/server/v1/settings.ts index f8bdb46c352f..1a1880aa545d 100644 --- a/apps/meteor/app/api/server/v1/settings.ts +++ b/apps/meteor/app/api/server/v1/settings.ts @@ -8,30 +8,33 @@ import { isSettingsUpdatePropsActions, isSettingsUpdatePropsColor, } from '@rocket.chat/rest-typings'; +import { Settings } from '@rocket.chat/models'; +import type { FindOptions } from 'mongodb'; -import { Settings } from '../../../models/server/raw'; import { hasPermission } from '../../../authorization/server'; import { API, ResultFor } from '../api'; import { SettingsEvents, settings } from '../../../settings/server'; import { setValue } from '../../../settings/server/raw'; -const fetchSettings = async ( +async function fetchSettings( query: Parameters[0], - sort: Parameters[1]['sort'], - offset: Parameters[1]['skip'], - count: Parameters[1]['limit'], - fields: Parameters[1]['projection'], -): Promise => { - const settings = (await Settings.find(query, { + sort: FindOptions['sort'], + offset: FindOptions['skip'], + count: FindOptions['limit'], + fields: FindOptions['projection'], +): Promise<{ settings: ISetting[]; totalCount: number }> { + const { cursor, totalCount } = Settings.findPaginated(query || {}, { sort: sort || { _id: 1 }, skip: offset, limit: count, projection: { _id: 1, value: 1, enterprise: 1, invalidValue: 1, modules: 1, ...fields }, - }).toArray()) as unknown as ISetting[]; + }); + + const [settings, total] = await Promise.all([cursor.toArray(), totalCount]); SettingsEvents.emit('fetch-settings', settings); - return settings; -}; + return { settings, totalCount: total }; +} // settings endpoints API.v1.addRoute( @@ -48,13 +51,13 @@ API.v1.addRoute( public: true, }; - const settings = await fetchSettings(ourQuery, sort, offset, count, fields); + const { settings, totalCount: total } = await fetchSettings(ourQuery, sort, offset, count, fields); return API.v1.success({ settings, count: settings.length, offset, - total: await Settings.find(ourQuery).count(), + total, }); }, }, @@ -126,13 +129,13 @@ API.v1.addRoute( ourQuery = Object.assign({}, query, ourQuery); - const settings = await fetchSettings(ourQuery, sort, offset, count, fields); + const { settings, totalCount: total } = await fetchSettings(ourQuery, sort, offset, count, fields); return API.v1.success({ settings, count: settings.length, offset, - total: Settings.find(ourQuery).count(), + total, }); }, }, @@ -154,7 +157,7 @@ API.v1.addRoute( }, post: { twoFactorRequired: true, - async action(): Promise> { + async action(): Promise> { if (!hasPermission(this.userId, 'edit-privileged-setting')) { return API.v1.unauthorized(); } diff --git a/apps/meteor/app/api/server/v1/subscriptions.js b/apps/meteor/app/api/server/v1/subscriptions.js deleted file mode 100644 index 6624ede0e6c4..000000000000 --- a/apps/meteor/app/api/server/v1/subscriptions.js +++ /dev/null @@ -1,99 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; - -import { Subscriptions } from '../../../models'; -import { API } from '../api'; - -API.v1.addRoute( - 'subscriptions.get', - { authRequired: true }, - { - get() { - const { updatedSince } = this.queryParams; - - let updatedSinceDate; - if (updatedSince) { - if (isNaN(Date.parse(updatedSince))) { - throw new Meteor.Error('error-roomId-param-invalid', 'The "lastUpdate" query parameter must be a valid date.'); - } else { - updatedSinceDate = new Date(updatedSince); - } - } - - let result; - Meteor.runAsUser(this.userId, () => { - result = Meteor.call('subscriptions/get', updatedSinceDate); - }); - - if (Array.isArray(result)) { - result = { - update: result, - remove: [], - }; - } - - return API.v1.success(result); - }, - }, -); - -API.v1.addRoute( - 'subscriptions.getOne', - { authRequired: true }, - { - get() { - const { roomId } = this.requestParams(); - - if (!roomId) { - return API.v1.failure("The 'roomId' param is required"); - } - - const subscription = Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId); - - return API.v1.success({ - subscription, - }); - }, - }, -); - -/** - This API is suppose to mark any room as read. - - Method: POST - Route: api/v1/subscriptions.read - Params: - - rid: The rid of the room to be marked as read. - */ -API.v1.addRoute( - 'subscriptions.read', - { authRequired: true }, - { - post() { - check(this.bodyParams, { - rid: String, - }); - - Meteor.runAsUser(this.userId, () => Meteor.call('readMessages', this.bodyParams.rid)); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'subscriptions.unread', - { authRequired: true }, - { - post() { - const { roomId, firstUnreadMessage } = this.bodyParams; - if (!roomId && firstUnreadMessage && !firstUnreadMessage._id) { - return API.v1.failure('At least one of "roomId" or "firstUnreadMessage._id" params is required'); - } - - Meteor.runAsUser(this.userId, () => Meteor.call('unreadMessages', firstUnreadMessage, roomId)); - - return API.v1.success(); - }, - }, -); diff --git a/apps/meteor/app/api/server/v1/subscriptions.ts b/apps/meteor/app/api/server/v1/subscriptions.ts new file mode 100644 index 000000000000..08b186301f91 --- /dev/null +++ b/apps/meteor/app/api/server/v1/subscriptions.ts @@ -0,0 +1,101 @@ +import { Meteor } from 'meteor/meteor'; +import { + isSubscriptionsGetProps, + isSubscriptionsGetOneProps, + isSubscriptionsReadProps, + isSubscriptionsUnreadProps, +} from '@rocket.chat/rest-typings'; +import { Subscriptions } from '@rocket.chat/models'; + +import { API } from '../api'; + +API.v1.addRoute( + 'subscriptions.get', + { + authRequired: true, + validateParams: isSubscriptionsGetProps, + }, + { + async get() { + const { updatedSince } = this.queryParams; + + let updatedSinceDate: Date | undefined; + if (updatedSince) { + if (isNaN(Date.parse(updatedSince as string))) { + throw new Meteor.Error('error-roomId-param-invalid', 'The "lastUpdate" query parameter must be a valid date.'); + } + updatedSinceDate = new Date(updatedSince as string); + } + + const result = await Meteor.call('subscriptions/get', updatedSinceDate); + + return API.v1.success( + Array.isArray(result) + ? { + update: result, + remove: [], + } + : result, + ); + }, + }, +); + +API.v1.addRoute( + 'subscriptions.getOne', + { + authRequired: true, + validateParams: isSubscriptionsGetOneProps, + }, + { + async get() { + const { roomId }: { [roomId: string]: {} } | Record = this.queryParams; + + if (!roomId) { + return API.v1.failure("The 'roomId' param is required"); + } + + return API.v1.success({ + subscription: await Subscriptions.findOneByRoomIdAndUserId(roomId as string, this.userId), + }); + }, + }, +); + +/** + This API is suppose to mark any room as read. + + Method: POST + Route: api/v1/subscriptions.read + Params: + - rid: The rid of the room to be marked as read. + */ +API.v1.addRoute( + 'subscriptions.read', + { + authRequired: true, + validateParams: isSubscriptionsReadProps, + }, + { + post() { + Meteor.call('readMessages', this.bodyParams.rid); + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'subscriptions.unread', + { + authRequired: true, + validateParams: isSubscriptionsUnreadProps, + }, + { + post() { + Meteor.call('unreadMessages', (this.bodyParams as any).firstUnreadMessage, (this.bodyParams as any).roomId); + + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/teams.ts b/apps/meteor/app/api/server/v1/teams.ts index 157f1ac6d894..4e905f51797e 100644 --- a/apps/meteor/app/api/server/v1/teams.ts +++ b/apps/meteor/app/api/server/v1/teams.ts @@ -1,8 +1,6 @@ -import type { FilterQuery } from 'mongodb'; import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { escapeRegExp } from '@rocket.chat/string-helpers'; -import type { IUser } from '@rocket.chat/core-typings'; import { isTeamsConvertToChannelProps, isTeamsRemoveRoomProps, @@ -141,11 +139,9 @@ API.v1.addRoute( }); } - await Promise.all([ - Team.unsetTeamIdOfRooms(this.userId, team._id), - Team.removeAllMembersFromTeam(team._id), - Team.deleteById(team._id), - ]); + await Promise.all([Team.unsetTeamIdOfRooms(this.userId, team._id), Team.removeAllMembersFromTeam(team._id)]); + + await Team.deleteById(team._id); return API.v1.success(); }, @@ -413,7 +409,7 @@ API.v1.addRoute( username: username ? new RegExp(escapeRegExp(username), 'i') : undefined, name: name ? new RegExp(escapeRegExp(name), 'i') : undefined, status: status ? { $in: status } : undefined, - } as FilterQuery; + }; const { records, total } = await Team.members(this.userId, team._id, canSeeAllMembers, { offset, count }, query); diff --git a/apps/meteor/app/api/server/v1/users.js b/apps/meteor/app/api/server/v1/users.js deleted file mode 100644 index 208637159681..000000000000 --- a/apps/meteor/app/api/server/v1/users.js +++ /dev/null @@ -1,1186 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Accounts } from 'meteor/accounts-base'; -import { Match, check } from 'meteor/check'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import _ from 'underscore'; - -import { Users, Subscriptions } from '../../../models/server'; -import { Users as UsersRaw } from '../../../models/server/raw'; -import { hasPermission } from '../../../authorization'; -import { settings } from '../../../settings/server'; -import { getURL } from '../../../utils'; -import { - validateCustomFields, - saveUser, - saveCustomFieldsWithoutValidation, - checkUsernameAvailability, - setUserAvatar, - saveCustomFields, - setStatusText, -} from '../../../lib/server'; -import { getFullUserDataByIdOrUsername } from '../../../lib/server/functions/getFullUserData'; -import { API } from '../api'; -import { getUploadFormData } from '../lib/getUploadFormData'; -import { findUsersToAutocomplete, getInclusiveFields, getNonEmptyFields, getNonEmptyQuery } from '../lib/users'; -import { getUserForCheck, emailCheck } from '../../../2fa/server/code'; -import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey'; -import { setUserStatus } from '../../../../imports/users-presence/server/activeUsers'; -import { resetTOTP } from '../../../2fa/server/functions/resetTOTP'; -import { Team } from '../../../../server/sdk'; -import { isValidQuery } from '../lib/isValidQuery'; - -API.v1.addRoute( - 'users.create', - { authRequired: true }, - { - post() { - check(this.bodyParams, { - email: String, - name: String, - password: String, - username: String, - active: Match.Maybe(Boolean), - bio: Match.Maybe(String), - nickname: Match.Maybe(String), - statusText: Match.Maybe(String), - roles: Match.Maybe(Array), - joinDefaultChannels: Match.Maybe(Boolean), - requirePasswordChange: Match.Maybe(Boolean), - setRandomPassword: Match.Maybe(Boolean), - sendWelcomeEmail: Match.Maybe(Boolean), - verified: Match.Maybe(Boolean), - customFields: Match.Maybe(Object), - }); - - // New change made by pull request #5152 - if (typeof this.bodyParams.joinDefaultChannels === 'undefined') { - this.bodyParams.joinDefaultChannels = true; - } - - if (this.bodyParams.customFields) { - validateCustomFields(this.bodyParams.customFields); - } - - const newUserId = saveUser(this.userId, this.bodyParams); - - if (this.bodyParams.customFields) { - saveCustomFieldsWithoutValidation(newUserId, this.bodyParams.customFields); - } - - if (typeof this.bodyParams.active !== 'undefined') { - Meteor.runAsUser(this.userId, () => { - Meteor.call('setUserActiveStatus', newUserId, this.bodyParams.active); - }); - } - - const { fields } = this.parseJsonQuery(); - - return API.v1.success({ user: Users.findOneById(newUserId, { fields }) }); - }, - }, -); - -API.v1.addRoute( - 'users.delete', - { authRequired: true }, - { - post() { - if (!hasPermission(this.userId, 'delete-user')) { - return API.v1.unauthorized(); - } - - const user = this.getUserFromParams(); - const { confirmRelinquish = false } = this.requestParams(); - - Meteor.runAsUser(this.userId, () => { - Meteor.call('deleteUser', user._id, confirmRelinquish); - }); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'users.deleteOwnAccount', - { authRequired: true }, - { - post() { - const { password } = this.bodyParams; - if (!password) { - return API.v1.failure('Body parameter "password" is required.'); - } - if (!settings.get('Accounts_AllowDeleteOwnAccount')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } - - const { confirmRelinquish = false } = this.requestParams(); - - Meteor.runAsUser(this.userId, () => { - Meteor.call('deleteUserOwnAccount', password, confirmRelinquish); - }); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'users.getAvatar', - { authRequired: false }, - { - get() { - const user = this.getUserFromParams(); - - const url = getURL(`/avatar/${user.username}`, { cdn: false, full: true }); - this.response.setHeader('Location', url); - - return { - statusCode: 307, - body: url, - }; - }, - }, -); - -API.v1.addRoute( - 'users.setActiveStatus', - { authRequired: true }, - { - post() { - check(this.bodyParams, { - userId: String, - activeStatus: Boolean, - confirmRelinquish: Match.Maybe(Boolean), - }); - - if (!hasPermission(this.userId, 'edit-other-user-active-status')) { - return API.v1.unauthorized(); - } - - Meteor.runAsUser(this.userId, () => { - const { userId, activeStatus, confirmRelinquish = false } = this.bodyParams; - Meteor.call('setUserActiveStatus', userId, activeStatus, confirmRelinquish); - }); - return API.v1.success({ - user: Users.findOneById(this.bodyParams.userId, { fields: { active: 1 } }), - }); - }, - }, -); - -API.v1.addRoute( - 'users.deactivateIdle', - { authRequired: true }, - { - post() { - check(this.bodyParams, { - daysIdle: Match.Integer, - role: Match.Optional(String), - }); - - if (!hasPermission(this.userId, 'edit-other-user-active-status')) { - return API.v1.unauthorized(); - } - - const { daysIdle, role = 'user' } = this.bodyParams; - - const lastLoggedIn = new Date(); - lastLoggedIn.setDate(lastLoggedIn.getDate() - daysIdle); - - const count = Users.setActiveNotLoggedInAfterWithRole(lastLoggedIn, role, false); - - return API.v1.success({ - count, - }); - }, - }, -); - -API.v1.addRoute( - 'users.getPresence', - { authRequired: true }, - { - get() { - if (this.isUserFromParams()) { - const user = Users.findOneById(this.userId); - return API.v1.success({ - presence: user.status, - connectionStatus: user.statusConnection, - lastLogin: user.lastLogin, - }); - } - - const user = this.getUserFromParams(); - - return API.v1.success({ - presence: user.status, - }); - }, - }, -); - -API.v1.addRoute( - 'users.info', - { authRequired: true }, - { - get() { - const { username, userId } = this.requestParams(); - const { fields } = this.parseJsonQuery(); - - check(userId, Match.Maybe(String)); - check(username, Match.Maybe(String)); - - if (userId !== undefined && username !== undefined) { - throw new Meteor.Error('invalid-filter', 'Cannot filter by id and username at once'); - } - - if (!userId && !username) { - throw new Meteor.Error('invalid-filter', 'Must filter by id or username'); - } - - const user = getFullUserDataByIdOrUsername({ userId: this.userId, filterId: userId, filterUsername: username }); - - if (!user) { - return API.v1.failure('User not found.'); - } - const myself = user._id === this.userId; - if (fields.userRooms === 1 && (myself || hasPermission(this.userId, 'view-other-user-channels'))) { - user.rooms = Subscriptions.findByUserId(user._id, { - fields: { - rid: 1, - name: 1, - t: 1, - roles: 1, - unread: 1, - }, - sort: { - t: 1, - name: 1, - }, - }).fetch(); - } - - return API.v1.success({ - user, - }); - }, - }, -); - -API.v1.addRoute( - 'users.list', - { - authRequired: true, - queryOperations: ['$or', '$and'], - }, - { - get() { - if (!hasPermission(this.userId, 'view-d-room')) { - return API.v1.unauthorized(); - } - - const { offset, count } = this.getPaginationItems(); - const { sort, fields, query } = this.parseJsonQuery(); - - const nonEmptyQuery = getNonEmptyQuery(query); - const nonEmptyFields = getNonEmptyFields(fields); - - const inclusiveFields = getInclusiveFields(nonEmptyFields); - - const inclusiveFieldsKeys = Object.keys(inclusiveFields); - - if ( - !isValidQuery( - nonEmptyQuery, - [ - ...inclusiveFieldsKeys, - inclusiveFieldsKeys.includes('emails') && 'emails.address.*', - inclusiveFieldsKeys.includes('username') && 'username.*', - inclusiveFieldsKeys.includes('name') && 'name.*', - ].filter(Boolean), - this.queryOperations, - ) - ) { - throw new Meteor.Error('error-invalid-query', isValidQuery.errors.join('\n')); - } - - const actualSort = sort && sort.name ? { nameInsensitive: sort.name, ...sort } : sort || { username: 1 }; - - const limit = - count !== 0 - ? [ - { - $limit: count, - }, - ] - : []; - - const result = Promise.await( - UsersRaw.col - .aggregate([ - { - $match: nonEmptyQuery, - }, - { - $project: inclusiveFields, - }, - { - $addFields: { - nameInsensitive: { - $toLower: '$name', - }, - }, - }, - { - $facet: { - sortedResults: [ - { - $sort: actualSort, - }, - { - $skip: offset, - }, - ...limit, - ], - totalCount: [{ $group: { _id: null, total: { $sum: 1 } } }], - }, - }, - ]) - .toArray(), - ); - - const { - sortedResults: users, - totalCount: [{ total } = { total: 0 }], - } = result[0]; - - return API.v1.success({ - users, - count: users.length, - offset, - total, - }); - }, - }, -); - -API.v1.addRoute( - 'users.register', - { - authRequired: false, - rateLimiterOptions: { - numRequestsAllowed: settings.get('Rate_Limiter_Limit_RegisterUser') ?? 1, - intervalTimeInMS: settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'), - }, - }, - { - post() { - if (this.userId) { - return API.v1.failure('Logged in users can not register again.'); - } - - // We set their username here, so require it - // The `registerUser` checks for the other requirements - check( - this.bodyParams, - Match.ObjectIncluding({ - username: String, - }), - ); - - if (!checkUsernameAvailability(this.bodyParams.username)) { - return API.v1.failure('Username is already in use'); - } - - // Register the user - const userId = Meteor.call('registerUser', this.bodyParams); - - // Now set their username - Meteor.runAsUser(userId, () => Meteor.call('setUsername', this.bodyParams.username)); - const { fields } = this.parseJsonQuery(); - - return API.v1.success({ user: Users.findOneById(userId, { fields }) }); - }, - }, -); - -API.v1.addRoute( - 'users.resetAvatar', - { authRequired: true }, - { - post() { - const user = this.getUserFromParams(); - - if (settings.get('Accounts_AllowUserAvatarChange') && user._id === this.userId) { - Meteor.runAsUser(this.userId, () => Meteor.call('resetAvatar')); - } else if (hasPermission(this.userId, 'edit-other-user-avatar')) { - Meteor.runAsUser(this.userId, () => Meteor.call('resetAvatar', user._id)); - } else { - throw new Meteor.Error('error-not-allowed', 'Reset avatar is not allowed', { - method: 'users.resetAvatar', - }); - } - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'users.setAvatar', - { authRequired: true }, - { - async post() { - check( - this.bodyParams, - Match.ObjectIncluding({ - avatarUrl: Match.Maybe(String), - userId: Match.Maybe(String), - username: Match.Maybe(String), - }), - ); - const canEditOtherUserAvatar = hasPermission(this.userId, 'edit-other-user-avatar'); - - if (!settings.get('Accounts_AllowUserAvatarChange') && !canEditOtherUserAvatar) { - throw new Meteor.Error('error-not-allowed', 'Change avatar is not allowed', { - method: 'users.setAvatar', - }); - } - - let user; - if (this.isUserFromParams()) { - user = Meteor.users.findOne(this.userId); - } else if (canEditOtherUserAvatar) { - user = this.getUserFromParams(); - } else { - return API.v1.unauthorized(); - } - - if (this.bodyParams.avatarUrl) { - setUserAvatar(user, this.bodyParams.avatarUrl, '', 'url'); - return API.v1.success(); - } - - const { image, ...fields } = await getUploadFormData({ - request: this.request, - }); - - if (!image) { - return API.v1.failure("The 'image' param is required"); - } - - const sentTheUserByFormData = fields.userId || fields.username; - if (sentTheUserByFormData) { - if (fields.userId) { - user = Users.findOneById(fields.userId, { fields: { username: 1 } }); - } else if (fields.username) { - user = Users.findOneByUsernameIgnoringCase(fields.username, { fields: { username: 1 } }); - } - - if (!user) { - throw new Meteor.Error('error-invalid-user', 'The optional "userId" or "username" param provided does not match any users'); - } - - const isAnotherUser = this.userId !== user._id; - if (isAnotherUser && !hasPermission(this.userId, 'edit-other-user-avatar')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } - } - - setUserAvatar(user, image.fileBuffer, image.mimetype, 'rest'); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'users.getStatus', - { authRequired: true }, - { - get() { - if (this.isUserFromParams()) { - const user = Users.findOneById(this.userId); - return API.v1.success({ - _id: user._id, - message: user.statusText, - connectionStatus: user.statusConnection, - status: user.status, - }); - } - - const user = this.getUserFromParams(); - - return API.v1.success({ - _id: user._id, - message: user.statusText, - status: user.status, - }); - }, - }, -); - -API.v1.addRoute( - 'users.setStatus', - { authRequired: true }, - { - post() { - check( - this.bodyParams, - Match.ObjectIncluding({ - status: Match.Maybe(String), - message: Match.Maybe(String), - }), - ); - - if (!settings.get('Accounts_AllowUserStatusMessageChange')) { - throw new Meteor.Error('error-not-allowed', 'Change status is not allowed', { - method: 'users.setStatus', - }); - } - - let user; - if (this.isUserFromParams()) { - user = Meteor.users.findOne(this.userId); - } else if (hasPermission(this.userId, 'edit-other-user-info')) { - user = this.getUserFromParams(); - } else { - return API.v1.unauthorized(); - } - - Meteor.runAsUser(user._id, () => { - if (this.bodyParams.message || this.bodyParams.message === '') { - setStatusText(user._id, this.bodyParams.message); - } - if (this.bodyParams.status) { - const validStatus = ['online', 'away', 'offline', 'busy']; - if (validStatus.includes(this.bodyParams.status)) { - const { status } = this.bodyParams; - - if (status === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) { - throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { - method: 'users.setStatus', - }); - } - - Meteor.users.update(user._id, { - $set: { - status, - statusDefault: status, - }, - }); - - setUserStatus(user, status); - } else { - throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', { - method: 'users.setStatus', - }); - } - } - }); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'users.update', - { authRequired: true, twoFactorRequired: true }, - { - post() { - check(this.bodyParams, { - userId: String, - data: Match.ObjectIncluding({ - email: Match.Maybe(String), - name: Match.Maybe(String), - password: Match.Maybe(String), - username: Match.Maybe(String), - bio: Match.Maybe(String), - nickname: Match.Maybe(String), - statusText: Match.Maybe(String), - active: Match.Maybe(Boolean), - roles: Match.Maybe(Array), - joinDefaultChannels: Match.Maybe(Boolean), - requirePasswordChange: Match.Maybe(Boolean), - sendWelcomeEmail: Match.Maybe(Boolean), - verified: Match.Maybe(Boolean), - customFields: Match.Maybe(Object), - }), - }); - - const userData = _.extend({ _id: this.bodyParams.userId }, this.bodyParams.data); - - Meteor.runAsUser(this.userId, () => saveUser(this.userId, userData)); - - if (this.bodyParams.data.customFields) { - saveCustomFields(this.bodyParams.userId, this.bodyParams.data.customFields); - } - - if (typeof this.bodyParams.data.active !== 'undefined') { - const { - userId, - data: { active }, - confirmRelinquish = false, - } = this.bodyParams; - - Meteor.runAsUser(this.userId, () => { - Meteor.call('setUserActiveStatus', userId, active, confirmRelinquish); - }); - } - const { fields } = this.parseJsonQuery(); - - return API.v1.success({ user: Users.findOneById(this.bodyParams.userId, { fields }) }); - }, - }, -); - -API.v1.addRoute( - 'users.updateOwnBasicInfo', - { authRequired: true }, - { - post() { - check(this.bodyParams, { - data: Match.ObjectIncluding({ - email: Match.Maybe(String), - name: Match.Maybe(String), - username: Match.Maybe(String), - nickname: Match.Maybe(String), - statusText: Match.Maybe(String), - currentPassword: Match.Maybe(String), - newPassword: Match.Maybe(String), - }), - customFields: Match.Maybe(Object), - }); - - const userData = { - email: this.bodyParams.data.email, - realname: this.bodyParams.data.name, - username: this.bodyParams.data.username, - nickname: this.bodyParams.data.nickname, - statusText: this.bodyParams.data.statusText, - newPassword: this.bodyParams.data.newPassword, - typedPassword: this.bodyParams.data.currentPassword, - }; - - // saveUserProfile now uses the default two factor authentication procedures, so we need to provide that - const twoFactorOptions = !userData.typedPassword - ? null - : { - twoFactorCode: userData.typedPassword, - twoFactorMethod: 'password', - }; - - Meteor.runAsUser(this.userId, () => Meteor.call('saveUserProfile', userData, this.bodyParams.customFields, twoFactorOptions)); - - return API.v1.success({ - user: Users.findOneById(this.userId, { fields: API.v1.defaultFieldsToExclude }), - }); - }, - }, -); - -API.v1.addRoute( - 'users.createToken', - { authRequired: true }, - { - post() { - const user = this.getUserFromParams(); - let data; - Meteor.runAsUser(this.userId, () => { - data = Meteor.call('createToken', user._id); - }); - return data ? API.v1.success({ data }) : API.v1.unauthorized(); - }, - }, -); - -API.v1.addRoute( - 'users.getPreferences', - { authRequired: true }, - { - get() { - const user = Users.findOneById(this.userId); - if (user.settings) { - const { preferences = {} } = user.settings; - preferences.language = user.language; - - return API.v1.success({ - preferences, - }); - } - return API.v1.failure(TAPi18n.__('Accounts_Default_User_Preferences_not_available').toUpperCase()); - }, - }, -); - -API.v1.addRoute( - 'users.setPreferences', - { authRequired: true }, - { - post() { - check(this.bodyParams, { - userId: Match.Maybe(String), - data: Match.ObjectIncluding({ - newRoomNotification: Match.Maybe(String), - newMessageNotification: Match.Maybe(String), - clockMode: Match.Maybe(Number), - useEmojis: Match.Maybe(Boolean), - convertAsciiEmoji: Match.Maybe(Boolean), - saveMobileBandwidth: Match.Maybe(Boolean), - collapseMediaByDefault: Match.Maybe(Boolean), - autoImageLoad: Match.Maybe(Boolean), - emailNotificationMode: Match.Maybe(String), - unreadAlert: Match.Maybe(Boolean), - notificationsSoundVolume: Match.Maybe(Number), - desktopNotifications: Match.Maybe(String), - pushNotifications: Match.Maybe(String), - enableAutoAway: Match.Maybe(Boolean), - highlights: Match.Maybe(Array), - desktopNotificationRequireInteraction: Match.Maybe(Boolean), - messageViewMode: Match.Maybe(Number), - showMessageInMainThread: Match.Maybe(Boolean), - hideUsernames: Match.Maybe(Boolean), - hideRoles: Match.Maybe(Boolean), - displayAvatars: Match.Maybe(Boolean), - hideFlexTab: Match.Maybe(Boolean), - sendOnEnter: Match.Maybe(String), - language: Match.Maybe(String), - sidebarShowFavorites: Match.Optional(Boolean), - sidebarShowUnread: Match.Optional(Boolean), - sidebarSortby: Match.Optional(String), - sidebarViewMode: Match.Optional(String), - sidebarDisplayAvatar: Match.Optional(Boolean), - sidebarGroupByType: Match.Optional(Boolean), - muteFocusedConversations: Match.Optional(Boolean), - }), - }); - if (this.bodyParams.userId && this.bodyParams.userId !== this.userId && !hasPermission(this.userId, 'edit-other-user-info')) { - throw new Meteor.Error('error-action-not-allowed', 'Editing user is not allowed'); - } - const userId = this.bodyParams.userId ? this.bodyParams.userId : this.userId; - if (!Users.findOneById(userId)) { - throw new Meteor.Error('error-invalid-user', 'The optional "userId" param provided does not match any users'); - } - - Meteor.runAsUser(userId, () => Meteor.call('saveUserPreferences', this.bodyParams.data)); - const user = Users.findOneById(userId, { - fields: { - 'settings.preferences': 1, - 'language': 1, - }, - }); - return API.v1.success({ - user: { - _id: user._id, - settings: { - preferences: { - ...user.settings.preferences, - language: user.language, - }, - }, - }, - }); - }, - }, -); - -API.v1.addRoute( - 'users.forgotPassword', - { authRequired: false }, - { - post() { - const { email } = this.bodyParams; - if (!email) { - return API.v1.failure("The 'email' param is required"); - } - - Meteor.call('sendForgotPasswordEmail', email); - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'users.getUsernameSuggestion', - { authRequired: true }, - { - get() { - const result = Meteor.runAsUser(this.userId, () => Meteor.call('getUsernameSuggestion')); - - return API.v1.success({ result }); - }, - }, -); - -API.v1.addRoute( - 'users.generatePersonalAccessToken', - { authRequired: true, twoFactorRequired: true }, - { - post() { - const { tokenName, bypassTwoFactor } = this.bodyParams; - if (!tokenName) { - return API.v1.failure("The 'tokenName' param is required"); - } - const token = Meteor.runAsUser(this.userId, () => Meteor.call('personalAccessTokens:generateToken', { tokenName, bypassTwoFactor })); - - return API.v1.success({ token }); - }, - }, -); - -API.v1.addRoute( - 'users.regeneratePersonalAccessToken', - { authRequired: true, twoFactorRequired: true }, - { - post() { - const { tokenName } = this.bodyParams; - if (!tokenName) { - return API.v1.failure("The 'tokenName' param is required"); - } - const token = Meteor.runAsUser(this.userId, () => Meteor.call('personalAccessTokens:regenerateToken', { tokenName })); - - return API.v1.success({ token }); - }, - }, -); - -API.v1.addRoute( - 'users.getPersonalAccessTokens', - { authRequired: true }, - { - get() { - if (!hasPermission(this.userId, 'create-personal-access-tokens')) { - throw new Meteor.Error('not-authorized', 'Not Authorized'); - } - const loginTokens = Users.getLoginTokensByUserId(this.userId).fetch()[0]; - const getPersonalAccessTokens = () => - loginTokens.services.resume.loginTokens - .filter((loginToken) => loginToken.type && loginToken.type === 'personalAccessToken') - .map((loginToken) => ({ - name: loginToken.name, - createdAt: loginToken.createdAt, - lastTokenPart: loginToken.lastTokenPart, - bypassTwoFactor: loginToken.bypassTwoFactor, - })); - - return API.v1.success({ - tokens: loginTokens ? getPersonalAccessTokens() : [], - }); - }, - }, -); - -API.v1.addRoute( - 'users.removePersonalAccessToken', - { authRequired: true, twoFactorRequired: true }, - { - post() { - const { tokenName } = this.bodyParams; - if (!tokenName) { - return API.v1.failure("The 'tokenName' param is required"); - } - Meteor.runAsUser(this.userId, () => - Meteor.call('personalAccessTokens:removeToken', { - tokenName, - }), - ); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'users.2fa.enableEmail', - { authRequired: true }, - { - post() { - Users.enableEmail2FAByUserId(this.userId); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'users.2fa.disableEmail', - { authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } }, - { - post() { - Users.disableEmail2FAByUserId(this.userId); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute('users.2fa.sendEmailCode', { - post() { - const { emailOrUsername } = this.bodyParams; - - if (!emailOrUsername) { - throw new Meteor.Error('error-parameter-required', 'emailOrUsername is required'); - } - - const method = emailOrUsername.includes('@') ? 'findOneByEmailAddress' : 'findOneByUsername'; - const userId = this.userId || Users[method](emailOrUsername, { fields: { _id: 1 } })?._id; - - if (!userId) { - this.logger.error('[2fa] User was not found when requesting 2fa email code'); - return API.v1.success(); - } - - emailCheck.sendEmailCode(getUserForCheck(userId)); - - return API.v1.success(); - }, -}); - -API.v1.addRoute( - 'users.presence', - { authRequired: true }, - { - get() { - const { from, ids } = this.queryParams; - - const options = { - fields: { - username: 1, - name: 1, - status: 1, - utcOffset: 1, - statusText: 1, - avatarETag: 1, - }, - }; - - if (ids) { - return API.v1.success({ - users: Users.findNotOfflineByIds(Array.isArray(ids) ? ids : ids.split(','), options).fetch(), - full: false, - }); - } - - if (from) { - const ts = new Date(from); - const diff = (Date.now() - ts) / 1000 / 60; - - if (diff < 10) { - return API.v1.success({ - users: Users.findNotIdUpdatedFrom(this.userId, ts, options).fetch(), - full: false, - }); - } - } - - return API.v1.success({ - users: Users.findUsersNotOffline(options).fetch(), - full: true, - }); - }, - }, -); - -API.v1.addRoute( - 'users.requestDataDownload', - { authRequired: true }, - { - get() { - const { fullExport = false } = this.queryParams; - const result = Meteor.runAsUser(this.userId, () => Meteor.call('requestDataDownload', { fullExport: fullExport === 'true' })); - - return API.v1.success({ - requested: result.requested, - exportOperation: result.exportOperation, - }); - }, - }, -); - -API.v1.addRoute( - 'users.logoutOtherClients', - { authRequired: true }, - { - async post() { - try { - const hashedToken = Accounts._hashLoginToken(this.request.headers['x-auth-token']); - - if (!(await UsersRaw.removeNonPATLoginTokensExcept(this.userId, hashedToken))) { - throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); - } - - const me = await UsersRaw.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1 } }); - - const token = me.services.resume.loginTokens.find((token) => token.hashedToken === hashedToken); - - const tokenExpires = new Date(token.when.getTime() + settings.get('Accounts_LoginExpiration') * 1000); - - return API.v1.success({ - token: this.request.headers['x-auth-token'], - tokenExpires, - }); - } catch (error) { - return API.v1.failure(error); - } - }, - }, -); - -API.v1.addRoute( - 'users.autocomplete', - { authRequired: true }, - { - get() { - const { selector } = this.queryParams; - - if (!selector) { - return API.v1.failure("The 'selector' param is required"); - } - - return API.v1.success( - Promise.await( - findUsersToAutocomplete({ - uid: this.userId, - selector: JSON.parse(selector), - }), - ), - ); - }, - }, -); - -API.v1.addRoute( - 'users.removeOtherTokens', - { authRequired: true }, - { - post() { - API.v1.success(Meteor.call('removeOtherTokens')); - }, - }, -); - -API.v1.addRoute( - 'users.resetE2EKey', - { authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } }, - { - post() { - // reset own keys - if (this.isUserFromParams()) { - resetUserE2EEncriptionKey(this.userId, false); - return API.v1.success(); - } - - // reset other user keys - const user = this.getUserFromParams(); - if (!user) { - throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); - } - - if (!settings.get('Accounts_TwoFactorAuthentication_Enforce_Password_Fallback')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } - - if (!hasPermission(Meteor.userId(), 'edit-other-user-e2ee')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } - - if (!resetUserE2EEncriptionKey(user._id, true)) { - return API.v1.failure(); - } - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'users.resetTOTP', - { authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } }, - { - post() { - // reset own keys - if (this.isUserFromParams()) { - Promise.await(resetTOTP(this.userId, false)); - return API.v1.success(); - } - - // reset other user keys - const user = this.getUserFromParams(); - if (!user) { - throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); - } - - if (!settings.get('Accounts_TwoFactorAuthentication_Enforce_Password_Fallback')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } - - if (!hasPermission(Meteor.userId(), 'edit-other-user-totp')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } - - Promise.await(resetTOTP(user._id, true)); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'users.listTeams', - { authRequired: true }, - { - get() { - check( - this.queryParams, - Match.ObjectIncluding({ - userId: Match.Maybe(String), - }), - ); - const { userId } = this.queryParams; - - if (!userId) { - throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); - } - - // If the caller has permission to view all teams, there's no need to filter the teams - const adminId = hasPermission(this.userId, 'view-all-teams') ? undefined : this.userId; - - const teams = Promise.await(Team.findBySubscribedUserIds(userId, adminId)); - - return API.v1.success({ - teams, - }); - }, - }, -); - -API.v1.addRoute( - 'users.logout', - { authRequired: true }, - { - post() { - const userId = this.bodyParams.userId || this.userId; - - if (userId !== this.userId && !hasPermission(this.userId, 'logout-other-user')) { - return API.v1.unauthorized(); - } - - // this method logs the user out automatically, if successful returns 1, otherwise 0 - if (!Users.unsetLoginTokens(userId)) { - throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); - } - - return API.v1.success({ - message: `User ${userId} has been logged out!`, - }); - }, - }, -); - -settings.watch('Rate_Limiter_Limit_RegisterUser', (value) => { - const userRegisterRoute = '/api/v1/users.registerpost'; - - API.v1.updateRateLimiterDictionaryForRoute(userRegisterRoute, value); -}); diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts new file mode 100644 index 000000000000..944f33dce8d6 --- /dev/null +++ b/apps/meteor/app/api/server/v1/users.ts @@ -0,0 +1,1078 @@ +import { + isUserCreateParamsPOST, + isUserSetActiveStatusParamsPOST, + isUserDeactivateIdleParamsPOST, + isUsersInfoParamsGetProps, + isUserRegisterParamsPOST, + isUserLogoutParamsPOST, + isUsersListTeamsProps, + isUsersAutocompleteProps, + isUsersSetAvatarProps, + isUsersUpdateParamsPOST, + isUsersUpdateOwnBasicInfoParamsPOST, + isUsersSetPreferencesParamsPOST, +} from '@rocket.chat/rest-typings'; +import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; +import { Match, check } from 'meteor/check'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { IExportOperation, IPersonalAccessToken, IUser } from '@rocket.chat/core-typings'; +import { Users as UsersRaw } from '@rocket.chat/models'; + +import { Users, Subscriptions } from '../../../models/server'; +import { hasPermission } from '../../../authorization/server'; +import { settings } from '../../../settings/server'; +import { + validateCustomFields, + saveUser, + saveCustomFieldsWithoutValidation, + checkUsernameAvailability, + setStatusText, + setUserAvatar, + saveCustomFields, +} from '../../../lib/server'; +import { getFullUserDataByIdOrUsername } from '../../../lib/server/functions/getFullUserData'; +import { API } from '../api'; +import { findUsersToAutocomplete, getInclusiveFields, getNonEmptyFields, getNonEmptyQuery } from '../lib/users'; +import { getUserForCheck, emailCheck } from '../../../2fa/server/code'; +import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey'; +import { resetTOTP } from '../../../2fa/server/functions/resetTOTP'; +import { Team } from '../../../../server/sdk'; +import { isValidQuery } from '../lib/isValidQuery'; +import { setUserStatus } from '../../../../imports/users-presence/server/activeUsers'; +import { getURL } from '../../../utils/server'; +import { getUploadFormData } from '../lib/getUploadFormData'; + +API.v1.addRoute( + 'users.getAvatar', + { authRequired: false }, + { + get() { + const user = this.getUserFromParams(); + + const url = getURL(`/avatar/${user.username}`, { cdn: false, full: true }); + this.response.setHeader('Location', url); + + return { + statusCode: 307, + body: url, + }; + }, + }, +); + +API.v1.addRoute( + 'users.update', + { authRequired: true, twoFactorRequired: true, validateParams: isUsersUpdateParamsPOST }, + { + post() { + const userData = { _id: this.bodyParams.userId, ...this.bodyParams.data }; + + Meteor.runAsUser(this.userId, () => saveUser(this.userId, userData)); + + if (this.bodyParams.data.customFields) { + saveCustomFields(this.bodyParams.userId, this.bodyParams.data.customFields); + } + + if (typeof this.bodyParams.data.active !== 'undefined') { + const { + userId, + data: { active }, + confirmRelinquish, + } = this.bodyParams; + + Meteor.call('setUserActiveStatus', userId, active, Boolean(confirmRelinquish)); + } + const { fields } = this.parseJsonQuery(); + + return API.v1.success({ user: Users.findOneById(this.bodyParams.userId, { fields }) }); + }, + }, +); + +API.v1.addRoute( + 'users.updateOwnBasicInfo', + { authRequired: true, validateParams: isUsersUpdateOwnBasicInfoParamsPOST }, + { + post() { + const userData = { + email: this.bodyParams.data.email, + realname: this.bodyParams.data.name, + username: this.bodyParams.data.username, + nickname: this.bodyParams.data.nickname, + statusText: this.bodyParams.data.statusText, + newPassword: this.bodyParams.data.newPassword, + typedPassword: this.bodyParams.data.currentPassword, + }; + + // saveUserProfile now uses the default two factor authentication procedures, so we need to provide that + const twoFactorOptions = !userData.typedPassword + ? null + : { + twoFactorCode: userData.typedPassword, + twoFactorMethod: 'password', + }; + + Meteor.call('saveUserProfile', userData, this.bodyParams.customFields, twoFactorOptions); + + return API.v1.success({ + user: Users.findOneById(this.userId, { fields: API.v1.defaultFieldsToExclude }), + }); + }, + }, +); + +API.v1.addRoute( + 'users.setPreferences', + { authRequired: true, validateParams: isUsersSetPreferencesParamsPOST }, + { + post() { + if (this.bodyParams.userId && this.bodyParams.userId !== this.userId && !hasPermission(this.userId, 'edit-other-user-info')) { + throw new Meteor.Error('error-action-not-allowed', 'Editing user is not allowed'); + } + const userId = this.bodyParams.userId ? this.bodyParams.userId : this.userId; + if (!Users.findOneById(userId)) { + throw new Meteor.Error('error-invalid-user', 'The optional "userId" param provided does not match any users'); + } + + Meteor.runAsUser(userId, () => Meteor.call('saveUserPreferences', this.bodyParams.data)); + const user = Users.findOneById(userId, { + fields: { + 'settings.preferences': 1, + 'language': 1, + }, + }); + return API.v1.success({ + user: { + _id: user._id, + settings: { + preferences: { + ...user.settings.preferences, + language: user.language, + }, + }, + }, + }); + }, + }, +); + +API.v1.addRoute( + 'users.setAvatar', + { authRequired: true, validateParams: isUsersSetAvatarProps }, + { + async post() { + const canEditOtherUserAvatar = hasPermission(this.userId, 'edit-other-user-avatar'); + + if (!settings.get('Accounts_AllowUserAvatarChange') && !canEditOtherUserAvatar) { + throw new Meteor.Error('error-not-allowed', 'Change avatar is not allowed', { + method: 'users.setAvatar', + }); + } + + let user = ((): IUser | undefined => { + if (this.isUserFromParams()) { + return Meteor.users.findOne(this.userId) as IUser | undefined; + } + if (canEditOtherUserAvatar) { + return this.getUserFromParams(); + } + })(); + + if (!user) { + return API.v1.unauthorized(); + } + + if (this.bodyParams.avatarUrl) { + setUserAvatar(user, this.bodyParams.avatarUrl, '', 'url'); + return API.v1.success(); + } + + const [image, fields] = await getUploadFormData( + { + request: this.request, + }, + { + field: 'image', + }, + ); + + if (!image) { + return API.v1.failure("The 'image' param is required"); + } + + const sentTheUserByFormData = fields.userId || fields.username; + if (sentTheUserByFormData) { + if (fields.userId) { + user = Users.findOneById(fields.userId, { fields: { username: 1 } }); + } else if (fields.username) { + user = Users.findOneByUsernameIgnoringCase(fields.username, { fields: { username: 1 } }); + } + + if (!user) { + throw new Meteor.Error('error-invalid-user', 'The optional "userId" or "username" param provided does not match any users'); + } + + const isAnotherUser = this.userId !== user._id; + if (isAnotherUser && !hasPermission(this.userId, 'edit-other-user-avatar')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } + } + + setUserAvatar(user, image.fileBuffer, image.mimetype, 'rest'); + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'users.create', + { authRequired: true, validateParams: isUserCreateParamsPOST }, + { + post() { + // New change made by pull request #5152 + if (typeof this.bodyParams.joinDefaultChannels === 'undefined') { + this.bodyParams.joinDefaultChannels = true; + } + + if (this.bodyParams.customFields) { + validateCustomFields(this.bodyParams.customFields); + } + + const newUserId = saveUser(this.userId, this.bodyParams); + + if (this.bodyParams.customFields) { + saveCustomFieldsWithoutValidation(newUserId, this.bodyParams.customFields); + } + + if (typeof this.bodyParams.active !== 'undefined') { + Meteor.call('setUserActiveStatus', newUserId, this.bodyParams.active); + } + + const { fields } = this.parseJsonQuery(); + + return API.v1.success({ user: Users.findOneById(newUserId, { fields }) }); + }, + }, +); + +API.v1.addRoute( + 'users.delete', + { authRequired: true }, + { + post() { + if (!hasPermission(this.userId, 'delete-user')) { + return API.v1.unauthorized(); + } + + const user = this.getUserFromParams(); + const { confirmRelinquish = false } = this.requestParams(); + + Meteor.call('deleteUser', user._id, confirmRelinquish); + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'users.deleteOwnAccount', + { authRequired: true }, + { + post() { + const { password } = this.bodyParams; + if (!password) { + return API.v1.failure('Body parameter "password" is required.'); + } + if (!settings.get('Accounts_AllowDeleteOwnAccount')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } + + const { confirmRelinquish = false } = this.requestParams(); + + Meteor.call('deleteUserOwnAccount', password, confirmRelinquish); + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'users.setActiveStatus', + { authRequired: true, validateParams: isUserSetActiveStatusParamsPOST }, + { + post() { + if (!hasPermission(this.userId, 'edit-other-user-active-status')) { + return API.v1.unauthorized(); + } + + const { userId, activeStatus, confirmRelinquish = false } = this.bodyParams; + Meteor.call('setUserActiveStatus', userId, activeStatus, confirmRelinquish); + return API.v1.success({ + user: Users.findOneById(this.bodyParams.userId, { fields: { active: 1 } }), + }); + }, + }, +); + +API.v1.addRoute( + 'users.deactivateIdle', + { authRequired: true, validateParams: isUserDeactivateIdleParamsPOST }, + { + post() { + if (!hasPermission(this.userId, 'edit-other-user-active-status')) { + return API.v1.unauthorized(); + } + + const { daysIdle, role = 'user' } = this.bodyParams; + + const lastLoggedIn = new Date(); + lastLoggedIn.setDate(lastLoggedIn.getDate() - daysIdle); + + const count = Users.setActiveNotLoggedInAfterWithRole(lastLoggedIn, role, false); + + return API.v1.success({ + count, + }); + }, + }, +); + +API.v1.addRoute( + 'users.info', + { authRequired: true, validateParams: isUsersInfoParamsGetProps }, + { + async get() { + const { fields } = this.parseJsonQuery(); + + const user = await getFullUserDataByIdOrUsername(this.userId, { + filterId: (this.queryParams as any).userId, + filterUsername: (this.queryParams as any).username, + }); + + if (!user) { + return API.v1.failure('User not found.'); + } + const myself = user._id === this.userId; + if (fields.userRooms === 1 && (myself || hasPermission(this.userId, 'view-other-user-channels'))) { + return API.v1.success({ + user: { + ...user, + rooms: Subscriptions.findByUserId(user._id, { + projection: { + rid: 1, + name: 1, + t: 1, + roles: 1, + unread: 1, + federated: 1, + }, + sort: { + t: 1, + name: 1, + }, + }).fetch(), + }, + }); + } + + return API.v1.success({ + user, + }); + }, + }, +); + +API.v1.addRoute( + 'users.list', + { + authRequired: true, + queryOperations: ['$or', '$and'], + }, + { + async get() { + if (!hasPermission(this.userId, 'view-d-room')) { + return API.v1.unauthorized(); + } + + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const nonEmptyQuery = getNonEmptyQuery(query, hasPermission(this.userId, 'view-full-other-user-info')); + const nonEmptyFields = getNonEmptyFields(fields); + + const inclusiveFields = getInclusiveFields(nonEmptyFields); + + const inclusiveFieldsKeys = Object.keys(inclusiveFields); + + if ( + !isValidQuery( + nonEmptyQuery, + [ + ...inclusiveFieldsKeys, + inclusiveFieldsKeys.includes('emails') && 'emails.address.*', + inclusiveFieldsKeys.includes('username') && 'username.*', + inclusiveFieldsKeys.includes('name') && 'name.*', + inclusiveFieldsKeys.includes('type') && 'type.*', + ].filter(Boolean) as string[], + this.queryOperations, + ) + ) { + throw new Meteor.Error('error-invalid-query', isValidQuery.errors.join('\n')); + } + + const actualSort = sort?.name ? { nameInsensitive: sort.name, ...sort } : sort || { username: 1 }; + + const limit = + count !== 0 + ? [ + { + $limit: count, + }, + ] + : []; + + const result = await UsersRaw.col + .aggregate<{ sortedResults: IUser[]; totalCount: { total: number }[] }>([ + { + $match: nonEmptyQuery, + }, + { + $project: inclusiveFields, + }, + { + $addFields: { + nameInsensitive: { + $toLower: '$name', + }, + }, + }, + { + $facet: { + sortedResults: [ + { + $sort: actualSort, + }, + { + $skip: offset, + }, + ...limit, + ], + totalCount: [{ $group: { _id: null, total: { $sum: 1 } } }], + }, + }, + ]) + .toArray(); + + const { + sortedResults: users, + totalCount: [{ total } = { total: 0 }], + } = result[0]; + + return API.v1.success({ + users, + count: users.length, + offset, + total, + }); + }, + }, +); + +API.v1.addRoute( + 'users.register', + { + authRequired: false, + rateLimiterOptions: { + numRequestsAllowed: settings.get('Rate_Limiter_Limit_RegisterUser') ?? 1, + intervalTimeInMS: settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'), + }, + validateParams: isUserRegisterParamsPOST, + }, + { + post() { + if (this.userId) { + return API.v1.failure('Logged in users can not register again.'); + } + + if (!checkUsernameAvailability(this.bodyParams.username)) { + return API.v1.failure('Username is already in use'); + } + + // Register the user + const userId = Meteor.call('registerUser', this.bodyParams); + + // Now set their username + Meteor.runAsUser(userId, () => Meteor.call('setUsername', this.bodyParams.username)); + const { fields } = this.parseJsonQuery(); + + return API.v1.success({ user: Users.findOneById(userId, { fields }) }); + }, + }, +); + +API.v1.addRoute( + 'users.resetAvatar', + { authRequired: true }, + { + post() { + const user = this.getUserFromParams(); + + if (settings.get('Accounts_AllowUserAvatarChange') && user._id === this.userId) { + Meteor.runAsUser(this.userId, () => Meteor.call('resetAvatar')); + } else if (hasPermission(this.userId, 'edit-other-user-avatar')) { + Meteor.runAsUser(this.userId, () => Meteor.call('resetAvatar', user._id)); + } else { + throw new Meteor.Error('error-not-allowed', 'Reset avatar is not allowed', { + method: 'users.resetAvatar', + }); + } + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'users.createToken', + { authRequired: true }, + { + post() { + const user = this.getUserFromParams(); + const data = Meteor.call('createToken', user._id); + return data ? API.v1.success({ data }) : API.v1.unauthorized(); + }, + }, +); + +API.v1.addRoute( + 'users.getPreferences', + { authRequired: true }, + { + get() { + const user = Users.findOneById(this.userId); + if (user.settings) { + const { preferences = {} } = user.settings; + preferences.language = user.language; + + return API.v1.success({ + preferences, + }); + } + return API.v1.failure(TAPi18n.__('Accounts_Default_User_Preferences_not_available').toUpperCase()); + }, + }, +); + +API.v1.addRoute( + 'users.forgotPassword', + { authRequired: false }, + { + post() { + const { email } = this.bodyParams; + if (!email) { + return API.v1.failure("The 'email' param is required"); + } + + Meteor.call('sendForgotPasswordEmail', email); + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'users.getUsernameSuggestion', + { authRequired: true }, + { + get() { + const result = Meteor.call('getUsernameSuggestion'); + + return API.v1.success({ result }); + }, + }, +); + +API.v1.addRoute( + 'users.generatePersonalAccessToken', + { authRequired: true, twoFactorRequired: true }, + { + post() { + const { tokenName, bypassTwoFactor } = this.bodyParams; + if (!tokenName) { + return API.v1.failure("The 'tokenName' param is required"); + } + const token = Meteor.call('personalAccessTokens:generateToken', { tokenName, bypassTwoFactor }); + + return API.v1.success({ token }); + }, + }, +); + +API.v1.addRoute( + 'users.regeneratePersonalAccessToken', + { authRequired: true, twoFactorRequired: true }, + { + post() { + const { tokenName } = this.bodyParams; + if (!tokenName) { + return API.v1.failure("The 'tokenName' param is required"); + } + const token = Meteor.call('personalAccessTokens:regenerateToken', { tokenName }); + + return API.v1.success({ token }); + }, + }, +); + +API.v1.addRoute( + 'users.getPersonalAccessTokens', + { authRequired: true }, + { + get() { + if (!hasPermission(this.userId, 'create-personal-access-tokens')) { + throw new Meteor.Error('not-authorized', 'Not Authorized'); + } + + const user = Users.getLoginTokensByUserId(this.userId).fetch()[0] as IUser | undefined; + + return API.v1.success({ + tokens: + user?.services?.resume?.loginTokens + ?.filter((loginToken: any) => loginToken.type === 'personalAccessToken') + .map((loginToken: IPersonalAccessToken) => ({ + name: loginToken.name, + createdAt: loginToken.createdAt.toISOString(), + lastTokenPart: loginToken.lastTokenPart, + bypassTwoFactor: Boolean(loginToken.bypassTwoFactor), + })) || [], + }); + }, + }, +); + +API.v1.addRoute( + 'users.removePersonalAccessToken', + { authRequired: true, twoFactorRequired: true }, + { + post() { + const { tokenName } = this.bodyParams; + if (!tokenName) { + return API.v1.failure("The 'tokenName' param is required"); + } + Meteor.call('personalAccessTokens:removeToken', { + tokenName, + }); + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'users.2fa.enableEmail', + { authRequired: true }, + { + post() { + Users.enableEmail2FAByUserId(this.userId); + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'users.2fa.disableEmail', + { authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } }, + { + post() { + Users.disableEmail2FAByUserId(this.userId); + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute('users.2fa.sendEmailCode', { + post() { + const { emailOrUsername } = this.bodyParams; + + if (!emailOrUsername) { + throw new Meteor.Error('error-parameter-required', 'emailOrUsername is required'); + } + + const method = emailOrUsername.includes('@') ? 'findOneByEmailAddress' : 'findOneByUsername'; + const userId = this.userId || Users[method](emailOrUsername, { fields: { _id: 1 } })?._id; + + if (!userId) { + // this.logger.error('[2fa] User was not found when requesting 2fa email code'); + return API.v1.success(); + } + + emailCheck.sendEmailCode(getUserForCheck(userId)); + + return API.v1.success(); + }, +}); + +API.v1.addRoute( + 'users.presence', + { authRequired: true }, + { + get() { + const { from, ids } = this.queryParams; + + const options = { + fields: { + username: 1, + name: 1, + status: 1, + utcOffset: 1, + statusText: 1, + avatarETag: 1, + }, + }; + + if (ids) { + return API.v1.success({ + users: Users.findNotOfflineByIds(Array.isArray(ids) ? ids : ids.split(','), options).fetch(), + full: false, + }); + } + + if (from) { + const ts = new Date(from); + const diff = (Date.now() - Number(ts)) / 1000 / 60; + + if (diff < 10) { + return API.v1.success({ + users: Users.findNotIdUpdatedFrom(this.userId, ts, options).fetch(), + full: false, + }); + } + } + + return API.v1.success({ + users: Users.findUsersNotOffline(options).fetch(), + full: true, + }); + }, + }, +); + +API.v1.addRoute( + 'users.requestDataDownload', + { authRequired: true }, + { + get() { + const { fullExport = false } = this.queryParams; + const result = Meteor.call('requestDataDownload', { fullExport: fullExport === 'true' }) as { + requested: boolean; + exportOperation: IExportOperation; + }; + + return API.v1.success({ + requested: Boolean(result.requested), + exportOperation: result.exportOperation, + }); + }, + }, +); + +API.v1.addRoute( + 'users.logoutOtherClients', + { authRequired: true }, + { + async post() { + const xAuthToken = this.request.headers['x-auth-token'] as string; + + if (!xAuthToken) { + throw new Meteor.Error('error-parameter-required', 'x-auth-token is required'); + } + const hashedToken = Accounts._hashLoginToken(xAuthToken); + + if (!(await UsersRaw.removeNonPATLoginTokensExcept(this.userId, hashedToken))) { + throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); + } + + const me = (await UsersRaw.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1 } })) as Pick; + + const token = me.services?.resume?.loginTokens?.find((token) => token.hashedToken === hashedToken); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const tokenExpires = new Date(token!.when.getTime() + settings.get('Accounts_LoginExpiration') * 1000); + + return API.v1.success({ + token: xAuthToken, + tokenExpires: tokenExpires.toISOString() || '', + }); + }, + }, +); + +API.v1.addRoute( + 'users.autocomplete', + { authRequired: true, validateParams: isUsersAutocompleteProps }, + { + async get() { + const { selector } = this.queryParams; + return API.v1.success( + await findUsersToAutocomplete({ + uid: this.userId, + selector: JSON.parse(selector), + }), + ); + }, + }, +); + +API.v1.addRoute( + 'users.removeOtherTokens', + { authRequired: true }, + { + post() { + return API.v1.success(Meteor.call('removeOtherTokens')); + }, + }, +); + +API.v1.addRoute( + 'users.resetE2EKey', + { authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } }, + { + post() { + if ('userId' in this.bodyParams || 'username' in this.bodyParams || 'user' in this.bodyParams) { + // reset other user keys + const user = this.getUserFromParams(); + if (!user) { + throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); + } + + if (!settings.get('Accounts_TwoFactorAuthentication_Enforce_Password_Fallback')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } + + if (!hasPermission(this.userId, 'edit-other-user-e2ee')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } + + if (!resetUserE2EEncriptionKey(user._id, true)) { + return API.v1.failure(); + } + + return API.v1.success(); + } + resetUserE2EEncriptionKey(this.userId, false); + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'users.resetTOTP', + { authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } }, + { + async post() { + // // reset own keys + if ('userId' in this.bodyParams || 'username' in this.bodyParams || 'user' in this.bodyParams) { + // reset other user keys + if (!hasPermission(this.userId, 'edit-other-user-totp')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } + + if (!settings.get('Accounts_TwoFactorAuthentication_Enforce_Password_Fallback')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } + + const user = this.getUserFromParams(); + if (!user) { + throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); + } + + await resetTOTP(user._id, true); + + return API.v1.success(); + } + await resetTOTP(this.userId, false); + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'users.listTeams', + { authRequired: true, validateParams: isUsersListTeamsProps }, + { + async get() { + check( + this.queryParams, + Match.ObjectIncluding({ + userId: Match.Maybe(String), + }), + ); + + const { userId } = this.queryParams; + + // If the caller has permission to view all teams, there's no need to filter the teams + const adminId = hasPermission(this.userId, 'view-all-teams') ? undefined : this.userId; + + const teams = await Team.findBySubscribedUserIds(userId, adminId); + + return API.v1.success({ + teams, + }); + }, + }, +); + +API.v1.addRoute( + 'users.logout', + { authRequired: true, validateParams: isUserLogoutParamsPOST }, + { + post() { + const userId = this.bodyParams.userId || this.userId; + + if (userId !== this.userId && !hasPermission(this.userId, 'logout-other-user')) { + return API.v1.unauthorized(); + } + + // this method logs the user out automatically, if successful returns 1, otherwise 0 + if (!Users.unsetLoginTokens(userId)) { + throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); + } + + return API.v1.success({ + message: `User ${userId} has been logged out!`, + }); + }, + }, +); + +API.v1.addRoute( + 'users.getPresence', + { authRequired: true }, + { + get() { + if (this.isUserFromParams()) { + const user = Users.findOneById(this.userId); + return API.v1.success({ + presence: user.status || 'offline', + connectionStatus: user.statusConnection || 'offline', + ...(user.lastLogin && { lastLogin: user.lastLogin }), + }); + } + + const user = this.getUserFromParams(); + + return API.v1.success({ + presence: user.status || 'offline', + }); + }, + }, +); + +API.v1.addRoute( + 'users.setStatus', + { authRequired: true }, + { + post() { + check( + this.bodyParams, + Match.ObjectIncluding({ + status: Match.Maybe(String), + message: Match.Maybe(String), + }), + ); + + if (!settings.get('Accounts_AllowUserStatusMessageChange')) { + throw new Meteor.Error('error-not-allowed', 'Change status is not allowed', { + method: 'users.setStatus', + }); + } + + const user = ((): IUser | undefined => { + if (this.isUserFromParams()) { + return Meteor.users.findOne(this.userId) as IUser; + } + if (hasPermission(this.userId, 'edit-other-user-info')) { + return this.getUserFromParams(); + } + })(); + + if (user === undefined) { + return API.v1.unauthorized(); + } + + Meteor.runAsUser(user._id, () => { + if (this.bodyParams.message || this.bodyParams.message === '') { + setStatusText(user._id, this.bodyParams.message); + } + if (this.bodyParams.status) { + const validStatus = ['online', 'away', 'offline', 'busy']; + if (validStatus.includes(this.bodyParams.status)) { + const { status } = this.bodyParams; + + if (status === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) { + throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { + method: 'users.setStatus', + }); + } + + Meteor.users.update(user._id, { + $set: { + status, + statusDefault: status, + }, + }); + + setUserStatus(user, status); + } else { + throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', { + method: 'users.setStatus', + }); + } + } + }); + + return API.v1.success(); + }, + }, +); + +// status: 'online' | 'offline' | 'away' | 'busy'; +// message?: string; +// _id: string; +// connectionStatus?: 'online' | 'offline' | 'away' | 'busy'; +// }; + +API.v1.addRoute( + 'users.getStatus', + { authRequired: true }, + { + get() { + if (this.isUserFromParams()) { + const user = Users.findOneById(this.userId); + return API.v1.success({ + _id: user._id, + // message: user.statusText, + connectionStatus: (user.statusConnection || 'offline') as 'online' | 'offline' | 'away' | 'busy', + status: (user.status || 'offline') as 'online' | 'offline' | 'away' | 'busy', + }); + } + + const user = this.getUserFromParams(); + + return API.v1.success({ + _id: user._id, + // message: user.statusText, + status: (user.status || 'offline') as 'online' | 'offline' | 'away' | 'busy', + }); + }, + }, +); + +settings.watch('Rate_Limiter_Limit_RegisterUser', (value) => { + const userRegisterRoute = '/api/v1/users.registerpost'; + + API.v1.updateRateLimiterDictionaryForRoute(userRegisterRoute, value); +}); diff --git a/apps/meteor/app/api/server/v1/videoConference.ts b/apps/meteor/app/api/server/v1/videoConference.ts index 34a78fdf09da..f471fcaebe40 100644 --- a/apps/meteor/app/api/server/v1/videoConference.ts +++ b/apps/meteor/app/api/server/v1/videoConference.ts @@ -1,25 +1,185 @@ -import { Meteor } from 'meteor/meteor'; +import type { VideoConference } from '@rocket.chat/core-typings'; +import { + isVideoConfStartProps, + isVideoConfJoinProps, + isVideoConfCancelProps, + isVideoConfInfoProps, + isVideoConfListProps, +} from '@rocket.chat/rest-typings'; -import { Rooms } from '../../../models/server'; import { API } from '../api'; +import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { VideoConf } from '../../../../server/sdk'; +import { videoConfProviders } from '../../../../server/lib/videoConfProviders'; +import { availabilityErrors } from '../../../../lib/videoConference/constants'; API.v1.addRoute( - 'video-conference/jitsi.update-timeout', - { authRequired: true }, + 'video-conference.start', + { authRequired: true, validateParams: isVideoConfStartProps, rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 60000 } }, { - post() { - const { roomId, joiningNow = true } = this.bodyParams; - if (!roomId) { - return API.v1.failure('The "roomId" parameter is required!'); + async post() { + const { roomId, title, allowRinging: requestRinging } = this.bodyParams; + const { userId } = this; + if (!userId || !(await canAccessRoomIdAsync(roomId, userId))) { + return API.v1.failure('invalid-params'); } - const room = Rooms.findOneById(roomId, { fields: { _id: 1 } }); - if (!room) { - return API.v1.failure('Room does not exist!'); + try { + const providerName = videoConfProviders.getActiveProvider(); + + if (!providerName) { + throw new Error(availabilityErrors.NOT_ACTIVE); + } + + const allowRinging = Boolean(requestRinging) && (await hasPermissionAsync(userId, 'videoconf-ring-users')); + + return API.v1.success({ + data: { + ...(await VideoConf.start(userId, roomId, { title, allowRinging })), + providerName, + }, + }); + } catch (e) { + return API.v1.failure(await VideoConf.diagnoseProvider(userId, roomId)); + } + }, + }, +); + +API.v1.addRoute( + 'video-conference.join', + { authOrAnonRequired: true, validateParams: isVideoConfJoinProps, rateLimiterOptions: { numRequestsAllowed: 2, intervalTimeInMS: 5000 } }, + { + async post() { + const { callId, state } = this.bodyParams; + const { userId } = this; + + const call = await VideoConf.get(callId); + if (!call) { + return API.v1.failure('invalid-params'); + } + + if (!(await canAccessRoomIdAsync(call.rid, userId))) { + return API.v1.failure('invalid-params'); + } + + let url: string | undefined; + + try { + url = await VideoConf.join(userId, callId, { + ...(state?.cam !== undefined ? { cam: state.cam } : {}), + ...(state?.mic !== undefined ? { mic: state.mic } : {}), + }); + } catch (e) { + if (userId) { + return API.v1.failure(await VideoConf.diagnoseProvider(userId, call.rid, call.providerName)); + } + } + + if (!url) { + return API.v1.failure('failed-to-get-url'); + } + + return API.v1.success({ + url, + providerName: call.providerName, + }); + }, + }, +); + +API.v1.addRoute( + 'video-conference.cancel', + { authRequired: true, validateParams: isVideoConfCancelProps, rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 60000 } }, + { + async post() { + const { callId } = this.bodyParams; + const { userId } = this; + + const call = await VideoConf.get(callId); + if (!call) { + return API.v1.failure('invalid-params'); + } + + if (!userId || !(await canAccessRoomIdAsync(call.rid, userId))) { + return API.v1.failure('invalid-params'); + } + + await VideoConf.cancel(userId, callId); + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'video-conference.info', + { authRequired: true, validateParams: isVideoConfInfoProps, rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 1000 } }, + { + async get() { + const { callId } = this.queryParams; + const { userId } = this; + + const call = await VideoConf.get(callId); + if (!call) { + return API.v1.failure('invalid-params'); } - const jitsiTimeout = Meteor.runAsUser(this.userId, () => Meteor.call('jitsi:updateTimeout', roomId, Boolean(joiningNow))); - return API.v1.success({ jitsiTimeout }); + if (!userId || !(await canAccessRoomIdAsync(call.rid, userId))) { + return API.v1.failure('invalid-params'); + } + + const capabilities = await VideoConf.listProviderCapabilities(call.providerName); + + return API.v1.success({ + ...(call as VideoConference), + capabilities, + }); + }, + }, +); + +API.v1.addRoute( + 'video-conference.list', + { authRequired: true, validateParams: isVideoConfListProps, rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 1000 } }, + { + async get() { + const { roomId } = this.queryParams; + const { userId } = this; + + const { offset, count } = this.getPaginationItems(); + + if (!userId || !(await canAccessRoomIdAsync(roomId, userId))) { + return API.v1.failure('invalid-params'); + } + + const data = await VideoConf.list(roomId, { offset, count }); + + return API.v1.success(data); + }, + }, +); + +API.v1.addRoute( + 'video-conference.providers', + { authRequired: true, rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 1000 } }, + { + async get() { + const data = await VideoConf.listProviders(); + + return API.v1.success({ data }); + }, + }, +); + +API.v1.addRoute( + 'video-conference.capabilities', + { authRequired: true, rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 1000 } }, + { + async get() { + const data = await VideoConf.listCapabilities(); + + return API.v1.success(data); }, }, ); diff --git a/apps/meteor/app/api/server/v1/voip/events.ts b/apps/meteor/app/api/server/v1/voip/events.ts index 13648aff6e64..5b3d61a46f6d 100644 --- a/apps/meteor/app/api/server/v1/voip/events.ts +++ b/apps/meteor/app/api/server/v1/voip/events.ts @@ -1,10 +1,10 @@ import { Match, check } from 'meteor/check'; import { VoipClientEvents } from '@rocket.chat/core-typings'; +import { VoipRoom } from '@rocket.chat/models'; import { API } from '../../api'; import { LivechatVoip } from '../../../../../server/sdk'; import { canAccessRoom } from '../../../../authorization/server'; -import { VoipRoom } from '../../../../models/server/raw'; API.v1.addRoute( 'voip/events', diff --git a/apps/meteor/app/api/server/v1/voip/extensions.ts b/apps/meteor/app/api/server/v1/voip/extensions.ts index 7d23a991fefd..598a8bce662e 100644 --- a/apps/meteor/app/api/server/v1/voip/extensions.ts +++ b/apps/meteor/app/api/server/v1/voip/extensions.ts @@ -1,8 +1,8 @@ import { Match, check } from 'meteor/check'; import type { IVoipExtensionBase } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; import { API } from '../../api'; -import { Users } from '../../../../models/server/raw/index'; import { Voip } from '../../../../../server/sdk'; import { generateJWT } from '../../../../utils/server/lib/JWTHelper'; import { settings } from '../../../../settings/server'; diff --git a/apps/meteor/app/api/server/v1/voip/omnichannel.ts b/apps/meteor/app/api/server/v1/voip/omnichannel.ts index 0a1d614e6855..28ef3608be67 100644 --- a/apps/meteor/app/api/server/v1/voip/omnichannel.ts +++ b/apps/meteor/app/api/server/v1/voip/omnichannel.ts @@ -1,8 +1,8 @@ import { Match, check } from 'meteor/check'; import { IUser, IVoipExtensionWithAgentInfo } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; import { API } from '../../api'; -import { Users } from '../../../../models/server/raw/index'; import { hasPermission } from '../../../../authorization/server/index'; import { LivechatVoip } from '../../../../../server/sdk'; import { logger } from './logger'; @@ -33,38 +33,6 @@ API.v1.addRoute( 'omnichannel/agent/extension', { authRequired: true }, { - // Get the extensions associated with the agent passed as request params. - async get() { - if (!hasPermission(this.userId, 'view-agent-extension-association')) { - return API.v1.unauthorized(); - } - check( - this.requestParams(), - Match.ObjectIncluding({ - username: String, - }), - ); - const { username } = this.requestParams(); - const user = await Users.findOneByAgentUsername(username, { - projection: { _id: 1 }, - }); - if (!user) { - return API.v1.notFound('User not found'); - } - const extension = await Users.getVoipExtensionByUserId(user._id, { - projection: { - _id: 1, - username: 1, - extension: 1, - }, - }); - if (!extension) { - return API.v1.notFound('Extension not found'); - } - return API.v1.success({ extension }); - }, - - // Create agent-extension association. async post() { if (!hasPermission(this.userId, 'manage-agent-extension-association')) { return API.v1.unauthorized(); @@ -109,7 +77,7 @@ API.v1.addRoute( } if (!user) { - return API.v1.notFound(); + return API.v1.notFound('User not found or does not have livechat-agent role'); } try { @@ -121,18 +89,55 @@ API.v1.addRoute( return API.v1.failure(`extension already in use ${extension}`); } }, + }, +); + +API.v1.addRoute( + 'omnichannel/agent/extension/:username', + { authRequired: true }, + { + // Get the extensions associated with the agent passed as request params. + async get() { + if (!hasPermission(this.userId, 'view-agent-extension-association')) { + return API.v1.unauthorized(); + } + check( + this.urlParams, + Match.ObjectIncluding({ + username: String, + }), + ); + const { username } = this.urlParams; + const user = await Users.findOneByAgentUsername(username, { + projection: { _id: 1 }, + }); + if (!user) { + return API.v1.notFound('User not found'); + } + const extension = await Users.getVoipExtensionByUserId(user._id, { + projection: { + _id: 1, + username: 1, + extension: 1, + }, + }); + if (!extension) { + return API.v1.notFound('Extension not found'); + } + return API.v1.success({ extension }); + }, async delete() { if (!hasPermission(this.userId, 'manage-agent-extension-association')) { return API.v1.unauthorized(); } check( - this.requestParams(), + this.urlParams, Match.ObjectIncluding({ username: String, }), ); - const { username } = this.requestParams(); + const { username } = this.urlParams; const user = await Users.findOneByAgentUsername(username, { projection: { _id: 1, diff --git a/apps/meteor/app/api/server/v1/voip/rooms.ts b/apps/meteor/app/api/server/v1/voip/rooms.ts index f04feaf0e084..1798591f57db 100644 --- a/apps/meteor/app/api/server/v1/voip/rooms.ts +++ b/apps/meteor/app/api/server/v1/voip/rooms.ts @@ -1,9 +1,9 @@ -import { Match, check } from 'meteor/check'; import { Random } from 'meteor/random'; -import type { ILivechatAgent } from '@rocket.chat/core-typings'; +import type { ILivechatAgent, IVoipRoom } from '@rocket.chat/core-typings'; +import { isVoipRoomProps, isVoipRoomsProps, isVoipRoomCloseProps } from '@rocket.chat/rest-typings'; +import { VoipRoom, LivechatVisitors, Users } from '@rocket.chat/models'; import { API } from '../../api'; -import { VoipRoom, LivechatVisitors, Users } from '../../../../models/server/raw'; import { LivechatVoip } from '../../../../../server/sdk'; import { hasPermission } from '../../../../authorization/server'; import { typedJsonParse } from '../../../../../lib/typedJSONParse'; @@ -24,6 +24,7 @@ const validateDateParams = (property: string, date: DateParam = {}): DateParam = const parseAndValidate = (property: string, date?: string): DateParam => { return validateDateParams(property, parseDateParams(date)); }; + /** * @openapi * /voip/server/api/v1/voip/room @@ -80,19 +81,38 @@ const parseAndValidate = (property: string, date?: string): DateParam => { * $ref: '#/components/schemas/ApiFailureV1' */ +const isRoomSearchProps = (props: any): props is { rid: string; token: string } => { + return 'rid' in props && 'token' in props; +}; + +const isRoomCreationProps = (props: any): props is { agentId: string; direction: IVoipRoom['direction'] } => { + return 'agentId' in props && 'direction' in props; +}; + API.v1.addRoute( 'voip/room', - { authRequired: false, rateLimiterOptions: { numRequestsAllowed: 5, intervalTimeInMS: 60000 } }, + { + authRequired: true, + rateLimiterOptions: { numRequestsAllowed: 5, intervalTimeInMS: 60000 }, + permissionsRequired: ['inbound-voip-calls'], + validateParams: isVoipRoomProps, + }, { async get() { - const defaultCheckParams = { - token: String, - agentId: Match.Maybe(String), - rid: Match.Maybe(String), - }; - check(this.queryParams, defaultCheckParams); - - const { token, rid, agentId } = this.queryParams; + const { token } = this.queryParams; + let agentId: string | undefined = undefined; + let direction: IVoipRoom['direction'] = 'inbound'; + let rid: string | undefined = undefined; + + if (isRoomCreationProps(this.queryParams)) { + agentId = this.queryParams.agentId; + direction = this.queryParams.direction; + } + + if (isRoomSearchProps(this.queryParams)) { + rid = this.queryParams.rid; + } + const guest = await LivechatVisitors.getVisitorByToken(token, {}); if (!guest) { return API.v1.failure('invalid-token'); @@ -103,6 +123,9 @@ API.v1.addRoute( if (room) { return API.v1.success({ room, newRoom: false }); } + if (!agentId) { + return API.v1.failure('agent-not-found'); + } const agentObj: ILivechatAgent = await Users.findOneAgentById(agentId, { projection: { username: 1 }, @@ -115,7 +138,11 @@ API.v1.addRoute( const agent = { agentId: _id, username }; const rid = Random.id(); - return API.v1.success(await LivechatVoip.getNewRoom(guest, agent, rid, { projection: API.v1.defaultFieldsToExclude })); + return API.v1.success( + await LivechatVoip.getNewRoom(guest, agent, rid, direction, { + projection: API.v1.defaultFieldsToExclude, + }), + ); } const room = await VoipRoom.findOneByIdAndVisitorToken(rid, token, { projection: API.v1.defaultFieldsToExclude }); @@ -129,20 +156,15 @@ API.v1.addRoute( API.v1.addRoute( 'voip/rooms', - { authRequired: true }, + { authRequired: true, validateParams: isVoipRoomsProps }, { async get() { const { offset, count } = this.getPaginationItems(); + const { sort, fields } = this.parseJsonQuery(); - const { agents, open, tags, queue, visitorId } = this.requestParams(); + const { agents, open, tags, queue, visitorId, direction, roomName } = this.requestParams(); const { createdAt: createdAtParam, closedAt: closedAtParam } = this.requestParams(); - check(agents, Match.Maybe([String])); - check(open, Match.Maybe(String)); - check(tags, Match.Maybe([String])); - check(queue, Match.Maybe(String)); - check(visitorId, Match.Maybe(String)); - // Reusing same L room permissions for simplicity const hasAdminAccess = hasPermission(this.userId, 'view-livechat-rooms'); const hasAgentAccess = hasPermission(this.userId, 'view-l-room') && agents?.includes(this.userId) && agents?.length === 1; @@ -162,6 +184,8 @@ API.v1.addRoute( visitorId, createdAt, closedAt, + direction, + roomName, options: { sort, offset, count, fields }, }), ); @@ -212,16 +236,10 @@ API.v1.addRoute( */ API.v1.addRoute( 'voip/room.close', - { authRequired: true }, + { authRequired: true, validateParams: isVoipRoomCloseProps, permissionsRequired: ['inbound-voip-calls'] }, { async post() { - check(this.bodyParams, { - rid: String, - token: String, - comment: Match.Maybe(String), - tags: Match.Maybe([String]), - }); - const { rid, token, comment, tags } = this.bodyParams; + const { rid, token, options } = this.bodyParams; const visitor = await LivechatVisitors.getVisitorByToken(token, {}); if (!visitor) { @@ -234,7 +252,7 @@ API.v1.addRoute( if (!room.open) { return API.v1.failure('room-closed'); } - const closeResult = await LivechatVoip.closeRoom(visitor, room, this.user, comment, tags); + const closeResult = await LivechatVoip.closeRoom(visitor, room, this.user, 'voip-call-wrapup', options); if (!closeResult) { return API.v1.failure(); } diff --git a/apps/meteor/app/apps/client/@types/IOrchestrator.ts b/apps/meteor/app/apps/client/@types/IOrchestrator.ts index f178cd03960d..848dcdfabf82 100644 --- a/apps/meteor/app/apps/client/@types/IOrchestrator.ts +++ b/apps/meteor/app/apps/client/@types/IOrchestrator.ts @@ -1,5 +1,5 @@ -import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata/IAppInfo'; import { ISetting } from '@rocket.chat/apps-engine/definition/settings/ISetting'; +import { App } from '@rocket.chat/core-typings'; export interface IDetailedDescription { raw: string; @@ -110,26 +110,6 @@ export enum EAppPurchaseType { PurchaseTypeSubscription = 'subscription', } -export interface IAppFromMarketplace { - appId: string; - latest: ILatest; - isAddon: boolean; - isEnterpriseOnly: boolean; - isBundle: boolean; - bundledAppIds: any[]; - bundledIn: IBundledIn[]; - isPurchased: boolean; - isSubscribed: boolean; - subscriptionInfo: ISubscriptionInfo; - price: number; - purchaseType: EAppPurchaseType; - isUsageBased: boolean; - createdAt: Date; - modifiedAt: Date; - pricingPlans: IPricingPlan[]; - addonId: string; -} - export interface ILanguageInfo { Params: string; Description: string; @@ -148,7 +128,6 @@ export interface IAppLanguage { export interface IAppExternalURL { url: string; - success: boolean; } export interface ICategory { @@ -159,13 +138,13 @@ export interface ICategory { title: string; } -export interface IDeletedInstalledApp { - app: IAppInfo; - success: boolean; -} +// export interface IDeletedInstalledApp { +// app: IAppInfo; +// success: boolean; +// } export interface IAppSynced { - app: IAppFromMarketplace; + app: App; success: boolean; } @@ -193,12 +172,3 @@ export interface ISettingsReturn { settings: ISettings; success: boolean; } - -export interface ISettingsPayload { - settings: ISetting[]; -} - -export interface ISettingsSetReturn { - updated: ISettings; - success: boolean; -} diff --git a/apps/meteor/app/apps/client/RealAppsEngineUIHost.js b/apps/meteor/app/apps/client/RealAppsEngineUIHost.js index c7d15fd467ef..6a73e7f38a7e 100644 --- a/apps/meteor/app/apps/client/RealAppsEngineUIHost.js +++ b/apps/meteor/app/apps/client/RealAppsEngineUIHost.js @@ -29,7 +29,7 @@ export class RealAppsEngineUIHost extends AppsEngineUIHost { let cachedMembers = []; try { - const { members } = await APIClient.get('v1/groups.members', { roomId: id }); + const { members } = await APIClient.get('/v1/groups.members', { roomId: id }); cachedMembers = members.map(({ _id, username }) => ({ id: _id, diff --git a/apps/meteor/app/apps/client/communication/websockets.js b/apps/meteor/app/apps/client/communication/websockets.js index 090fb059edaf..67f03e0df1ee 100644 --- a/apps/meteor/app/apps/client/communication/websockets.js +++ b/apps/meteor/app/apps/client/communication/websockets.js @@ -50,8 +50,8 @@ export class AppWebsocketReceiver extends Emitter { } onCommandAddedOrUpdated = (command) => { - APIClient.v1.get('commands.get', { command }).then((result) => { - slashCommands.commands[command] = result.command; + APIClient.get('/v1/commands.get', { command }).then((result) => { + slashCommands.add(result.command); }); }; diff --git a/apps/meteor/app/apps/client/gameCenter/gameCenter.js b/apps/meteor/app/apps/client/gameCenter/gameCenter.js index bd3e19669302..6486573f4974 100644 --- a/apps/meteor/app/apps/client/gameCenter/gameCenter.js +++ b/apps/meteor/app/apps/client/gameCenter/gameCenter.js @@ -8,7 +8,7 @@ import { handleError } from '../../../../client/lib/utils/handleError'; const getExternalComponents = async (instance) => { try { - const { externalComponents } = await APIClient.get('apps/externalComponents'); + const { externalComponents } = await APIClient.get('/apps/externalComponents'); instance.games.set(externalComponents); } catch (e) { handleError(e); diff --git a/apps/meteor/app/apps/client/gameCenter/gameContainer.js b/apps/meteor/app/apps/client/gameCenter/gameContainer.js index 7ee2285ee901..d954c06d1c25 100644 --- a/apps/meteor/app/apps/client/gameCenter/gameContainer.js +++ b/apps/meteor/app/apps/client/gameCenter/gameContainer.js @@ -62,7 +62,7 @@ Template.GameContainer.events({ Template.GameContainer.onCreated(async () => { const externalComponent = await getExternalComponent(); - APIClient.post('apps/externalComponentEvent', { + APIClient.post('/apps/externalComponentEvent', { event: 'IPostExternalComponentOpened', externalComponent, }); @@ -71,7 +71,7 @@ Template.GameContainer.onCreated(async () => { Template.GameContainer.onDestroyed(async () => { const externalComponent = await getExternalComponent(); - APIClient.post('apps/externalComponentEvent', { + APIClient.post('/apps/externalComponentEvent', { event: 'IPostExternalComponentClosed', externalComponent, }); diff --git a/apps/meteor/app/apps/client/orchestrator.ts b/apps/meteor/app/apps/client/orchestrator.ts index ecbe4c9ed818..d4052fa67308 100644 --- a/apps/meteor/app/apps/client/orchestrator.ts +++ b/apps/meteor/app/apps/client/orchestrator.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-var-requires */ +import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import { AppClientManager } from '@rocket.chat/apps-engine/client/AppClientManager'; import { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api'; import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; @@ -6,6 +7,7 @@ import { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPe import { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage/IAppStorageItem'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; +import { AppScreenshot, Serialized } from '@rocket.chat/core-typings'; import { App } from '../../../client/views/admin/apps/types'; import { dispatchToastMessage } from '../../../client/lib/toast'; @@ -13,65 +15,19 @@ import { settings } from '../../settings/client'; import { CachedCollectionManager } from '../../ui-cached-collection'; import { createDeferredValue } from '../lib/misc/DeferredValue'; import { - IPricingPlan, - EAppPurchaseType, - IAppFromMarketplace, + // IAppFromMarketplace, IAppLanguage, IAppExternalURL, ICategory, - IDeletedInstalledApp, - IAppSynced, - IAppScreenshots, - IAuthor, - IDetailedChangelog, - IDetailedDescription, - ISubscriptionInfo, - ISettingsReturn, - ISettingsPayload, - ISettingsSetReturn, + // IAppSynced, + // IAppScreenshots, + // IScreenshot, } from './@types/IOrchestrator'; import { AppWebsocketReceiver } from './communication'; import { handleI18nResources } from './i18n'; import { RealAppsEngineUIHost } from './RealAppsEngineUIHost'; - -const { APIClient } = require('../../utils'); -const { hasAtLeastOnePermission } = require('../../authorization'); - -export interface IAppsFromMarketplace { - price: number; - pricingPlans: IPricingPlan[]; - purchaseType: EAppPurchaseType; - isEnterpriseOnly: boolean; - modifiedAt: Date; - internalId: string; - id: string; - name: string; - nameSlug: string; - version: string; - categories: string[]; - description: string; - detailedDescription: IDetailedDescription; - detailedChangelog: IDetailedChangelog; - requiredApiVersion: string; - author: IAuthor; - classFile: string; - iconFile: string; - iconFileData: string; - status: string; - isVisible: boolean; - createdDate: Date; - modifiedDate: Date; - isPurchased: boolean; - isSubscribed: boolean; - subscriptionInfo: ISubscriptionInfo; - compiled: boolean; - compileJobId: string; - changesNote: string; - languages: string[]; - privacyPolicySummary: string; - internalChangesNote: string; - permissions: IPermission[]; -} +import { APIClient } from '../../utils/client'; +import { hasAtLeastOnePermission } from '../../authorization/client'; class AppClientOrchestrator { private _appClientUIHost: RealAppsEngineUIHost; @@ -123,8 +79,9 @@ class AppClientOrchestrator { } } - public screenshots(appId: string): IAppScreenshots { - return APIClient.get(`apps/${appId}/screenshots`); + public async screenshots(appId: string): Promise { + const { screenshots } = await APIClient.get(`/apps/${appId}/screenshots`); + return screenshots; } public isEnabled(): Promise | undefined { @@ -132,13 +89,24 @@ class AppClientOrchestrator { } public async getApps(): Promise { - const { apps } = await APIClient.get('apps'); - return apps; + const result = await APIClient.get<'/apps'>('/apps'); + + if ('apps' in result) { + // TODO: chapter day: multiple results are returned, but we only need one + return result.apps as App[]; + } + throw new Error('Invalid response from API'); } - public async getAppsFromMarketplace(): Promise { - const appsOverviews: IAppFromMarketplace[] = await APIClient.get('apps', { marketplace: 'true' }); - return appsOverviews.map((app: IAppFromMarketplace) => { + public async getAppsFromMarketplace(): Promise { + const result = await APIClient.get('/apps', { marketplace: 'true' }); + + if (!Array.isArray(result)) { + // TODO: chapter day: multiple results are returned, but we only need one + throw new Error('Invalid response from API'); + } + + return (result as App[]).map((app: App) => { const { latest, price, pricingPlans, purchaseType, isEnterpriseOnly, modifiedAt } = app; return { ...latest, @@ -152,59 +120,59 @@ class AppClientOrchestrator { } public async getAppsOnBundle(bundleId: string): Promise { - const { apps } = await APIClient.get(`apps/bundles/${bundleId}/apps`); + const { apps } = await APIClient.get(`/apps/bundles/${bundleId}/apps`); return apps; } public async getAppsLanguages(): Promise { - const { apps } = await APIClient.get('apps/languages'); + const { apps } = await APIClient.get('/apps/languages'); return apps; } public async getApp(appId: string): Promise { - const { app } = await APIClient.get(`apps/${appId}`); + const { app } = await APIClient.get(`/apps/${appId}` as any); return app; } - public async getAppFromMarketplace(appId: string, version: string): Promise { - const { app } = await APIClient.get(`apps/${appId}`, { - marketplace: 'true', - version, - }); - return app; + public async getAppFromMarketplace(appId: string, version: string): Promise<{ app: App; success: boolean }> { + const result = await APIClient.get( + `/apps/${appId}` as any, + { + marketplace: 'true', + version, + } as any, + ); + return result; } public async getLatestAppFromMarketplace(appId: string, version: string): Promise { - const { app } = await APIClient.get(`apps/${appId}`, { - marketplace: 'true', - update: 'true', - appVersion: version, - }); + const { app } = await APIClient.get( + `/apps/${appId}` as any, + { + marketplace: 'true', + update: 'true', + appVersion: version, + } as any, + ); return app; } - public async getAppSettings(appId: string): Promise { - const { settings } = await APIClient.get(`apps/${appId}/settings`); - return settings; - } - - public async setAppSettings(appId: string, settings: ISettingsPayload): Promise { - const { updated } = await APIClient.post(`apps/${appId}/settings`, undefined, { settings }); - return updated; + public async setAppSettings(appId: string, settings: ISetting[]): Promise { + await APIClient.post(`/apps/${appId}/settings`, { settings }); } public async getAppApis(appId: string): Promise { - const { apis } = await APIClient.get(`apps/${appId}/apis`); + const { apis } = await APIClient.get(`/apps/${appId}/apis`); return apis; } public async getAppLanguages(appId: string): Promise { - const { languages } = await APIClient.get(`apps/${appId}/languages`); + const { languages } = await APIClient.get(`/apps/${appId}/languages`); return languages; } - public async installApp(appId: string, version: string, permissionsGranted: IPermission[]): Promise { - const { app } = await APIClient.post('apps/', { + public async installApp(appId: string, version: string, permissionsGranted: IPermission[]): Promise { + const { app } = await APIClient.post('/apps', { appId, marketplace: true, version, @@ -214,48 +182,50 @@ class AppClientOrchestrator { } public async updateApp(appId: string, version: string, permissionsGranted: IPermission[]): Promise { - const { app } = await APIClient.post(`apps/${appId}`, { + const result = (await (APIClient.post as any)(`/apps/${appId}` as any, { appId, marketplace: true, version, permissionsGranted, - }); - return app; - } + })) as any; - public uninstallApp(appId: string): IDeletedInstalledApp { - return APIClient.delete(`apps/${appId}`); - } - - public syncApp(appId: string): IAppSynced { - return APIClient.post(`apps/${appId}/sync`); + if ('app' in result) { + return result; + } + throw new Error('App not found'); } public async setAppStatus(appId: string, status: AppStatus): Promise { - const { status: effectiveStatus } = await APIClient.post(`apps/${appId}/status`, { status }); + const { status: effectiveStatus } = await APIClient.post(`/apps/${appId}/status`, { status }); return effectiveStatus; } - public enableApp(appId: string): Promise { - return this.setAppStatus(appId, AppStatus.MANUALLY_ENABLED); - } - public disableApp(appId: string): Promise { return this.setAppStatus(appId, AppStatus.MANUALLY_ENABLED); } - public buildExternalUrl(appId: string, purchaseType = 'buy', details = false): IAppExternalURL { - return APIClient.get('apps', { + public async buildExternalUrl(appId: string, purchaseType: 'buy' | 'subscription' = 'buy', details = false): Promise { + const result = await APIClient.get('/apps', { buildExternalUrl: 'true', appId, purchaseType, - details, + details: `${details}`, }); + + if ('url' in result) { + return result; + } + throw new Error('Failed to build external url'); } - public async getCategories(): Promise { - const categories = await APIClient.get('apps', { categories: 'true' }); - return categories; + public async getCategories(): Promise> { + const result = await APIClient.get('/apps', { categories: 'true' }); + + if (Array.isArray(result)) { + // TODO: chapter day: multiple results are returned, but we only need one + return result as Serialized[]; + } + throw new Error('Failed to get categories'); } public getUIHost(): RealAppsEngineUIHost { @@ -279,7 +249,7 @@ Meteor.startup(() => { }); Tracker.autorun(() => { - const isEnabled = settings.get('Apps_Framework_enabled'); + const isEnabled = settings.get('/Apps_Framework_enabled'); Apps.load(isEnabled); }); }); diff --git a/apps/meteor/app/apps/server/bridges/activation.ts b/apps/meteor/app/apps/server/bridges/activation.ts index ac0a2654cae8..a610200b896e 100644 --- a/apps/meteor/app/apps/server/bridges/activation.ts +++ b/apps/meteor/app/apps/server/bridges/activation.ts @@ -1,8 +1,8 @@ import { AppActivationBridge as ActivationBridge } from '@rocket.chat/apps-engine/server/bridges/AppActivationBridge'; import { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import { Users } from '@rocket.chat/models'; -import { Users } from '../../../models/server/raw'; import { AppServerOrchestrator } from '../orchestrator'; export class AppActivationBridge extends ActivationBridge { diff --git a/apps/meteor/app/apps/server/bridges/api.ts b/apps/meteor/app/apps/server/bridges/api.ts index ea2b8f05e689..fcca596a47b4 100644 --- a/apps/meteor/app/apps/server/bridges/api.ts +++ b/apps/meteor/app/apps/server/bridges/api.ts @@ -7,6 +7,7 @@ import { AppApi } from '@rocket.chat/apps-engine/server/managers/AppApi'; import { RequestMethod } from '@rocket.chat/apps-engine/definition/accessors'; import { AppServerOrchestrator } from '../orchestrator'; +import { authenticationMiddleware } from '../../../api/server/middlewares/authentication'; const apiServer = express(); @@ -14,10 +15,10 @@ apiServer.disable('x-powered-by'); WebApp.connectHandlers.use(apiServer); -type RequestWithPrivateHash = Request & { +interface IRequestWithPrivateHash extends Request { _privateHash?: string; content?: any; -}; +} export class AppApisBridge extends ApiBridge { appRouters: Map; @@ -27,7 +28,7 @@ export class AppApisBridge extends ApiBridge { super(); this.appRouters = new Map(); - apiServer.use('/api/apps/private/:appId/:hash', (req: RequestWithPrivateHash, res: Response) => { + apiServer.use('/api/apps/private/:appId/:hash', (req: IRequestWithPrivateHash, res: Response) => { const notFound = (): Response => res.sendStatus(404); const router = this.appRouters.get(req.params.appId); @@ -73,7 +74,7 @@ export class AppApisBridge extends ApiBridge { } if (router[method] instanceof Function) { - router[method](routePath, Meteor.bindEnvironment(this._appApiExecutor(endpoint, appId))); + router[method](routePath, this._authMiddleware(endpoint, appId), Meteor.bindEnvironment(this._appApiExecutor(endpoint, appId))); } } @@ -85,6 +86,11 @@ export class AppApisBridge extends ApiBridge { } } + private _authMiddleware(endpoint: IApiEndpoint, _appId: string): RequestHandler { + const authFunction = authenticationMiddleware({ rejectUnauthorized: !!endpoint.authRequired }); + return Meteor.bindEnvironment(authFunction); + } + private _verifyApi(api: IApi, endpoint: IApiEndpoint): void { if (typeof api !== 'object') { throw new Error('Invalid Api parameter provided, it must be a valid IApi object.'); @@ -96,7 +102,7 @@ export class AppApisBridge extends ApiBridge { } private _appApiExecutor(endpoint: IApiEndpoint, appId: string): RequestHandler { - return (req: RequestWithPrivateHash, res: Response): void => { + return (req: IRequestWithPrivateHash, res: Response): void => { const request: IApiRequest = { method: req.method.toLowerCase() as RequestMethod, headers: req.headers as { [key: string]: string }, @@ -104,6 +110,7 @@ export class AppApisBridge extends ApiBridge { params: req.params || {}, content: req.body, privateHash: req._privateHash, + user: req.user && this.orch.getConverters()?.get('users')?.convertToApp(req.user), }; this.orch diff --git a/apps/meteor/app/apps/server/bridges/bridges.js b/apps/meteor/app/apps/server/bridges/bridges.js index d03160e02d0f..828be594640d 100644 --- a/apps/meteor/app/apps/server/bridges/bridges.js +++ b/apps/meteor/app/apps/server/bridges/bridges.js @@ -18,6 +18,7 @@ import { AppLivechatBridge } from './livechat'; import { AppUploadBridge } from './uploads'; import { UiInteractionBridge } from './uiInteraction'; import { AppSchedulerBridge } from './scheduler'; +import { AppVideoConferenceBridge } from './videoConferences'; export class RealAppBridges extends AppBridges { constructor(orch) { @@ -41,6 +42,7 @@ export class RealAppBridges extends AppBridges { this._uiInteractionBridge = new UiInteractionBridge(orch); this._schedulerBridge = new AppSchedulerBridge(orch); this._cloudWorkspaceBridge = new AppCloudBridge(orch); + this._videoConfBridge = new AppVideoConferenceBridge(orch); } getCommandBridge() { @@ -114,4 +116,8 @@ export class RealAppBridges extends AppBridges { getCloudWorkspaceBridge() { return this._cloudWorkspaceBridge; } + + getVideoConferenceBridge() { + return this._videoConfBridge; + } } diff --git a/apps/meteor/app/apps/server/bridges/commands.ts b/apps/meteor/app/apps/server/bridges/commands.ts index 8a6726e3925a..8c9a429af170 100644 --- a/apps/meteor/app/apps/server/bridges/commands.ts +++ b/apps/meteor/app/apps/server/bridges/commands.ts @@ -1,11 +1,12 @@ 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'; import { AppServerOrchestrator } from '../orchestrator'; +import { parseParameters } from '../../../../lib/utils/parseParameters'; export class AppCommandsBridge extends CommandBridge { disabledCommands: Map; @@ -114,7 +115,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,13 +161,24 @@ 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; - const params = parameters.length === 0 || parameters === ' ' ? [] : parameters.split(' '); + const params = parseParameters(parameters); - const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params), threadId, triggerId); + const context = new SlashCommandContext( + Object.freeze(user), + Object.freeze(room), + Object.freeze(params) as string[], + threadId, + triggerId, + ); Promise.await(this.orch.getManager()?.getCommandManager().executeCommand(command, context)); } @@ -175,9 +187,9 @@ export class AppCommandsBridge extends CommandBridge { const user = this.orch.getConverters()?.get('users').convertById(Meteor.userId()); const room = this.orch.getConverters()?.get('rooms').convertById(message.rid); const threadId = message.tmid; - const params = parameters.length === 0 || parameters === ' ' ? [] : parameters.split(' '); + const params = parseParameters(parameters); - const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params), threadId); + const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params) as string[], threadId); return Promise.await(this.orch.getManager()?.getCommandManager().getPreviews(command, context)); } @@ -191,10 +203,16 @@ export class AppCommandsBridge extends CommandBridge { const user = this.orch.getConverters()?.get('users').convertById(Meteor.userId()); const room = this.orch.getConverters()?.get('rooms').convertById(message.rid); const threadId = message.tmid; - const params = parameters.length === 0 || parameters === ' ' ? [] : parameters.split(' '); + const params = parseParameters(parameters); - const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params), threadId, triggerId); + const context = new SlashCommandContext( + Object.freeze(user), + Object.freeze(room), + Object.freeze(params) as string[], + 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/apps/server/bridges/http.ts b/apps/meteor/app/apps/server/bridges/http.ts index f93ac8a42459..cd0cae9122fe 100644 --- a/apps/meteor/app/apps/server/bridges/http.ts +++ b/apps/meteor/app/apps/server/bridges/http.ts @@ -114,7 +114,7 @@ export class AppHttpBridge extends HttpBridge { } return result; - } catch (e) { + } catch (e: any) { return e.response; } } diff --git a/apps/meteor/app/apps/server/bridges/internal.ts b/apps/meteor/app/apps/server/bridges/internal.ts index 64cb2cc59ed0..7b21e61139ed 100644 --- a/apps/meteor/app/apps/server/bridges/internal.ts +++ b/apps/meteor/app/apps/server/bridges/internal.ts @@ -1,10 +1,10 @@ import { InternalBridge } from '@rocket.chat/apps-engine/server/bridges/InternalBridge'; import { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import type { ISubscription } from '@rocket.chat/core-typings'; +import { Settings } from '@rocket.chat/models'; import { AppServerOrchestrator } from '../orchestrator'; import { Subscriptions } from '../../../models/server'; -import { Settings } from '../../../models/server/raw'; export class AppInternalBridge extends InternalBridge { // eslint-disable-next-line no-empty-function diff --git a/apps/meteor/app/apps/server/bridges/listeners.js b/apps/meteor/app/apps/server/bridges/listeners.js index 57e0a9b63bba..ced6e1aea31b 100644 --- a/apps/meteor/app/apps/server/bridges/listeners.js +++ b/apps/meteor/app/apps/server/bridges/listeners.js @@ -7,6 +7,7 @@ export class AppListenerBridge { } async handleEvent(event, ...payload) { + // eslint-disable-next-line complexity const method = (() => { switch (event) { case AppInterface.IPreMessageSentPrevent: diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 23a0e91411da..28f28a9ae4c9 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -11,10 +11,11 @@ import { IUser } from '@rocket.chat/apps-engine/definition/users'; import { IMessage } from '@rocket.chat/apps-engine/definition/messages'; import { IExtraRoomParams } from '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator'; import { OmnichannelSourceType } from '@rocket.chat/core-typings'; +import { LivechatVisitors } from '@rocket.chat/models'; import { getRoom } from '../../../livechat/server/api/lib/livechat'; import { Livechat } from '../../../livechat/server/lib/Livechat'; -import { Users, LivechatDepartment, LivechatVisitors, LivechatRooms } from '../../../models/server'; +import { Users, LivechatDepartment, LivechatRooms } from '../../../models/server'; import { AppServerOrchestrator } from '../orchestrator'; export class AppLivechatBridge extends LivechatBridge { @@ -216,9 +217,9 @@ export class AppLivechatBridge extends LivechatBridge { console.warn('The method AppLivechatBridge.findVisitors is deprecated. Please consider using its alternatives'); } - return LivechatVisitors.find(query) - .fetch() - .map((visitor: IVisitor) => this.orch.getConverters()?.get('visitors').convertVisitor(visitor)); + return (await LivechatVisitors.find(query).toArray()).map( + (visitor) => visitor && this.orch.getConverters()?.get('visitors').convertVisitor(visitor), + ); } protected async findVisitorById(id: string, appId: string): Promise { @@ -230,19 +231,28 @@ export class AppLivechatBridge extends LivechatBridge { protected async findVisitorByEmail(email: string, appId: string): Promise { this.orch.debugLog(`The App ${appId} is looking for livechat visitors.`); - return this.orch.getConverters()?.get('visitors').convertVisitor(LivechatVisitors.findOneGuestByEmailAddress(email)); + return this.orch + .getConverters() + ?.get('visitors') + .convertVisitor(await LivechatVisitors.findOneGuestByEmailAddress(email)); } protected async findVisitorByToken(token: string, appId: string): Promise { this.orch.debugLog(`The App ${appId} is looking for livechat visitors.`); - return this.orch.getConverters()?.get('visitors').convertVisitor(LivechatVisitors.getVisitorByToken(token, {})); + return this.orch + .getConverters() + ?.get('visitors') + .convertVisitor(await LivechatVisitors.getVisitorByToken(token, {})); } protected async findVisitorByPhoneNumber(phoneNumber: string, appId: string): Promise { this.orch.debugLog(`The App ${appId} is looking for livechat visitors.`); - return this.orch.getConverters()?.get('visitors').convertVisitor(LivechatVisitors.findOneVisitorByPhone(phoneNumber)); + return this.orch + .getConverters() + ?.get('visitors') + .convertVisitor(await LivechatVisitors.findOneVisitorByPhone(phoneNumber)); } protected async findDepartmentByIdOrName(value: string, appId: string): Promise { diff --git a/apps/meteor/app/apps/server/bridges/scheduler.ts b/apps/meteor/app/apps/server/bridges/scheduler.ts index 0bb388c21ee5..6daede78134d 100644 --- a/apps/meteor/app/apps/server/bridges/scheduler.ts +++ b/apps/meteor/app/apps/server/bridges/scheduler.ts @@ -1,4 +1,4 @@ -import Agenda from 'agenda'; +import { Agenda, Job } from '@rocket.chat/agenda'; import { ObjectID } from 'bson'; import { MongoInternals } from 'meteor/mongo'; import { StartupType, IProcessor, IOnetimeSchedule, IRecurringSchedule } from '@rocket.chat/apps-engine/definition/scheduler'; @@ -6,7 +6,7 @@ import { SchedulerBridge } from '@rocket.chat/apps-engine/server/bridges/Schedul import { AppServerOrchestrator } from '../orchestrator'; -function _callProcessor(processor: Function): (job: Agenda.Job) => void { +function _callProcessor(processor: Function): (job: Job) => void { return (job): void => { const data = job?.attrs?.data || {}; diff --git a/apps/meteor/app/apps/server/bridges/settings.ts b/apps/meteor/app/apps/server/bridges/settings.ts index 9262642b2d3c..9062eb923fd0 100644 --- a/apps/meteor/app/apps/server/bridges/settings.ts +++ b/apps/meteor/app/apps/server/bridges/settings.ts @@ -1,7 +1,7 @@ import { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import { ServerSettingBridge } from '@rocket.chat/apps-engine/server/bridges/ServerSettingBridge'; +import { Settings } from '@rocket.chat/models'; -import { Settings } from '../../../models/server/raw'; import { AppServerOrchestrator } from '../orchestrator'; export class AppSettingBridge extends ServerSettingBridge { @@ -56,6 +56,16 @@ export class AppSettingBridge extends ServerSettingBridge { throw new Error(`The setting "${setting.id}" is not readable.`); } - throw new Error('Method not implemented.'); + await Settings.updateValueById(setting.id, setting.value); + } + + protected async incrementValue(id: string, value: number, appId: string): Promise { + this.orch.debugLog(`The App ${appId} is incrementing the value of the setting ${id}.`); + + if (!(await this.isReadableById(id, appId))) { + throw new Error(`The setting "${id}" is not readable.`); + } + + await Settings.incrementValueById(id, value); } } diff --git a/apps/meteor/app/apps/server/bridges/users.ts b/apps/meteor/app/apps/server/bridges/users.ts index 68e5e54fc1f2..012ad09227c5 100644 --- a/apps/meteor/app/apps/server/bridges/users.ts +++ b/apps/meteor/app/apps/server/bridges/users.ts @@ -2,10 +2,10 @@ import { Random } from 'meteor/random'; import { UserPresence } from 'meteor/konecty:user-presence'; import { UserBridge } from '@rocket.chat/apps-engine/server/bridges/UserBridge'; import { IUserCreationOptions, IUser } from '@rocket.chat/apps-engine/definition/users'; +import { Subscriptions, Users as UsersRaw } from '@rocket.chat/models'; import { setUserAvatar, checkUsernameAvailability, deleteUser } from '../../../lib/server/functions'; import { Users } from '../../../models/server'; -import { Subscriptions, Users as UsersRaw } from '../../../models/server/raw'; import { AppServerOrchestrator } from '../orchestrator'; export class AppUserBridge extends UserBridge { @@ -98,7 +98,7 @@ export class AppUserBridge extends UserBridge { const { status } = fields; delete fields.status; - await UsersRaw.update({ _id: user.id }, { $set: fields }); + await UsersRaw.update({ _id: user.id }, { $set: fields as any }); if (status) { UserPresence.setDefaultStatus(user.id, status); diff --git a/apps/meteor/app/apps/server/bridges/videoConferences.ts b/apps/meteor/app/apps/server/bridges/videoConferences.ts new file mode 100644 index 000000000000..93c9e445f91c --- /dev/null +++ b/apps/meteor/app/apps/server/bridges/videoConferences.ts @@ -0,0 +1,71 @@ +import { VideoConferenceBridge } from '@rocket.chat/apps-engine/server/bridges/VideoConferenceBridge'; +import { AppVideoConference, VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; +import { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders'; + +import { VideoConf } from '../../../../server/sdk'; +import { AppServerOrchestrator } from '../orchestrator'; +import { videoConfProviders } from '../../../../server/lib/videoConfProviders'; +import type { AppVideoConferencesConverter } from '../converters/videoConferences'; + +export class AppVideoConferenceBridge extends VideoConferenceBridge { + // eslint-disable-next-line no-empty-function + constructor(private readonly orch: AppServerOrchestrator) { + super(); + } + + protected async getById(callId: string, appId: string): Promise { + this.orch.debugLog(`The App ${appId} is getting the video conference byId: "${callId}"`); + + return this.orch.getConverters()?.get('videoConferences').convertById(callId); + } + + protected async create(call: AppVideoConference, appId: string): Promise { + this.orch.debugLog(`The App ${appId} is creating a video conference.`); + + return ( + await VideoConf.create({ + type: 'videoconference', + ...call, + }) + ).callId; + } + + protected async update(call: VideoConference, appId: string): Promise { + this.orch.debugLog(`The App ${appId} is updating a video conference.`); + + const oldData = call._id && (await VideoConf.getUnfiltered(call._id)); + if (!oldData) { + throw new Error('A video conference must exist to update.'); + } + + const data = (this.orch.getConverters()?.get('videoConferences') as AppVideoConferencesConverter).convertAppVideoConference(call); + await VideoConf.setProviderData(call._id, data.providerData); + + for (const { _id, ts } of data.users) { + if (oldData.users.find((user) => user._id === _id)) { + continue; + } + + VideoConf.addUser(call._id, _id, ts); + } + + if (data.endedBy && data.endedBy._id !== oldData.endedBy?._id) { + await VideoConf.setEndedBy(call._id, data.endedBy._id); + } else if (data.endedAt) { + await VideoConf.setEndedAt(call._id, data.endedAt); + } + + if (data.status > oldData.status) { + await VideoConf.setStatus(call._id, data.status); + } + } + + protected async registerProvider(info: IVideoConfProvider, appId: string): Promise { + this.orch.debugLog(`The App ${appId} is registering a video conference provider.`); + videoConfProviders.registerProvider(info.name, info.capabilities || {}, appId); + } + + protected async unRegisterProvider(info: IVideoConfProvider): Promise { + videoConfProviders.unRegisterProvider(info.name); + } +} diff --git a/apps/meteor/app/apps/server/communication/index.js b/apps/meteor/app/apps/server/communication/index.ts similarity index 100% rename from apps/meteor/app/apps/server/communication/index.js rename to apps/meteor/app/apps/server/communication/index.ts diff --git a/apps/meteor/app/apps/server/communication/methods.js b/apps/meteor/app/apps/server/communication/methods.js deleted file mode 100644 index 1bccb4fbed46..000000000000 --- a/apps/meteor/app/apps/server/communication/methods.js +++ /dev/null @@ -1,95 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Settings } from '../../../models/server/raw'; -import { hasPermission } from '../../../authorization/server'; -import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; - -const waitToLoad = function (orch) { - return new Promise((resolve) => { - let id = setInterval(() => { - if (orch.isEnabled() && orch.isLoaded()) { - clearInterval(id); - id = -1; - resolve(); - } - }, 100); - }); -}; - -const waitToUnload = function (orch) { - return new Promise((resolve) => { - let id = setInterval(() => { - if (!orch.isEnabled() && !orch.isLoaded()) { - clearInterval(id); - id = -1; - resolve(); - } - }, 100); - }); -}; - -export class AppMethods { - constructor(orch) { - this._orch = orch; - - this._addMethods(); - } - - isEnabled() { - return typeof this._orch !== 'undefined' && this._orch.isEnabled(); - } - - isLoaded() { - return typeof this._orch !== 'undefined' && this._orch.isEnabled() && this._orch.isLoaded(); - } - - _addMethods() { - const instance = this; - - Meteor.methods({ - 'apps/is-enabled'() { - return instance.isEnabled(); - }, - - 'apps/is-loaded'() { - return instance.isLoaded(); - }, - - 'apps/go-enable': twoFactorRequired(function _appsGoEnable() { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'apps/go-enable', - }); - } - - if (!hasPermission(Meteor.userId(), 'manage-apps')) { - throw new Meteor.Error('error-action-not-allowed', 'Not allowed', { - method: 'apps/go-enable', - }); - } - - Settings.updateValueById('Apps_Framework_enabled', true); - - Promise.await(waitToLoad(instance._orch)); - }), - - 'apps/go-disable': twoFactorRequired(function _appsGoDisable() { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'apps/go-enable', - }); - } - - if (!hasPermission(Meteor.userId(), 'manage-apps')) { - throw new Meteor.Error('error-action-not-allowed', 'Not allowed', { - method: 'apps/go-enable', - }); - } - - Settings.updateValueById('Apps_Framework_enabled', false); - - Promise.await(waitToUnload(instance._orch)); - }), - }); - } -} diff --git a/apps/meteor/app/apps/server/communication/methods.ts b/apps/meteor/app/apps/server/communication/methods.ts new file mode 100644 index 000000000000..5533651aecb3 --- /dev/null +++ b/apps/meteor/app/apps/server/communication/methods.ts @@ -0,0 +1,100 @@ +import { Meteor } from 'meteor/meteor'; +import { SettingValue } from '@rocket.chat/core-typings'; +import { Settings } from '@rocket.chat/models'; + +import { hasPermission } from '../../../authorization/server'; +import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; +import { AppServerOrchestrator } from '../orchestrator'; + +const waitToLoad = function (orch: AppServerOrchestrator): unknown { + return new Promise((resolve) => { + const id = setInterval(() => { + if (orch.isEnabled() && orch.isLoaded()) { + clearInterval(id); + resolve(); + } + }, 100); + }); +}; + +const waitToUnload = function (orch: AppServerOrchestrator): unknown { + return new Promise((resolve) => { + const id = setInterval(() => { + if (!orch.isEnabled() && !orch.isLoaded()) { + clearInterval(id); + resolve(); + } + }, 100); + }); +}; + +export class AppMethods { + private orch: AppServerOrchestrator; + + constructor(orch: AppServerOrchestrator) { + this.orch = orch; + + this.addMethods(); + } + + isEnabled(): SettingValue { + return typeof this.orch !== 'undefined' && this.orch.isEnabled(); + } + + isLoaded(): boolean { + return Boolean(typeof this.orch !== 'undefined' && this.orch.isEnabled() && this.orch.isLoaded()); + } + + private addMethods(): void { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const instance = this; + + Meteor.methods({ + 'apps/is-enabled'() { + return instance.isEnabled(); + }, + + 'apps/is-loaded'() { + return instance.isLoaded(); + }, + + 'apps/go-enable': twoFactorRequired(function _appsGoEnable() { + const uid = Meteor.userId(); + if (!uid) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'apps/go-enable', + }); + } + + if (!hasPermission(uid, 'manage-apps')) { + throw new Meteor.Error('error-action-not-allowed', 'Not allowed', { + method: 'apps/go-enable', + }); + } + + Settings.updateValueById('Apps_Framework_enabled', true); + + Promise.await(waitToLoad(instance.orch)); + }), + + 'apps/go-disable': twoFactorRequired(function _appsGoDisable() { + const uid = Meteor.userId(); + if (!uid) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'apps/go-enable', + }); + } + + if (!hasPermission(uid, 'manage-apps')) { + throw new Meteor.Error('error-action-not-allowed', 'Not allowed', { + method: 'apps/go-enable', + }); + } + + Settings.updateValueById('Apps_Framework_enabled', false); + + Promise.await(waitToUnload(instance.orch)); + }), + }); + } +} diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index 2bc64e1f8be5..632a57ea24f7 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { HTTP } from 'meteor/http'; +import { Settings } from '@rocket.chat/models'; import { API } from '../../../api/server'; import { getUploadFormData } from '../../../api/server/lib/getUploadFormData'; @@ -7,7 +8,6 @@ import { getWorkspaceAccessToken, getUserCloudAccessToken } from '../../../cloud import { settings } from '../../../settings/server'; import { Info } from '../../../utils'; import { Users } from '../../../models/server'; -import { Settings } from '../../../models/server/raw'; import { Apps } from '../orchestrator'; import { formatAppInstanceForRest } from '../../lib/misc/formatAppInstanceForRest'; import { actionButtonsHandler } from './endpoints/actionButtonsHandler'; @@ -213,10 +213,13 @@ export class AppsRestApi { return API.v1.failure({ error: 'Direct installation of an App is disabled.' }); } - const formData = await getUploadFormData({ - request: this.request, - }); - buff = formData?.app?.fileBuffer; + const [app, formData] = await getUploadFormData( + { + request: this.request, + }, + { field: 'app' }, + ); + buff = app?.fileBuffer; permissionsGranted = (() => { try { const permissions = JSON.parse(formData?.permissions || ''); @@ -462,10 +465,13 @@ export class AppsRestApi { return API.v1.failure({ error: 'Direct updating of an App is disabled.' }); } - const formData = await getUploadFormData({ - request: this.request, - }); - buff = formData?.app?.fileBuffer; + const [app, formData] = await getUploadFormData( + { + request: this.request, + }, + { field: 'app' }, + ); + buff = app?.fileBuffer; permissionsGranted = (() => { try { const permissions = JSON.parse(formData?.permissions || ''); @@ -522,6 +528,38 @@ export class AppsRestApi { }, ); + this.api.addRoute( + ':id/versions', + { authRequired: true, permissionsRequired: ['manage-apps'] }, + { + get() { + const baseUrl = orchestrator.getMarketplaceUrl(); + + const headers = {}; // DO NOT ATTACH THE FRAMEWORK/ENGINE VERSION HERE. + const token = getWorkspaceAccessToken(); + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + let result; + try { + result = HTTP.get(`${baseUrl}/v1/apps/${this.urlParams.id}`, { + headers, + }); + } catch (e) { + return handleError('Unable to access Marketplace. Does the server has access to the internet?', e); + } + + if (!result || result.statusCode !== 200) { + orchestrator.getRocketChatLogger().error('Error getting the App versions from the Marketplace:', result.data); + return API.v1.failure(); + } + + return API.v1.success({ apps: result.data }); + }, + }, + ); + this.api.addRoute( ':id/sync', { authRequired: true, permissionsRequired: ['manage-apps'] }, diff --git a/apps/meteor/app/apps/server/communication/uikit.js b/apps/meteor/app/apps/server/communication/uikit.js deleted file mode 100644 index 27b7c906f8cf..000000000000 --- a/apps/meteor/app/apps/server/communication/uikit.js +++ /dev/null @@ -1,321 +0,0 @@ -import express from 'express'; -import cors from 'cors'; -import rateLimit from 'express-rate-limit'; -import { Meteor } from 'meteor/meteor'; -import { WebApp } from 'meteor/webapp'; -import { UIKitIncomingInteractionType } from '@rocket.chat/apps-engine/definition/uikit'; -import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata'; - -import { Users } from '../../../models/server'; -import { settings } from '../../../settings/server'; -import { Apps } from '../orchestrator'; -import { UiKitCoreApp } from '../../../../server/sdk'; - -const apiServer = express(); - -apiServer.disable('x-powered-by'); - -let corsEnabled = false; -let allowListOrigins = []; - -settings.watch('API_Enable_CORS', (value) => { - corsEnabled = value; -}); - -settings.watch('API_CORS_Origin', (value) => { - allowListOrigins = value - ? value - .trim() - .split(',') - .map((origin) => String(origin).trim().toLocaleLowerCase()) - : []; -}); - -const corsOptions = { - origin: (origin, callback) => { - if ( - !origin || - !corsEnabled || - allowListOrigins.includes('*') || - allowListOrigins.includes(origin) || - origin === settings.get('Site_Url') - ) { - callback(null, true); - } else { - callback('Not allowed by CORS', false); - } - }, -}; - -WebApp.connectHandlers.use(apiServer); - -// eslint-disable-next-line new-cap -const router = express.Router(); - -const unauthorized = (res) => - res.status(401).send({ - status: 'error', - message: 'You must be logged in to do this.', - }); - -Meteor.startup(() => { - // use specific rate limit of 600 (which is 60 times the default limits) requests per minute (around 10/second) - const apiLimiter = rateLimit({ - windowMs: settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'), - max: settings.get('API_Enable_Rate_Limiter_Limit_Calls_Default') * 60, - skip: () => - settings.get('API_Enable_Rate_Limiter') !== true || - (process.env.NODE_ENV === 'development' && settings.get('API_Enable_Rate_Limiter_Dev') !== true), - }); - router.use(apiLimiter); -}); - -router.use((req, res, next) => { - const { 'x-user-id': userId, 'x-auth-token': authToken, 'x-visitor-token': visitorToken } = req.headers; - - if (userId && authToken) { - req.user = Users.findOneByIdAndLoginToken(userId, authToken); - req.userId = req.user._id; - } - - if (visitorToken) { - req.visitor = Apps.getConverters().get('visitors').convertByToken(visitorToken); - } - - if (!req.user && !req.visitor) { - return unauthorized(res); - } - - next(); -}); - -apiServer.use('/api/apps/ui.interaction/', cors(corsOptions), router); - -const getPayloadForType = (type, req) => { - if (type === UIKitIncomingInteractionType.BLOCK) { - const { type, actionId, triggerId, mid, rid, payload, container } = req.body; - - const { visitor, user } = req; - const room = rid; // orch.getConverters().get('rooms').convertById(rid); - const message = mid; - - return { - type, - container, - actionId, - message, - triggerId, - payload, - user, - visitor, - room, - }; - } - - if (type === UIKitIncomingInteractionType.VIEW_CLOSED) { - const { - type, - actionId, - payload: { view, isCleared }, - } = req.body; - - const { user } = req; - - return { - type, - actionId, - user, - payload: { - view, - isCleared, - }, - }; - } - - if (type === UIKitIncomingInteractionType.VIEW_SUBMIT) { - const { type, actionId, triggerId, payload } = req.body; - - const { user } = req; - - return { - type, - actionId, - triggerId, - payload, - user, - }; - } - - throw new Error('Type not supported'); -}; - -router.post('/:appId', async (req, res, next) => { - const { appId } = req.params; - - const isCore = await UiKitCoreApp.isRegistered(appId); - if (!isCore) { - return next(); - } - - const { type } = req.body; - - try { - const payload = { - ...getPayloadForType(type, req), - appId, - }; - - const result = await UiKitCoreApp[type](payload); - - res.send(result); - } catch (e) { - console.error('ops', e); - res.status(500).send({ error: e.message }); - } -}); - -const appsRoutes = (orch) => (req, res) => { - const { appId } = req.params; - - const { type } = req.body; - - switch (type) { - case UIKitIncomingInteractionType.BLOCK: { - const { type, actionId, triggerId, mid, rid, payload, container } = req.body; - - const { visitor } = req; - const room = orch.getConverters().get('rooms').convertById(rid); - const user = orch.getConverters().get('users').convertToApp(req.user); - const message = mid && orch.getConverters().get('messages').convertById(mid); - - const action = { - type, - container, - appId, - actionId, - message, - triggerId, - payload, - user, - visitor, - room, - }; - - try { - const eventInterface = !visitor ? AppInterface.IUIKitInteractionHandler : AppInterface.IUIKitLivechatInteractionHandler; - - const result = Promise.await(orch.triggerEvent(eventInterface, action)); - - res.send(result); - } catch (e) { - res.status(500).send(e.message); - } - break; - } - - case UIKitIncomingInteractionType.VIEW_CLOSED: { - const { - type, - actionId, - payload: { view, isCleared }, - } = req.body; - - const user = orch.getConverters().get('users').convertToApp(req.user); - - const action = { - type, - appId, - actionId, - user, - payload: { - view, - isCleared, - }, - }; - - try { - Promise.await(orch.triggerEvent('IUIKitInteractionHandler', action)); - - res.sendStatus(200); - } catch (e) { - res.status(500).send(e.message); - } - break; - } - - case UIKitIncomingInteractionType.VIEW_SUBMIT: { - const { type, actionId, triggerId, payload } = req.body; - - const user = orch.getConverters().get('users').convertToApp(req.user); - - const action = { - type, - appId, - actionId, - triggerId, - payload, - user, - }; - - try { - const result = Promise.await(orch.triggerEvent('IUIKitInteractionHandler', action)); - - res.send(result); - } catch (e) { - res.status(500).send(e.message); - } - break; - } - - case UIKitIncomingInteractionType.ACTION_BUTTON: { - const { - type, - actionId, - triggerId, - rid, - mid, - payload: { context }, - } = req.body; - - const room = orch.getConverters().get('rooms').convertById(rid); - const user = orch.getConverters().get('users').convertToApp(req.user); - const message = mid && orch.getConverters().get('messages').convertById(mid); - - const action = { - type, - appId, - actionId, - triggerId, - user, - room, - message, - payload: { - context, - }, - }; - - try { - const result = Promise.await(orch.triggerEvent('IUIKitInteractionHandler', action)); - - res.send(result); - } catch (e) { - res.status(500).send(e.message); - } - break; - } - - default: { - res.status(400).send({ error: 'Unknown action' }); - } - } - - // TODO: validate payloads per type -}; - -export class AppUIKitInteractionApi { - constructor(orch) { - this.orch = orch; - - router.post('/:appId', appsRoutes(orch)); - } -} diff --git a/apps/meteor/app/apps/server/communication/uikit.ts b/apps/meteor/app/apps/server/communication/uikit.ts new file mode 100644 index 000000000000..a9a8ddc4673b --- /dev/null +++ b/apps/meteor/app/apps/server/communication/uikit.ts @@ -0,0 +1,326 @@ +import express, { Request, Response } from 'express'; +import cors from 'cors'; +import rateLimit from 'express-rate-limit'; +import { Meteor } from 'meteor/meteor'; +import { WebApp } from 'meteor/webapp'; +import { UIKitIncomingInteractionType } from '@rocket.chat/apps-engine/definition/uikit'; +import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata'; + +import { settings } from '../../../settings/server'; +import { Apps, AppServerOrchestrator } from '../orchestrator'; +import { UiKitCoreApp } from '../../../../server/sdk'; +import { authenticationMiddleware } from '../../../api/server/middlewares/authentication'; + +const apiServer = express(); + +apiServer.disable('x-powered-by'); + +let corsEnabled = false; +let allowListOrigins: string[] = []; + +settings.watch('API_Enable_CORS', (value: boolean) => { + corsEnabled = value; +}); + +settings.watch('API_CORS_Origin', (value: string) => { + allowListOrigins = value + ? value + .trim() + .split(',') + .map((origin) => String(origin).trim().toLocaleLowerCase()) + : []; +}); + +WebApp.connectHandlers.use(apiServer); + +// eslint-disable-next-line new-cap +const router = express.Router(); + +const unauthorized = (res: Response): unknown => + res.status(401).send({ + status: 'error', + message: 'You must be logged in to do this.', + }); + +Meteor.startup(() => { + // use specific rate limit of 600 (which is 60 times the default limits) requests per minute (around 10/second) + const apiLimiter = rateLimit({ + windowMs: settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'), + max: (settings.get('API_Enable_Rate_Limiter_Limit_Calls_Default') as number) * 60, + skip: () => + settings.get('API_Enable_Rate_Limiter') !== true || + (process.env.NODE_ENV === 'development' && settings.get('API_Enable_Rate_Limiter_Dev') !== true), + }); + + router.use(apiLimiter); +}); + +router.use(authenticationMiddleware({ rejectUnauthorized: false })); + +router.use((req: Request, res, next) => { + const { 'x-visitor-token': visitorToken } = req.headers; + + if (visitorToken) { + req.body.visitor = Apps.getConverters()?.get('visitors').convertByToken(visitorToken); + } + + if (!req.user && !req.body.visitor) { + return unauthorized(res); + } + + next(); +}); + +const corsOptions = { + origin: (origin: string | undefined, callback: Function): void => { + if ( + !origin || + !corsEnabled || + allowListOrigins.includes('*') || + allowListOrigins.includes(origin) || + origin === settings.get('Site_Url') + ) { + callback(null, true); + } else { + callback('Not allowed by CORS', false); + } + }, +}; + +apiServer.use('/api/apps/ui.interaction/', cors(corsOptions), router); // didn't have the rateLimiter option + +const getPayloadForType = (type: UIKitIncomingInteractionType, req: Request): {} => { + if (type === UIKitIncomingInteractionType.BLOCK) { + const { type, actionId, triggerId, mid, rid, payload, container } = req.body; + + const { visitor } = req.body; + const { user } = req; + + const room = rid; // orch.getConverters().get('rooms').convertById(rid); + const message = mid; + + return { + type, + container, + actionId, + message, + triggerId, + payload, + user, + visitor, + room, + }; + } + + if (type === UIKitIncomingInteractionType.VIEW_CLOSED) { + const { + type, + actionId, + payload: { view, isCleared }, + } = req.body; + + const { user } = req; + + return { + type, + actionId, + user, + payload: { + view, + isCleared, + }, + }; + } + + if (type === UIKitIncomingInteractionType.VIEW_SUBMIT) { + const { type, actionId, triggerId, payload } = req.body; + + const { user } = req; + + return { + type, + actionId, + triggerId, + payload, + user, + }; + } + + throw new Error('Type not supported'); +}; + +router.post('/:appId', async (req, res, next) => { + const { appId } = req.params; + + const isCore = await UiKitCoreApp.isRegistered(appId); + if (!isCore) { + return next(); + } + + // eslint-disable-next-line prefer-destructuring + const type: UIKitIncomingInteractionType = req.body.type; + + try { + const payload = { + ...getPayloadForType(type, req), + appId, + }; + + const result = await (UiKitCoreApp as any)[type](payload); // TO-DO: fix type + + res.send(result); + } catch (e) { + if (e instanceof Error) res.status(500).send({ error: e.message }); + else res.status(500).send({ error: e }); + } +}); + +const appsRoutes = + (orch: AppServerOrchestrator) => + (req: Request, res: Response): void => { + const { appId } = req.params; + + const { type } = req.body; + + switch (type) { + case UIKitIncomingInteractionType.BLOCK: { + const { type, actionId, triggerId, mid, rid, payload, container } = req.body; + + const { visitor } = req.body; + const room = orch.getConverters()?.get('rooms').convertById(rid); + const user = orch.getConverters()?.get('users').convertToApp(req.user); + const message = mid && orch.getConverters()?.get('messages').convertById(mid); + + const action = { + type, + container, + appId, + actionId, + message, + triggerId, + payload, + user, + visitor, + room, + }; + + try { + const eventInterface = !visitor ? AppInterface.IUIKitInteractionHandler : AppInterface.IUIKitLivechatInteractionHandler; + + const result = Promise.await(orch.triggerEvent(eventInterface, action)); + + res.send(result); + } catch (e) { + res.status(500).send(e); // e.message + } + break; + } + + case UIKitIncomingInteractionType.VIEW_CLOSED: { + const { + type, + actionId, + payload: { view, isCleared }, + } = req.body; + + const user = orch.getConverters()?.get('users').convertToApp(req.user); + + const action = { + type, + appId, + actionId, + user, + payload: { + view, + isCleared, + }, + }; + + try { + const result = Promise.await(orch.triggerEvent('IUIKitInteractionHandler', action)); + + res.send(result); + } catch (e) { + res.status(500).send(e); // e.message + } + break; + } + + case UIKitIncomingInteractionType.VIEW_SUBMIT: { + const { type, actionId, triggerId, payload } = req.body; + + const user = orch.getConverters()?.get('users').convertToApp(req.user); + + const action = { + type, + appId, + actionId, + triggerId, + payload, + user, + }; + + try { + const result = Promise.await(orch.triggerEvent('IUIKitInteractionHandler', action)); + + res.send(result); + } catch (e) { + res.status(500).send(e); // e.message + } + break; + } + + case UIKitIncomingInteractionType.ACTION_BUTTON: { + const { + type, + actionId, + triggerId, + rid, + mid, + payload: { context }, + } = req.body; + + const room = orch.getConverters()?.get('rooms').convertById(rid); + const user = orch.getConverters()?.get('users').convertToApp(req.user); + const message = mid && orch.getConverters()?.get('messages').convertById(mid); + + const action = { + type, + appId, + actionId, + triggerId, + user, + room, + message, + payload: { + context, + }, + }; + + try { + const result = Promise.await(orch.triggerEvent('IUIKitInteractionHandler', action)); + + res.send(result); + } catch (e) { + res.status(500).send(e); // e.message + } + break; + } + + default: { + res.status(400).send({ error: 'Unknown action' }); + } + } + + // TODO: validate payloads per type + }; + +export class AppUIKitInteractionApi { + orch: AppServerOrchestrator; + + constructor(orch: AppServerOrchestrator) { + this.orch = orch; + + router.post('/:appId', appsRoutes(orch)); + } +} diff --git a/apps/meteor/app/apps/server/communication/websockets.js b/apps/meteor/app/apps/server/communication/websockets.js deleted file mode 100644 index 005e74345733..000000000000 --- a/apps/meteor/app/apps/server/communication/websockets.js +++ /dev/null @@ -1,197 +0,0 @@ -import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; - -import { SystemLogger } from '../../../../server/lib/logger/system'; -import notifications from '../../../notifications/server/lib/Notifications'; - -export const AppEvents = Object.freeze({ - APP_ADDED: 'app/added', - APP_REMOVED: 'app/removed', - APP_UPDATED: 'app/updated', - APP_STATUS_CHANGE: 'app/statusUpdate', - APP_SETTING_UPDATED: 'app/settingUpdated', - COMMAND_ADDED: 'command/added', - COMMAND_DISABLED: 'command/disabled', - COMMAND_UPDATED: 'command/updated', - COMMAND_REMOVED: 'command/removed', - ACTIONS_CHANGED: 'actions/changed', -}); - -export class AppServerListener { - constructor(orch, engineStreamer, clientStreamer, received) { - this.orch = orch; - this.engineStreamer = engineStreamer; - this.clientStreamer = clientStreamer; - this.received = received; - - this.engineStreamer.on(AppEvents.APP_STATUS_CHANGE, this.onAppStatusUpdated.bind(this)); - this.engineStreamer.on(AppEvents.APP_REMOVED, this.onAppRemoved.bind(this)); - this.engineStreamer.on(AppEvents.APP_UPDATED, this.onAppUpdated.bind(this)); - this.engineStreamer.on(AppEvents.APP_ADDED, this.onAppAdded.bind(this)); - this.engineStreamer.on(AppEvents.ACTIONS_CHANGED, this.onActionsChanged.bind(this)); - - this.engineStreamer.on(AppEvents.APP_SETTING_UPDATED, this.onAppSettingUpdated.bind(this)); - this.engineStreamer.on(AppEvents.COMMAND_ADDED, this.onCommandAdded.bind(this)); - this.engineStreamer.on(AppEvents.COMMAND_DISABLED, this.onCommandDisabled.bind(this)); - this.engineStreamer.on(AppEvents.COMMAND_UPDATED, this.onCommandUpdated.bind(this)); - this.engineStreamer.on(AppEvents.COMMAND_REMOVED, this.onCommandRemoved.bind(this)); - } - - async onAppAdded(appId) { - await this.orch.getManager().loadOne(appId); - this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_ADDED, appId); - } - - async onAppStatusUpdated({ appId, status }) { - const app = this.orch.getManager().getOneById(appId); - - if (!app || app.getStatus() === status) { - return; - } - - this.received.set(`${AppEvents.APP_STATUS_CHANGE}_${appId}`, { - appId, - status, - when: new Date(), - }); - - if (AppStatusUtils.isEnabled(status)) { - await this.orch.getManager().enable(appId).catch(SystemLogger.error); - this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_STATUS_CHANGE, { appId, status }); - } else if (AppStatusUtils.isDisabled(status)) { - await this.orch.getManager().disable(appId, status, true).catch(SystemLogger.error); - this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_STATUS_CHANGE, { appId, status }); - } - } - - async onAppSettingUpdated({ appId, setting }) { - this.received.set(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting.id}`, { - appId, - setting, - when: new Date(), - }); - await this.orch.getManager().getSettingsManager().updateAppSetting(appId, setting); - this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_SETTING_UPDATED, { appId }); - } - - async onAppUpdated(appId) { - this.received.set(`${AppEvents.APP_UPDATED}_${appId}`, { appId, when: new Date() }); - - const storageItem = await this.orch.getStorage().retrieveOne(appId); - - const appPackage = await this.orch.getAppSourceStorage().fetch(storageItem); - - await this.orch.getManager().updateLocal(storageItem, appPackage); - - this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_UPDATED, appId); - } - - async onAppRemoved(appId) { - const app = this.orch.getManager().getOneById(appId); - - if (!app) { - return; - } - - await this.orch.getManager().removeLocal(appId); - this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_REMOVED, appId); - } - - async onCommandAdded(command) { - this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_ADDED, command); - } - - async onCommandDisabled(command) { - this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_DISABLED, command); - } - - async onCommandUpdated(command) { - this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_UPDATED, command); - } - - async onCommandRemoved(command) { - this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_REMOVED, command); - } - - async onActionsChanged() { - this.clientStreamer.emitWithoutBroadcast(AppEvents.ACTIONS_CHANGED); - } -} - -export class AppServerNotifier { - constructor(orch) { - this.engineStreamer = notifications.streamAppsEngine; - - // This is used to broadcast to the web clients - this.clientStreamer = notifications.streamApps; - - this.received = new Map(); - this.listener = new AppServerListener(orch, this.engineStreamer, this.clientStreamer, this.received); - } - - async appAdded(appId) { - this.engineStreamer.emit(AppEvents.APP_ADDED, appId); - this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_ADDED, appId); - } - - async appRemoved(appId) { - this.engineStreamer.emit(AppEvents.APP_REMOVED, appId); - this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_REMOVED, appId); - } - - async appUpdated(appId) { - if (this.received.has(`${AppEvents.APP_UPDATED}_${appId}`)) { - this.received.delete(`${AppEvents.APP_UPDATED}_${appId}`); - return; - } - - this.engineStreamer.emit(AppEvents.APP_UPDATED, appId); - this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_UPDATED, appId); - } - - async appStatusUpdated(appId, status) { - if (this.received.has(`${AppEvents.APP_STATUS_CHANGE}_${appId}`)) { - const details = this.received.get(`${AppEvents.APP_STATUS_CHANGE}_${appId}`); - if (details.status === status) { - this.received.delete(`${AppEvents.APP_STATUS_CHANGE}_${appId}`); - return; - } - } - - this.engineStreamer.emit(AppEvents.APP_STATUS_CHANGE, { appId, status }); - this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_STATUS_CHANGE, { appId, status }); - } - - async appSettingsChange(appId, setting) { - if (this.received.has(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting.id}`)) { - this.received.delete(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting.id}`); - return; - } - - this.engineStreamer.emit(AppEvents.APP_SETTING_UPDATED, { appId, setting }); - this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_SETTING_UPDATED, { appId }); - } - - async commandAdded(command) { - this.engineStreamer.emit(AppEvents.COMMAND_ADDED, command); - this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_ADDED, command); - } - - async commandDisabled(command) { - this.engineStreamer.emit(AppEvents.COMMAND_DISABLED, command); - this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_DISABLED, command); - } - - async commandUpdated(command) { - this.engineStreamer.emit(AppEvents.COMMAND_UPDATED, command); - this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_UPDATED, command); - } - - async commandRemoved(command) { - this.engineStreamer.emit(AppEvents.COMMAND_REMOVED, command); - this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_REMOVED, command); - } - - async actionsChanged() { - this.clientStreamer.emitWithoutBroadcast(AppEvents.ACTIONS_CHANGED); - } -} diff --git a/apps/meteor/app/apps/server/communication/websockets.ts b/apps/meteor/app/apps/server/communication/websockets.ts new file mode 100644 index 000000000000..cfdfdf2b77b5 --- /dev/null +++ b/apps/meteor/app/apps/server/communication/websockets.ts @@ -0,0 +1,220 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { AppStatus, AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; +import { ISetting } from '@rocket.chat/core-typings'; +import { IStreamer } from 'meteor/rocketchat:streamer'; + +import { SystemLogger } from '../../../../server/lib/logger/system'; +import notifications from '../../../notifications/server/lib/Notifications'; +import { AppServerOrchestrator } from '../orchestrator'; + +export enum AppEvents { + APP_ADDED = 'app/added', + APP_REMOVED = 'app/removed', + APP_UPDATED = 'app/updated', + APP_STATUS_CHANGE = 'app/statusUpdate', + APP_SETTING_UPDATED = 'app/settingUpdated', + COMMAND_ADDED = 'command/added', + COMMAND_DISABLED = 'command/disabled', + COMMAND_UPDATED = 'command/updated', + COMMAND_REMOVED = 'command/removed', + ACTIONS_CHANGED = 'actions/changed', +} + +export class AppServerListener { + private orch: AppServerOrchestrator; + + engineStreamer: IStreamer; + + clientStreamer: IStreamer; + + received; + + constructor(orch: AppServerOrchestrator, engineStreamer: IStreamer, clientStreamer: IStreamer, received: Map) { + this.orch = orch; + this.engineStreamer = engineStreamer; + this.clientStreamer = clientStreamer; + this.received = received; + + this.engineStreamer.on(AppEvents.APP_STATUS_CHANGE, this.onAppStatusUpdated.bind(this)); + this.engineStreamer.on(AppEvents.APP_REMOVED, this.onAppRemoved.bind(this)); + this.engineStreamer.on(AppEvents.APP_UPDATED, this.onAppUpdated.bind(this)); + this.engineStreamer.on(AppEvents.APP_ADDED, this.onAppAdded.bind(this)); + this.engineStreamer.on(AppEvents.ACTIONS_CHANGED, this.onActionsChanged.bind(this)); + + this.engineStreamer.on(AppEvents.APP_SETTING_UPDATED, this.onAppSettingUpdated.bind(this)); + this.engineStreamer.on(AppEvents.COMMAND_ADDED, this.onCommandAdded.bind(this)); + this.engineStreamer.on(AppEvents.COMMAND_DISABLED, this.onCommandDisabled.bind(this)); + this.engineStreamer.on(AppEvents.COMMAND_UPDATED, this.onCommandUpdated.bind(this)); + this.engineStreamer.on(AppEvents.COMMAND_REMOVED, this.onCommandRemoved.bind(this)); + } + + async onAppAdded(appId: string): Promise { + await (this.orch.getManager()! as any).loadOne(appId); // TO-DO: fix type + this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_ADDED, appId); + } + + async onAppStatusUpdated({ appId, status }: { appId: string; status: AppStatus }): Promise { + const app = this.orch.getManager()?.getOneById(appId); + + if (!app || app.getStatus() === status) { + return; + } + + this.received.set(`${AppEvents.APP_STATUS_CHANGE}_${appId}`, { + appId, + status, + when: new Date(), + }); + + if (AppStatusUtils.isEnabled(status)) { + await this.orch.getManager()?.enable(appId).catch(SystemLogger.error); + this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_STATUS_CHANGE, { appId, status }); + } else if (AppStatusUtils.isDisabled(status)) { + await this.orch.getManager()?.disable(appId, status, true).catch(SystemLogger.error); + this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_STATUS_CHANGE, { appId, status }); + } + } + + async onAppSettingUpdated({ appId, setting }: { appId: string; setting: ISetting }): Promise { + this.received.set(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting._id}`, { + appId, + setting, + when: new Date(), + }); + await this.orch + .getManager()! + .getSettingsManager() + .updateAppSetting(appId, setting as any); // TO-DO: fix type of `setting` + this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_SETTING_UPDATED, { appId }); + } + + async onAppUpdated(appId: string): Promise { + this.received.set(`${AppEvents.APP_UPDATED}_${appId}`, { appId, when: new Date() }); + + const storageItem = await this.orch.getStorage()!.retrieveOne(appId); + + const appPackage = await this.orch.getAppSourceStorage()!.fetch(storageItem); + + await this.orch.getManager()!.updateLocal(storageItem, appPackage); + + this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_UPDATED, appId); + } + + async onAppRemoved(appId: string): Promise { + const app = this.orch.getManager()!.getOneById(appId); + + if (!app) { + return; + } + + await this.orch.getManager()!.removeLocal(appId); + this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_REMOVED, appId); + } + + async onCommandAdded(command: string): Promise { + this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_ADDED, command); + } + + async onCommandDisabled(command: string): Promise { + this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_DISABLED, command); + } + + async onCommandUpdated(command: string): Promise { + this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_UPDATED, command); + } + + async onCommandRemoved(command: string): Promise { + this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_REMOVED, command); + } + + async onActionsChanged(): Promise { + this.clientStreamer.emitWithoutBroadcast(AppEvents.ACTIONS_CHANGED); + } +} + +export class AppServerNotifier { + engineStreamer: IStreamer; + + clientStreamer: IStreamer; + + received: Map; + + listener: AppServerListener; + + constructor(orch: AppServerOrchestrator) { + this.engineStreamer = notifications.streamAppsEngine; + + // This is used to broadcast to the web clients + this.clientStreamer = notifications.streamApps; + + this.received = new Map(); + this.listener = new AppServerListener(orch, this.engineStreamer, this.clientStreamer, this.received); + } + + async appAdded(appId: string): Promise { + this.engineStreamer.emit(AppEvents.APP_ADDED, appId); + this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_ADDED, appId); + } + + async appRemoved(appId: string): Promise { + this.engineStreamer.emit(AppEvents.APP_REMOVED, appId); + this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_REMOVED, appId); + } + + async appUpdated(appId: string): Promise { + if (this.received.has(`${AppEvents.APP_UPDATED}_${appId}`)) { + this.received.delete(`${AppEvents.APP_UPDATED}_${appId}`); + return; + } + + this.engineStreamer.emit(AppEvents.APP_UPDATED, appId); + this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_UPDATED, appId); + } + + async appStatusUpdated(appId: string, status: AppStatus): Promise { + if (this.received.has(`${AppEvents.APP_STATUS_CHANGE}_${appId}`)) { + const details = this.received.get(`${AppEvents.APP_STATUS_CHANGE}_${appId}`); + if (details.status === status) { + this.received.delete(`${AppEvents.APP_STATUS_CHANGE}_${appId}`); + return; + } + } + + this.engineStreamer.emit(AppEvents.APP_STATUS_CHANGE, { appId, status }); + this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_STATUS_CHANGE, { appId, status }); + } + + async appSettingsChange(appId: string, setting: ISetting): Promise { + if (this.received.has(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting._id}`)) { + this.received.delete(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting._id}`); + return; + } + + this.engineStreamer.emit(AppEvents.APP_SETTING_UPDATED, { appId, setting }); + this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_SETTING_UPDATED, { appId }); + } + + async commandAdded(command: string): Promise { + this.engineStreamer.emit(AppEvents.COMMAND_ADDED, command); + this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_ADDED, command); + } + + async commandDisabled(command: string): Promise { + this.engineStreamer.emit(AppEvents.COMMAND_DISABLED, command); + this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_DISABLED, command); + } + + async commandUpdated(command: string): Promise { + this.engineStreamer.emit(AppEvents.COMMAND_UPDATED, command); + this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_UPDATED, command); + } + + async commandRemoved(command: string): Promise { + this.engineStreamer.emit(AppEvents.COMMAND_REMOVED, command); + this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_REMOVED, command); + } + + async actionsChanged(): Promise { + this.clientStreamer.emitWithoutBroadcast(AppEvents.ACTIONS_CHANGED); + } +} diff --git a/apps/meteor/app/apps/server/converters/index.js b/apps/meteor/app/apps/server/converters/index.js index edb1bcf57cb1..d5fe67636dc0 100644 --- a/apps/meteor/app/apps/server/converters/index.js +++ b/apps/meteor/app/apps/server/converters/index.js @@ -2,5 +2,6 @@ import { AppMessagesConverter } from './messages'; import { AppRoomsConverter } from './rooms'; import { AppSettingsConverter } from './settings'; import { AppUsersConverter } from './users'; +import { AppVideoConferencesConverter } from './videoConferences'; -export { AppMessagesConverter, AppRoomsConverter, AppSettingsConverter, AppUsersConverter }; +export { AppMessagesConverter, AppRoomsConverter, AppSettingsConverter, AppUsersConverter, AppVideoConferencesConverter }; diff --git a/apps/meteor/app/apps/server/converters/messages.js b/apps/meteor/app/apps/server/converters/messages.js index 35d6daa4b463..25931452edd0 100644 --- a/apps/meteor/app/apps/server/converters/messages.js +++ b/apps/meteor/app/apps/server/converters/messages.js @@ -1,6 +1,6 @@ import { Random } from 'meteor/random'; -import { Messages, Rooms, Users } from '../../../models'; +import { Messages, Rooms, Users } from '../../../models/server'; import { transformMappedData } from '../../lib/misc/transformMappedData'; export class AppMessagesConverter { diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index 12857a94e117..4a9f6225af15 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -1,6 +1,7 @@ import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { LivechatVisitors } from '@rocket.chat/models'; -import { Rooms, Users, LivechatVisitors, LivechatDepartment } from '../../../models'; +import { Rooms, Users, LivechatDepartment } from '../../../models/server'; import { transformMappedData } from '../../lib/misc/transformMappedData'; export class AppRoomsConverter { @@ -36,7 +37,7 @@ export class AppRoomsConverter { let v; if (room.visitor) { - const visitor = LivechatVisitors.findOneById(room.visitor.id); + const visitor = Promise.await(LivechatVisitors.findOneById(room.visitor.id)); v = { _id: visitor._id, username: visitor.username, diff --git a/apps/meteor/app/apps/server/converters/settings.js b/apps/meteor/app/apps/server/converters/settings.js index bc5949bc7ccd..da3e075deb67 100644 --- a/apps/meteor/app/apps/server/converters/settings.js +++ b/apps/meteor/app/apps/server/converters/settings.js @@ -1,6 +1,5 @@ import { SettingType } from '@rocket.chat/apps-engine/definition/settings'; - -import { Settings } from '../../../models/server/raw'; +import { Settings } from '@rocket.chat/models'; export class AppSettingsConverter { constructor(orch) { diff --git a/apps/meteor/app/apps/server/converters/uploads.js b/apps/meteor/app/apps/server/converters/uploads.js index efbda7ae5fd1..d386e52fdcac 100644 --- a/apps/meteor/app/apps/server/converters/uploads.js +++ b/apps/meteor/app/apps/server/converters/uploads.js @@ -1,5 +1,6 @@ +import { Uploads } from '@rocket.chat/models'; + import { transformMappedData } from '../../lib/misc/transformMappedData'; -import { Uploads } from '../../../models/server/raw'; export class AppUploadsConverter { constructor(orch) { diff --git a/apps/meteor/app/apps/server/converters/users.js b/apps/meteor/app/apps/server/converters/users.js index e8891b9dd720..8c84f598934e 100644 --- a/apps/meteor/app/apps/server/converters/users.js +++ b/apps/meteor/app/apps/server/converters/users.js @@ -1,6 +1,6 @@ import { UserStatusConnection, UserType } from '@rocket.chat/apps-engine/definition/users'; -import { Users } from '../../../models'; +import { Users } from '../../../models/server'; export class AppUsersConverter { constructor(orch) { diff --git a/apps/meteor/app/apps/server/converters/videoConferences.ts b/apps/meteor/app/apps/server/converters/videoConferences.ts new file mode 100644 index 000000000000..dd4e2c113b6f --- /dev/null +++ b/apps/meteor/app/apps/server/converters/videoConferences.ts @@ -0,0 +1,36 @@ +import type { VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences'; +import type { IVideoConference } from '@rocket.chat/core-typings'; + +import { VideoConf } from '../../../../server/sdk'; +import type { AppServerOrchestrator } from '../orchestrator'; + +export class AppVideoConferencesConverter { + // @ts-ignore + private orch: AppServerOrchestrator; + + constructor(orch: AppServerOrchestrator) { + this.orch = orch; + } + + async convertById(callId: string): Promise { + const call = await VideoConf.getUnfiltered(callId); + + return this.convertVideoConference(call); + } + + convertVideoConference(call: IVideoConference | null): VideoConference | undefined { + if (!call) { + return; + } + + return { + ...call, + } as VideoConference; + } + + convertAppVideoConference(call: VideoConference): IVideoConference { + return { + ...call, + } as IVideoConference; + } +} diff --git a/apps/meteor/app/apps/server/converters/visitors.js b/apps/meteor/app/apps/server/converters/visitors.js index 40c29e1c59a8..361aa3758c6a 100644 --- a/apps/meteor/app/apps/server/converters/visitors.js +++ b/apps/meteor/app/apps/server/converters/visitors.js @@ -1,19 +1,21 @@ -import LivechatVisitors from '../../../models/server/models/LivechatVisitors'; +import { LivechatVisitors } from '@rocket.chat/models'; + import { transformMappedData } from '../../lib/misc/transformMappedData'; +// TODO: check if functions from this converter can be async export class AppVisitorsConverter { constructor(orch) { this.orch = orch; } convertById(id) { - const visitor = LivechatVisitors.findOneById(id); + const visitor = Promise.await(LivechatVisitors.findOneById(id)); return this.convertVisitor(visitor); } convertByToken(token) { - const visitor = LivechatVisitors.getVisitorByToken(token); + const visitor = Promise.await(LivechatVisitors.getVisitorByToken(token)); return this.convertVisitor(visitor); } diff --git a/apps/meteor/app/apps/server/cron.js b/apps/meteor/app/apps/server/cron.js index d70ba36d1e70..86a0e74b937e 100644 --- a/apps/meteor/app/apps/server/cron.js +++ b/apps/meteor/app/apps/server/cron.js @@ -3,12 +3,12 @@ import { HTTP } from 'meteor/http'; import { SyncedCron } from 'meteor/littledata:synced-cron'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import { Settings } from '@rocket.chat/models'; import { Apps } from './orchestrator'; import { getWorkspaceAccessToken } from '../../cloud/server'; import { Users } from '../../models/server'; import { sendMessagesToAdmins } from '../../../server/lib/sendMessagesToAdmins'; -import { Settings } from '../../models/server/raw'; const notifyAdminsAboutInvalidApps = Meteor.bindEnvironment(function _notifyAdminsAboutInvalidApps(apps) { if (!apps) { diff --git a/apps/meteor/app/apps/server/orchestrator.js b/apps/meteor/app/apps/server/orchestrator.js index 85a46fdc4a16..9d3bdf7a35a9 100644 --- a/apps/meteor/app/apps/server/orchestrator.js +++ b/apps/meteor/app/apps/server/orchestrator.js @@ -8,7 +8,13 @@ import { AppsLogsModel, AppsModel, AppsPersistenceModel } from '../../models/ser import { settings, settingsRegistry } from '../../settings/server'; import { RealAppBridges } from './bridges'; import { AppMethods, AppServerNotifier, AppsRestApi, AppUIKitInteractionApi } from './communication'; -import { AppMessagesConverter, AppRoomsConverter, AppSettingsConverter, AppUsersConverter } from './converters'; +import { + AppMessagesConverter, + AppRoomsConverter, + AppSettingsConverter, + AppUsersConverter, + AppVideoConferencesConverter, +} from './converters'; import { AppDepartmentsConverter } from './converters/departments'; import { AppUploadsConverter } from './converters/uploads'; import { AppVisitorsConverter } from './converters/visitors'; @@ -54,6 +60,7 @@ export class AppServerOrchestrator { this._converters.set('visitors', new AppVisitorsConverter(this)); this._converters.set('departments', new AppDepartmentsConverter(this)); this._converters.set('uploads', new AppUploadsConverter(this)); + this._converters.set('videoConferences', new AppVideoConferencesConverter(this)); this._bridges = new RealAppBridges(this); diff --git a/apps/meteor/app/assets/server/assets.js b/apps/meteor/app/assets/server/assets.js deleted file mode 100644 index 79ced079ea60..000000000000 --- a/apps/meteor/app/assets/server/assets.js +++ /dev/null @@ -1,525 +0,0 @@ -import crypto from 'crypto'; - -import { Meteor } from 'meteor/meteor'; -import { WebApp, WebAppInternals } from 'meteor/webapp'; -import { WebAppHashing } from 'meteor/webapp-hashing'; -import _ from 'underscore'; -import sizeOf from 'image-size'; -import sharp from 'sharp'; - -import { settings, settingsRegistry } from '../../settings/server'; -import { getURL } from '../../utils/lib/getURL'; -import { mime } from '../../utils/lib/mimeTypes'; -import { hasPermission } from '../../authorization'; -import { RocketChatFile } from '../../file'; -import { Settings } from '../../models/server'; - -const RocketChatAssetsInstance = new RocketChatFile.GridFS({ - name: 'assets', -}); - -const assets = { - logo: { - label: 'logo (svg, png, jpg)', - defaultUrl: 'images/logo/logo.svg', - constraints: { - type: 'image', - extensions: ['svg', 'png', 'jpg', 'jpeg'], - }, - wizard: { - step: 3, - order: 2, - }, - }, - background: { - label: 'login background (svg, png, jpg)', - constraints: { - type: 'image', - extensions: ['svg', 'png', 'jpg', 'jpeg'], - }, - }, - favicon_ico: { - label: 'favicon (ico)', - defaultUrl: 'favicon.ico', - constraints: { - type: 'image', - extensions: ['ico'], - }, - }, - favicon: { - label: 'favicon (svg)', - defaultUrl: 'images/logo/icon.svg', - constraints: { - type: 'image', - extensions: ['svg'], - }, - }, - favicon_16: { - label: 'favicon 16x16 (png)', - defaultUrl: 'images/logo/favicon-16x16.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 16, - height: 16, - }, - }, - favicon_32: { - label: 'favicon 32x32 (png)', - defaultUrl: 'images/logo/favicon-32x32.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 32, - height: 32, - }, - }, - favicon_192: { - label: 'android-chrome 192x192 (png)', - defaultUrl: 'images/logo/android-chrome-192x192.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 192, - height: 192, - }, - }, - favicon_512: { - label: 'android-chrome 512x512 (png)', - defaultUrl: 'images/logo/android-chrome-512x512.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 512, - height: 512, - }, - }, - touchicon_180: { - label: 'apple-touch-icon 180x180 (png)', - defaultUrl: 'images/logo/apple-touch-icon.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 180, - height: 180, - }, - }, - touchicon_180_pre: { - label: 'apple-touch-icon-precomposed 180x180 (png)', - defaultUrl: 'images/logo/apple-touch-icon-precomposed.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 180, - height: 180, - }, - }, - tile_70: { - label: 'mstile 70x70 (png)', - defaultUrl: 'images/logo/mstile-70x70.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 70, - height: 70, - }, - }, - tile_144: { - label: 'mstile 144x144 (png)', - defaultUrl: 'images/logo/mstile-144x144.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 144, - height: 144, - }, - }, - tile_150: { - label: 'mstile 150x150 (png)', - defaultUrl: 'images/logo/mstile-150x150.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 150, - height: 150, - }, - }, - tile_310_square: { - label: 'mstile 310x310 (png)', - defaultUrl: 'images/logo/mstile-310x310.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 310, - height: 310, - }, - }, - tile_310_wide: { - label: 'mstile 310x150 (png)', - defaultUrl: 'images/logo/mstile-310x150.png', - constraints: { - type: 'image', - extensions: ['png'], - width: 310, - height: 150, - }, - }, - safari_pinned: { - label: 'safari pinned tab (svg)', - defaultUrl: 'images/logo/safari-pinned-tab.svg', - constraints: { - type: 'image', - extensions: ['svg'], - }, - }, -}; - -export const RocketChatAssets = new (class { - get mime() { - return mime; - } - - get assets() { - return assets; - } - - setAsset(binaryContent, contentType, asset) { - if (!assets[asset]) { - throw new Meteor.Error('error-invalid-asset', 'Invalid asset', { - function: 'RocketChat.Assets.setAsset', - }); - } - - const extension = mime.extension(contentType); - if (assets[asset].constraints.extensions.includes(extension) === false) { - throw new Meteor.Error(contentType, `Invalid file type: ${contentType}`, { - function: 'RocketChat.Assets.setAsset', - errorTitle: 'error-invalid-file-type', - }); - } - - const file = Buffer.from(binaryContent, 'binary'); - if (assets[asset].constraints.width || assets[asset].constraints.height) { - const dimensions = sizeOf(file); - if (assets[asset].constraints.width && assets[asset].constraints.width !== dimensions.width) { - throw new Meteor.Error('error-invalid-file-width', 'Invalid file width', { - function: 'Invalid file width', - }); - } - if (assets[asset].constraints.height && assets[asset].constraints.height !== dimensions.height) { - throw new Meteor.Error('error-invalid-file-height'); - } - } - - const rs = RocketChatFile.bufferToStream(file); - RocketChatAssetsInstance.deleteFile(asset); - - const ws = RocketChatAssetsInstance.createWriteStream(asset, contentType); - ws.on( - 'end', - Meteor.bindEnvironment(function () { - return Meteor.setTimeout(function () { - const key = `Assets_${asset}`; - const value = { - url: `assets/${asset}.${extension}`, - defaultUrl: assets[asset].defaultUrl, - }; - - Settings.updateValueById(key, value); - return RocketChatAssets.processAsset(key, value); - }, 200); - }), - ); - - rs.pipe(ws); - } - - unsetAsset(asset) { - if (!assets[asset]) { - throw new Meteor.Error('error-invalid-asset', 'Invalid asset', { - function: 'RocketChat.Assets.unsetAsset', - }); - } - - RocketChatAssetsInstance.deleteFile(asset); - const key = `Assets_${asset}`; - const value = { - defaultUrl: assets[asset].defaultUrl, - }; - - Settings.updateValueById(key, value); - RocketChatAssets.processAsset(key, value); - } - - refreshClients() { - return process.emit('message', { - refresh: 'client', - }); - } - - processAsset(settingKey, settingValue) { - if (settingKey.indexOf('Assets_') !== 0) { - return; - } - - const assetKey = settingKey.replace(/^Assets_/, ''); - const assetValue = assets[assetKey]; - - if (!assetValue) { - return; - } - - if (!settingValue || !settingValue.url) { - assetValue.cache = undefined; - return; - } - - const file = RocketChatAssetsInstance.getFileSync(assetKey); - if (!file) { - assetValue.cache = undefined; - return; - } - - const hash = crypto.createHash('sha1').update(file.buffer).digest('hex'); - const extension = settingValue.url.split('.').pop(); - - assetValue.cache = { - path: `assets/${assetKey}.${extension}`, - cacheable: false, - sourceMapUrl: undefined, - where: 'client', - type: 'asset', - content: file.buffer, - extension, - url: `/assets/${assetKey}.${extension}?${hash}`, - size: file.length, - uploadDate: file.uploadDate, - contentType: file.contentType, - hash, - }; - - return assetValue.cache; - } - - getURL(assetName, options = { cdn: false, full: true }) { - const asset = settings.get(assetName); - const url = asset.url || asset.defaultUrl; - - return getURL(url, options); - } -})(); - -settingsRegistry.addGroup('Assets'); - -settingsRegistry.add('Assets_SvgFavicon_Enable', true, { - type: 'boolean', - group: 'Assets', - i18nLabel: 'Enable_Svg_Favicon', -}); - -function addAssetToSetting(asset, value) { - const key = `Assets_${asset}`; - - settingsRegistry.add( - key, - { - defaultUrl: value.defaultUrl, - }, - { - type: 'asset', - group: 'Assets', - fileConstraints: value.constraints, - i18nLabel: value.label, - asset, - public: true, - wizard: value.wizard, - }, - ); - - const currentValue = settings.get(key); - - if (typeof currentValue === 'object' && currentValue.defaultUrl !== assets[asset].defaultUrl) { - currentValue.defaultUrl = assets[asset].defaultUrl; - Settings.updateValueById(key, currentValue); - } -} - -for (const key of Object.keys(assets)) { - const value = assets[key]; - addAssetToSetting(key, value); -} - -settings.watchByRegex(/^Assets_/, (key, value) => RocketChatAssets.processAsset(key, value)); - -Meteor.startup(function () { - return Meteor.setTimeout(function () { - return process.emit('message', { - refresh: 'client', - }); - }, 200); -}); - -const { calculateClientHash } = WebAppHashing; - -WebAppHashing.calculateClientHash = function (manifest, includeFilter, runtimeConfigOverride) { - for (const key of Object.keys(assets)) { - const value = assets[key]; - if (!value.cache && !value.defaultUrl) { - continue; - } - - let cache = {}; - if (value.cache) { - cache = { - path: value.cache.path, - cacheable: value.cache.cacheable, - sourceMapUrl: value.cache.sourceMapUrl, - where: value.cache.where, - type: value.cache.type, - url: value.cache.url, - size: value.cache.size, - hash: value.cache.hash, - }; - } else { - const extension = value.defaultUrl.split('.').pop(); - cache = { - path: `assets/${key}.${extension}`, - cacheable: false, - sourceMapUrl: undefined, - where: 'client', - type: 'asset', - url: `/assets/${key}.${extension}?v3`, - hash: 'v3', - }; - } - - const manifestItem = _.findWhere(manifest, { - path: key, - }); - - if (manifestItem) { - const index = manifest.indexOf(manifestItem); - manifest[index] = cache; - } else { - manifest.push(cache); - } - } - - return calculateClientHash.call(this, manifest, includeFilter, runtimeConfigOverride); -}; - -Meteor.methods({ - refreshClients() { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'refreshClients', - }); - } - - const _hasPermission = hasPermission(Meteor.userId(), 'manage-assets'); - if (!_hasPermission) { - throw new Meteor.Error('error-action-not-allowed', 'Managing assets not allowed', { - method: 'refreshClients', - action: 'Managing_assets', - }); - } - - return RocketChatAssets.refreshClients(); - }, - - unsetAsset(asset) { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'unsetAsset', - }); - } - - const _hasPermission = hasPermission(Meteor.userId(), 'manage-assets'); - if (!_hasPermission) { - throw new Meteor.Error('error-action-not-allowed', 'Managing assets not allowed', { - method: 'unsetAsset', - action: 'Managing_assets', - }); - } - - return RocketChatAssets.unsetAsset(asset); - }, - - setAsset(binaryContent, contentType, asset) { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'setAsset', - }); - } - - const _hasPermission = hasPermission(Meteor.userId(), 'manage-assets'); - if (!_hasPermission) { - throw new Meteor.Error('error-action-not-allowed', 'Managing assets not allowed', { - method: 'setAsset', - action: 'Managing_assets', - }); - } - - RocketChatAssets.setAsset(binaryContent, contentType, asset); - }, -}); - -WebApp.connectHandlers.use( - '/assets/', - Meteor.bindEnvironment(function (req, res, next) { - const params = { - asset: decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, '')).replace(/\.[^.]*$/, ''), - }; - - const file = assets[params.asset] && assets[params.asset].cache; - - const format = req.url.replace(/.*\.([a-z]+)(?:$|\?.*)/i, '$1'); - - if ( - assets[params.asset] && - Array.isArray(assets[params.asset].constraints.extensions) && - !assets[params.asset].constraints.extensions.includes(format) - ) { - res.writeHead(403); - return res.end(); - } - if (!file) { - const defaultUrl = assets[params.asset] && assets[params.asset].defaultUrl; - if (defaultUrl) { - const assetUrl = format && ['png', 'svg'].includes(format) ? defaultUrl.replace(/(svg|png)$/, format) : defaultUrl; - req.url = `/${assetUrl}`; - WebAppInternals.staticFilesMiddleware(WebAppInternals.staticFilesByArch, req, res, next); - } else { - res.writeHead(404); - res.end(); - } - - return; - } - - const reqModifiedHeader = req.headers['if-modified-since']; - if (reqModifiedHeader) { - if (reqModifiedHeader === (file.uploadDate && file.uploadDate.toUTCString())) { - res.setHeader('Last-Modified', reqModifiedHeader); - res.writeHead(304); - res.end(); - return; - } - } - - res.setHeader('Cache-Control', 'public, max-age=0'); - res.setHeader('Expires', '-1'); - - if (format && format !== file.extension && ['png', 'jpg', 'jpeg'].includes(format)) { - res.setHeader('Content-Type', `image/${format}`); - sharp(file.content).toFormat(format).pipe(res); - return; - } - - res.setHeader('Last-Modified', (file.uploadDate && file.uploadDate.toUTCString()) || new Date().toUTCString()); - res.setHeader('Content-Type', file.contentType); - res.setHeader('Content-Length', file.size); - res.writeHead(200); - res.end(file.content); - }), -); diff --git a/apps/meteor/app/assets/server/assets.ts b/apps/meteor/app/assets/server/assets.ts new file mode 100644 index 000000000000..8fe2e76cb85e --- /dev/null +++ b/apps/meteor/app/assets/server/assets.ts @@ -0,0 +1,546 @@ +import crypto from 'crypto'; +import { ServerResponse, IncomingMessage } from 'http'; + +import { Meteor } from 'meteor/meteor'; +import { WebApp, WebAppInternals } from 'meteor/webapp'; +import { WebAppHashing } from 'meteor/webapp-hashing'; +import _ from 'underscore'; +import sizeOf from 'image-size'; +import sharp from 'sharp'; +import { NextHandleFunction } from 'connect'; +import { IRocketChatAssets, IRocketChatAsset } from '@rocket.chat/core-typings'; + +import { settings, settingsRegistry } from '../../settings/server'; +import { getURL } from '../../utils/lib/getURL'; +import { getExtension } from '../../utils/lib/mimeTypes'; +import { hasPermission } from '../../authorization/server'; +import { RocketChatFile } from '../../file'; +import { Settings } from '../../models/server'; + +const RocketChatAssetsInstance = new RocketChatFile.GridFS({ + name: 'assets', +}); +const assets: IRocketChatAssets = { + logo: { + label: 'logo (svg, png, jpg)', + defaultUrl: 'images/logo/logo.svg', + constraints: { + type: 'image', + extensions: ['svg', 'png', 'jpg', 'jpeg'], + }, + wizard: { + step: 3, + order: 2, + }, + }, + background: { + label: 'login background (svg, png, jpg)', + constraints: { + type: 'image', + extensions: ['svg', 'png', 'jpg', 'jpeg'], + }, + }, + // eslint-disable-next-line @typescript-eslint/camelcase + favicon_ico: { + label: 'favicon (ico)', + defaultUrl: 'favicon.ico', + constraints: { + type: 'image', + extensions: ['ico'], + }, + }, + favicon: { + label: 'favicon (svg)', + defaultUrl: 'images/logo/icon.svg', + constraints: { + type: 'image', + extensions: ['svg'], + }, + }, + // eslint-disable-next-line @typescript-eslint/camelcase + favicon_16: { + label: 'favicon 16x16 (png)', + defaultUrl: 'images/logo/favicon-16x16.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 16, + height: 16, + }, + }, + // eslint-disable-next-line @typescript-eslint/camelcase + favicon_32: { + label: 'favicon 32x32 (png)', + defaultUrl: 'images/logo/favicon-32x32.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 32, + height: 32, + }, + }, + // eslint-disable-next-line @typescript-eslint/camelcase + favicon_192: { + label: 'android-chrome 192x192 (png)', + defaultUrl: 'images/logo/android-chrome-192x192.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 192, + height: 192, + }, + }, + // eslint-disable-next-line @typescript-eslint/camelcase + favicon_512: { + label: 'android-chrome 512x512 (png)', + defaultUrl: 'images/logo/android-chrome-512x512.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 512, + height: 512, + }, + }, + // eslint-disable-next-line @typescript-eslint/camelcase + touchicon_180: { + label: 'apple-touch-icon 180x180 (png)', + defaultUrl: 'images/logo/apple-touch-icon.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 180, + height: 180, + }, + }, + // eslint-disable-next-line @typescript-eslint/camelcase + touchicon_180_pre: { + label: 'apple-touch-icon-precomposed 180x180 (png)', + defaultUrl: 'images/logo/apple-touch-icon-precomposed.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 180, + height: 180, + }, + }, + // eslint-disable-next-line @typescript-eslint/camelcase + tile_70: { + label: 'mstile 70x70 (png)', + defaultUrl: 'images/logo/mstile-70x70.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 70, + height: 70, + }, + }, + // eslint-disable-next-line @typescript-eslint/camelcase + tile_144: { + label: 'mstile 144x144 (png)', + defaultUrl: 'images/logo/mstile-144x144.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 144, + height: 144, + }, + }, + // eslint-disable-next-line @typescript-eslint/camelcase + tile_150: { + label: 'mstile 150x150 (png)', + defaultUrl: 'images/logo/mstile-150x150.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 150, + height: 150, + }, + }, + // eslint-disable-next-line @typescript-eslint/camelcase + tile_310_square: { + label: 'mstile 310x310 (png)', + defaultUrl: 'images/logo/mstile-310x310.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 310, + height: 310, + }, + }, + // eslint-disable-next-line @typescript-eslint/camelcase + tile_310_wide: { + label: 'mstile 310x150 (png)', + defaultUrl: 'images/logo/mstile-310x150.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 310, + height: 150, + }, + }, + // eslint-disable-next-line @typescript-eslint/camelcase + safari_pinned: { + label: 'safari pinned tab (svg)', + defaultUrl: 'images/logo/safari-pinned-tab.svg', + constraints: { + type: 'image', + extensions: ['svg'], + }, + }, +}; + +function getAssetByKey(key: string): IRocketChatAsset { + return assets[key as keyof IRocketChatAssets]; +} + +class RocketChatAssetsClass { + get assets(): IRocketChatAssets { + return assets; + } + + public setAsset(binaryContent: BufferEncoding, contentType: string, asset: string): void { + const assetInstance = getAssetByKey(asset); + if (!assetInstance) { + throw new Meteor.Error('error-invalid-asset', 'Invalid asset', { + function: 'RocketChat.Assets.setAsset', + }); + } + + const extension = getExtension(contentType); + if (assetInstance.constraints.extensions.includes(extension) === false) { + throw new Meteor.Error(contentType, `Invalid file type: ${contentType}`, { + function: 'RocketChat.Assets.setAsset', + errorTitle: 'error-invalid-file-type', + }); + } + + const file = Buffer.from(binaryContent, 'binary'); + if (assetInstance.constraints.width || assetInstance.constraints.height) { + const dimensions = sizeOf(file); + if (assetInstance.constraints.width && assetInstance.constraints.width !== dimensions.width) { + throw new Meteor.Error('error-invalid-file-width', 'Invalid file width', { + function: 'Invalid file width', + }); + } + if (assetInstance.constraints.height && assetInstance.constraints.height !== dimensions.height) { + throw new Meteor.Error('error-invalid-file-height'); + } + } + + const rs = RocketChatFile.bufferToStream(file); + RocketChatAssetsInstance.deleteFile(asset); + + const ws = RocketChatAssetsInstance.createWriteStream(asset, contentType); + ws.on( + 'end', + Meteor.bindEnvironment(function () { + return Meteor.setTimeout(function () { + const key = `Assets_${asset}`; + const value = { + url: `assets/${asset}.${extension}`, + defaultUrl: assetInstance.defaultUrl, + }; + + Settings.updateValueById(key, value); + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return RocketChatAssets.processAsset(key, value); + }, 200); + }), + ); + + rs.pipe(ws); + } + + public unsetAsset(asset: string): void { + if (!getAssetByKey(asset)) { + throw new Meteor.Error('error-invalid-asset', 'Invalid asset', { + function: 'RocketChat.Assets.unsetAsset', + }); + } + + RocketChatAssetsInstance.deleteFile(asset); + const key = `Assets_${asset}`; + const value = { + defaultUrl: getAssetByKey(asset).defaultUrl, + }; + + Settings.updateValueById(key, value); + // eslint-disable-next-line @typescript-eslint/no-use-before-define + RocketChatAssets.processAsset(key, value); + } + + public refreshClients(): boolean { + return (process.emit as Function)('message', { + refresh: 'client', + }); + } + + public processAsset(settingKey: string, settingValue: any): Record | undefined { + if (settingKey.indexOf('Assets_') !== 0) { + return; + } + + const assetKey = settingKey.replace(/^Assets_/, ''); + const assetValue = getAssetByKey(assetKey); + + if (!assetValue) { + return; + } + + if (!settingValue || !settingValue.url) { + assetValue.cache = undefined; + return; + } + + const file = RocketChatAssetsInstance.getFileSync(assetKey); + if (!file) { + assetValue.cache = undefined; + return; + } + + const hash = crypto.createHash('sha1').update(file.buffer).digest('hex'); + const extension = settingValue.url.split('.').pop(); + + assetValue.cache = { + path: `assets/${assetKey}.${extension}`, + cacheable: false, + sourceMapUrl: undefined, + where: 'client', + type: 'asset', + content: file.buffer, + extension, + url: `/assets/${assetKey}.${extension}?${hash}`, + size: file.length, + uploadDate: file.uploadDate, + contentType: file.contentType, + hash, + }; + + return assetValue.cache; + } + + public getURL(assetName: string, options = { cdn: false, full: true }): string { + const asset = settings.get(assetName); + const url = asset.url || asset.defaultUrl; + + return getURL(url, options); + } +} + +export const RocketChatAssets = new RocketChatAssetsClass(); + +settingsRegistry.addGroup('Assets', function () { + this.add('Assets_SvgFavicon_Enable', true, { + type: 'boolean', + group: 'Assets', + i18nLabel: 'Enable_Svg_Favicon', + }); +}); + +function addAssetToSetting(asset: string, value: IRocketChatAsset): void { + const key = `Assets_${asset}`; + + settingsRegistry.add( + key, + { + defaultUrl: value.defaultUrl, + }, + { + type: 'asset', + group: 'Assets', + fileConstraints: value.constraints, + i18nLabel: value.label, + asset, + public: true, + wizard: value.wizard, + }, + ); + + const currentValue = settings.get(key); + + if (typeof currentValue === 'object' && currentValue.defaultUrl !== getAssetByKey(asset).defaultUrl) { + currentValue.defaultUrl = getAssetByKey(asset).defaultUrl; + Settings.updateValueById(key, currentValue); + } +} + +for (const key of Object.keys(assets)) { + const value = getAssetByKey(key); + addAssetToSetting(key, value); +} + +settings.watchByRegex(/^Assets_/, (key, value) => RocketChatAssets.processAsset(key, value)); + +Meteor.startup(function () { + return Meteor.setTimeout(function () { + return (process.emit as Function)('message', { + refresh: 'client', + }); + }, 200); +}); + +const { calculateClientHash } = WebAppHashing; + +WebAppHashing.calculateClientHash = function (manifest: Record, includeFilter: Function, runtimeConfigOverride: any): string { + for (const key of Object.keys(assets)) { + const value = getAssetByKey(key); + if (!value.cache && !value.defaultUrl) { + continue; + } + + let cache = {}; + if (value.cache) { + cache = { + path: value.cache.path, + cacheable: value.cache.cacheable, + sourceMapUrl: value.cache.sourceMapUrl, + where: value.cache.where, + type: value.cache.type, + url: value.cache.url, + size: value.cache.size, + hash: value.cache.hash, + }; + } else { + const extension = value.defaultUrl?.split('.').pop(); + cache = { + path: `assets/${key}.${extension}`, + cacheable: false, + sourceMapUrl: undefined, + where: 'client', + type: 'asset', + url: `/assets/${key}.${extension}?v3`, + hash: 'v3', + }; + } + + const manifestItem = _.findWhere(manifest, { + path: key, + }); + + if (manifestItem) { + const index = manifest.indexOf(manifestItem); + manifest[index] = cache; + } else { + manifest.push(cache); + } + } + + return calculateClientHash.call(this, manifest, includeFilter, runtimeConfigOverride); +}; + +Meteor.methods({ + refreshClients() { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'refreshClients', + }); + } + + const _hasPermission = hasPermission(Meteor.userId() as string, 'manage-assets'); + if (!_hasPermission) { + throw new Meteor.Error('error-action-not-allowed', 'Managing assets not allowed', { + method: 'refreshClients', + action: 'Managing_assets', + }); + } + + return RocketChatAssets.refreshClients(); + }, + + unsetAsset(asset) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'unsetAsset', + }); + } + + const _hasPermission = hasPermission(Meteor.userId() as string, 'manage-assets'); + if (!_hasPermission) { + throw new Meteor.Error('error-action-not-allowed', 'Managing assets not allowed', { + method: 'unsetAsset', + action: 'Managing_assets', + }); + } + + return RocketChatAssets.unsetAsset(asset); + }, + + setAsset(binaryContent, contentType, asset) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'setAsset', + }); + } + + const _hasPermission = hasPermission(Meteor.userId() as string, 'manage-assets'); + if (!_hasPermission) { + throw new Meteor.Error('error-action-not-allowed', 'Managing assets not allowed', { + method: 'setAsset', + action: 'Managing_assets', + }); + } + + RocketChatAssets.setAsset(binaryContent, contentType, asset); + }, +}); + +const listener = Meteor.bindEnvironment((req: IncomingMessage, res: ServerResponse, next: NextHandleFunction) => { + if (!req.url) { + return; + } + const params = { + asset: decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, '')).replace(/\.[^.]*$/, ''), + }; + + const asset = getAssetByKey(params.asset); + const file = asset?.cache; + + const format = req.url.replace(/.*\.([a-z]+)(?:$|\?.*)/i, '$1'); + + if (asset && Array.isArray(asset.constraints.extensions) && !asset.constraints.extensions.includes(format)) { + res.writeHead(403); + return res.end(); + } + if (!file) { + const defaultUrl = asset?.defaultUrl; + if (defaultUrl) { + const assetUrl = format && ['png', 'svg'].includes(format) ? defaultUrl.replace(/(svg|png)$/, format) : defaultUrl; + req.url = `/${assetUrl}`; + WebAppInternals.staticFilesMiddleware((WebAppInternals as Record).staticFilesByArch, req, res, next); + } else { + res.writeHead(404); + res.end(); + } + + return; + } + + const reqModifiedHeader = req.headers['if-modified-since']; + if (reqModifiedHeader) { + if (reqModifiedHeader === (file.uploadDate && file.uploadDate.toUTCString())) { + res.setHeader('Last-Modified', reqModifiedHeader); + res.writeHead(304); + res.end(); + return; + } + } + + res.setHeader('Cache-Control', 'public, max-age=0'); + res.setHeader('Expires', '-1'); + + if (format && format !== file.extension && ['png', 'jpg', 'jpeg'].includes(format)) { + res.setHeader('Content-Type', `image/${format}`); + sharp(file.content) + .toFormat(format as any) + .pipe(res); + return; + } + + res.setHeader('Last-Modified', (file.uploadDate && file.uploadDate.toUTCString()) || new Date().toUTCString()); + res.setHeader('Content-Type', file.contentType); + res.setHeader('Content-Length', file.size); + res.writeHead(200); + res.end(file.content); +}); + +WebApp.connectHandlers.use('/assets/', listener); diff --git a/apps/meteor/app/assets/server/index.js b/apps/meteor/app/assets/server/index.ts similarity index 100% rename from apps/meteor/app/assets/server/index.js rename to apps/meteor/app/assets/server/index.ts diff --git a/apps/meteor/app/authentication/server/lib/restrictLoginAttempts.ts b/apps/meteor/app/authentication/server/lib/restrictLoginAttempts.ts index f9a5ff632e81..f49abd3ac68d 100644 --- a/apps/meteor/app/authentication/server/lib/restrictLoginAttempts.ts +++ b/apps/meteor/app/authentication/server/lib/restrictLoginAttempts.ts @@ -1,14 +1,14 @@ -import moment from 'moment'; import type { IServerEvent } from '@rocket.chat/core-typings'; import { ServerEventType } from '@rocket.chat/core-typings'; +import { Rooms, ServerEvents, Sessions, Users } from '@rocket.chat/models'; +import moment from 'moment'; -import { ILoginAttempt } from '../ILoginAttempt'; -import { ServerEvents, Users, Rooms, Sessions } from '../../../models/server/raw'; -import { settings } from '../../../settings/server'; import { addMinutesToADate } from '../../../../lib/utils/addMinutesToADate'; import { getClientAddress } from '../../../../server/lib/getClientAddress'; import { sendMessage } from '../../../lib/server/functions'; import { Logger } from '../../../logger/server'; +import { settings } from '../../../settings/server'; +import { ILoginAttempt } from '../ILoginAttempt'; const logger = new Logger('LoginProtection'); @@ -94,7 +94,8 @@ export const isValidAttemptByUser = async (login: ILoginAttempt): Promise { CachedCollectionManager.onLogin(async () => { - const { roles } = await APIClient.v1.get('roles.list'); + const { roles } = await APIClient.get('/v1/roles.list'); // if a role is checked before this collection is populated, it will return undefined Roles._collection._docs._map = new Map(roles.map((record) => [Roles._collection._docs._idStringify(record._id), record])); Object.values(Roles._collection.queries).forEach((query) => Roles._collection._recomputeResults(query)); diff --git a/apps/meteor/app/authorization/index.js b/apps/meteor/app/authorization/index.js index a67eca871efb..c20f7ea60706 100644 --- a/apps/meteor/app/authorization/index.js +++ b/apps/meteor/app/authorization/index.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; if (Meteor.isClient) { - module.exports = require('./client/index.js'); + module.exports = require('./client/'); } if (Meteor.isServer) { module.exports = require('./server/index.js'); diff --git a/apps/meteor/app/authorization/server/functions/canAccessRoom.ts b/apps/meteor/app/authorization/server/functions/canAccessRoom.ts index 7b0361f7719d..16e0a76b51e5 100644 --- a/apps/meteor/app/authorization/server/functions/canAccessRoom.ts +++ b/apps/meteor/app/authorization/server/functions/canAccessRoom.ts @@ -8,7 +8,6 @@ export const roomAccessAttributes = { t: 1, teamId: 1, prid: 1, - tokenpass: 1, }; export const canAccessRoom = (...args: Parameters): boolean => Promise.await(canAccessRoomAsync(...args)); diff --git a/apps/meteor/app/authorization/server/functions/canDeleteMessage.js b/apps/meteor/app/authorization/server/functions/canDeleteMessage.js deleted file mode 100644 index 1e5f2a577eef..000000000000 --- a/apps/meteor/app/authorization/server/functions/canDeleteMessage.js +++ /dev/null @@ -1,50 +0,0 @@ -import { hasPermissionAsync } from './hasPermission'; -import { getValue } from '../../../settings/server/raw'; -import { Rooms } from '../../../models'; - -const elapsedTime = (ts) => { - const dif = Date.now() - ts; - return Math.round(dif / 1000 / 60); -}; - -export const canDeleteMessageAsync = async (uid, { u, rid, ts }) => { - const forceDelete = await hasPermissionAsync(uid, 'force-delete-message', rid); - - if (forceDelete) { - return true; - } - - if (!ts) { - return false; - } - const deleteAllowed = await getValue('Message_AllowDeleting'); - - if (!deleteAllowed) { - return false; - } - - const allowedToDeleteAny = await hasPermissionAsync(uid, 'delete-message', rid); - - const allowed = allowedToDeleteAny || (uid === u._id && (await hasPermissionAsync(uid, 'delete-own-message', rid))); - if (!allowed) { - return false; - } - const blockDeleteInMinutes = await getValue('Message_AllowDeleting_BlockDeleteInMinutes'); - - if (blockDeleteInMinutes) { - const timeElapsedForMessage = elapsedTime(ts); - return timeElapsedForMessage <= blockDeleteInMinutes; - } - - const room = await Rooms.findOneById(rid, { fields: { ro: 1, unmuted: 1 } }); - if (room.ro === true && !(await hasPermissionAsync(uid, 'post-readonly', rid))) { - // Unless the user was manually unmuted - if (!(room.unmuted || []).includes(u.username)) { - throw new Error("You can't delete messages because the room is readonly."); - } - } - - return true; -}; - -export const canDeleteMessage = (uid, { u, rid, ts }) => Promise.await(canDeleteMessageAsync(uid, { u, rid, ts })); diff --git a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts new file mode 100644 index 000000000000..36d7b30394c9 --- /dev/null +++ b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts @@ -0,0 +1,53 @@ +import { IUser } from '@rocket.chat/core-typings'; + +import { hasPermissionAsync } from './hasPermission'; +import { getValue } from '../../../settings/server/raw'; +import { Rooms } from '../../../models/server'; + +const elapsedTime = (ts: number): number => { + const dif = Date.now() - ts; + return Math.round(dif / 1000 / 60); +}; + +export const canDeleteMessageAsync = async (uid: string, { u, rid, ts }: { u: IUser; rid: string; ts: number }): Promise => { + const forceDelete = await hasPermissionAsync(uid, 'force-delete-message', rid); + + if (forceDelete) { + return true; + } + + if (!ts) { + return false; + } + const deleteAllowed = await getValue('Message_AllowDeleting'); + + if (!deleteAllowed) { + return false; + } + + const allowedToDeleteAny = await hasPermissionAsync(uid, 'delete-message', rid); + + const allowed = allowedToDeleteAny || (uid === u._id && (await hasPermissionAsync(uid, 'delete-own-message', rid))); + if (!allowed) { + return false; + } + const blockDeleteInMinutes = await getValue('Message_AllowDeleting_BlockDeleteInMinutes'); + + if (blockDeleteInMinutes) { + const timeElapsedForMessage = elapsedTime(ts); + return timeElapsedForMessage <= blockDeleteInMinutes; + } + + const room = await Rooms.findOneById(rid, { fields: { ro: 1, unmuted: 1 } }); + if (room.ro === true && !(await hasPermissionAsync(uid, 'post-readonly', rid))) { + // Unless the user was manually unmuted + if (!(room.unmuted || []).includes(u.username)) { + throw new Error("You can't delete messages because the room is readonly."); + } + } + + return true; +}; + +export const canDeleteMessage = (uid: string, { u, rid, ts }: { u: IUser; rid: string; ts: number }): boolean => + Promise.await(canDeleteMessageAsync(uid, { u, rid, ts })); diff --git a/apps/meteor/app/authorization/server/functions/canSendMessage.ts b/apps/meteor/app/authorization/server/functions/canSendMessage.ts index d94da194fe22..59e1da2eb142 100644 --- a/apps/meteor/app/authorization/server/functions/canSendMessage.ts +++ b/apps/meteor/app/authorization/server/functions/canSendMessage.ts @@ -1,8 +1,8 @@ import { IRoom, IUser } from '@rocket.chat/core-typings'; +import { Subscriptions, Rooms } from '@rocket.chat/models'; import { canAccessRoomAsync } from './canAccessRoom'; import { hasPermissionAsync } from './hasPermission'; -import { Subscriptions, Rooms } from '../../../models/server/raw'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; @@ -14,7 +14,7 @@ const subscriptionOptions = { }; async function validateRoomMessagePermissionsAsync( - room: IRoom, + room: IRoom | null, { uid, username, type }: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] }, extraData: Record, ): Promise { @@ -51,6 +51,10 @@ export async function canSendMessageAsync( extraData: Record, ): Promise { const room = await Rooms.findOneById(rid); + if (!room) { + throw new Error('error-invalid-room'); + } + await validateRoomMessagePermissionsAsync(room, { uid, username, type }, extraData); return room; } diff --git a/apps/meteor/app/authorization/server/functions/getRoles.ts b/apps/meteor/app/authorization/server/functions/getRoles.ts index 5ace1e614527..657b546aaaae 100644 --- a/apps/meteor/app/authorization/server/functions/getRoles.ts +++ b/apps/meteor/app/authorization/server/functions/getRoles.ts @@ -1,5 +1,4 @@ import type { IRole } from '@rocket.chat/core-typings'; - -import { Roles } from '../../../models/server/raw'; +import { Roles } from '@rocket.chat/models'; export const getRoles = (): IRole[] => Promise.await(Roles.find().toArray()); diff --git a/apps/meteor/app/authorization/server/functions/getUsersInRole.ts b/apps/meteor/app/authorization/server/functions/getUsersInRole.ts index dee7e46ab2fa..b73bd133ba0f 100644 --- a/apps/meteor/app/authorization/server/functions/getUsersInRole.ts +++ b/apps/meteor/app/authorization/server/functions/getUsersInRole.ts @@ -1,26 +1,51 @@ -import { Cursor, FindOneOptions, WithoutProjection } from 'mongodb'; +import { FindCursor, FindOptions } from 'mongodb'; import type { IRole, IUser } from '@rocket.chat/core-typings'; +import { Roles, Subscriptions, Users } from '@rocket.chat/models'; +import { FindPaginated } from '@rocket.chat/model-typings'; +import { compact } from 'lodash'; -import { Roles } from '../../../models/server/raw'; +export function getUsersInRole(roleId: IRole['_id'], scope?: string): Promise>; -export function getUsersInRole(roleId: IRole['_id'], scope?: string): Promise>; +export function getUsersInRole(roleId: IRole['_id'], scope: string | undefined, options: FindOptions): Promise>; -export function getUsersInRole( +export function getUsersInRole

( roleId: IRole['_id'], scope: string | undefined, - options: WithoutProjection>, -): Promise>; + options: FindOptions

, +): Promise>; export function getUsersInRole

( roleId: IRole['_id'], scope: string | undefined, - options: FindOneOptions

, -): Promise>; + options?: any | undefined, +): Promise> { + // TODO move the code from Roles.findUsersInRole to here and change all places to use this function + return Roles.findUsersInRole(roleId, scope, options); +} -export function getUsersInRole

( +export async function getUsersInRolePaginated( roleId: IRole['_id'], scope: string | undefined, options?: any | undefined, -): Promise> { - return Roles.findUsersInRole(roleId, scope, options); +): Promise>> { + if (process.env.NODE_ENV === 'development' && (scope === 'Users' || scope === 'Subscriptions')) { + throw new Error('Roles.findUsersInRole method received a role scope instead of a scope value.'); + } + + const role = await Roles.findOneById>(roleId, { projection: { scope: 1 } }); + if (!role) { + throw new Error('role not found'); + } + + switch (role.scope) { + case 'Subscriptions': + const subscriptions = await Subscriptions.findByRolesAndRoomId({ roles: role._id, rid: scope }, { projection: { 'u._id': 1 } }) + .map((subscription) => subscription.u?._id) + .toArray(); + + return Users.findPaginated({ _id: { $in: compact(subscriptions) } }, options || {}); + case 'Users': + default: + return Users.findPaginatedUsersInRoles([role._id], options); + } } diff --git a/apps/meteor/app/authorization/server/functions/hasRole.ts b/apps/meteor/app/authorization/server/functions/hasRole.ts index 9057072a2787..040d28d8a905 100644 --- a/apps/meteor/app/authorization/server/functions/hasRole.ts +++ b/apps/meteor/app/authorization/server/functions/hasRole.ts @@ -1,6 +1,5 @@ import type { IRole, IUser, IRoom, ISubscription } from '@rocket.chat/core-typings'; - -import { Roles } from '../../../models/server/raw'; +import { Roles } from '@rocket.chat/models'; export const hasAnyRoleAsync = async ( userId: IUser['_id'], diff --git a/apps/meteor/app/authorization/server/functions/upsertPermissions.ts b/apps/meteor/app/authorization/server/functions/upsertPermissions.ts index 29ea924d7a18..837020591c01 100644 --- a/apps/meteor/app/authorization/server/functions/upsertPermissions.ts +++ b/apps/meteor/app/authorization/server/functions/upsertPermissions.ts @@ -1,9 +1,9 @@ /* eslint no-multi-spaces: 0 */ import type { IPermission, ISetting } from '@rocket.chat/core-typings'; +import { Permissions, Settings } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; import { getSettingPermissionId, CONSTANTS } from '../../lib'; -import { Permissions, Settings } from '../../../models/server/raw'; import { createOrUpdateProtectedRoleAsync } from '../../../../server/lib/roles/createOrUpdateProtectedRole'; export const upsertPermissions = async (): Promise => { @@ -224,6 +224,7 @@ export const upsertPermissions = async (): Promise => { { _id: 'remove-slackbridge-links', roles: ['admin'] }, { _id: 'view-import-operations', roles: ['admin'] }, { _id: 'clear-oembed-cache', roles: ['admin'] }, + { _id: 'videoconf-ring-users', roles: ['admin', 'owner', 'moderator', 'user'] }, ]; for await (const permission of permissions) { diff --git a/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts b/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts index 946a7813b226..8041cb8b7301 100644 --- a/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts +++ b/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; +import { Permissions } from '@rocket.chat/models'; import { hasPermission } from '../functions/hasPermission'; import { CONSTANTS, AuthorizationUtils } from '../../lib'; -import { Permissions } from '../../../models/server/raw'; Meteor.methods({ async 'authorization:addPermissionToRole'(permissionId, role) { diff --git a/apps/meteor/app/authorization/server/methods/addUserToRole.ts b/apps/meteor/app/authorization/server/methods/addUserToRole.ts index 072f5162fcbd..c33f950e0dbc 100644 --- a/apps/meteor/app/authorization/server/methods/addUserToRole.ts +++ b/apps/meteor/app/authorization/server/methods/addUserToRole.ts @@ -1,12 +1,12 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import type { IRole, IUser, IRoom } from '@rocket.chat/core-typings'; +import { Roles } from '@rocket.chat/models'; import { Users } from '../../../models/server'; import { settings } from '../../../settings/server'; import { hasPermission } from '../functions/hasPermission'; import { api } from '../../../../server/sdk/api'; -import { Roles } from '../../../models/server/raw'; import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ diff --git a/apps/meteor/app/authorization/server/methods/deleteRole.ts b/apps/meteor/app/authorization/server/methods/deleteRole.ts index 4d9fb5238fc9..71858b228705 100644 --- a/apps/meteor/app/authorization/server/methods/deleteRole.ts +++ b/apps/meteor/app/authorization/server/methods/deleteRole.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import type { IRole } from '@rocket.chat/core-typings'; +import { Roles } from '@rocket.chat/models'; -import { Roles } from '../../../models/server/raw'; import { hasPermission } from '../functions/hasPermission'; import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; diff --git a/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts b/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts index f690c29d5ea2..0d2ecf59333f 100644 --- a/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts +++ b/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; +import { Permissions } from '@rocket.chat/models'; import { hasPermission } from '../functions/hasPermission'; import { CONSTANTS } from '../../lib'; -import { Permissions } from '../../../models/server/raw'; Meteor.methods({ async 'authorization:removeRoleFromPermission'(permissionId, role) { diff --git a/apps/meteor/app/authorization/server/methods/removeUserFromRole.ts b/apps/meteor/app/authorization/server/methods/removeUserFromRole.ts index 996c17f8b910..3eeda9c2c84e 100644 --- a/apps/meteor/app/authorization/server/methods/removeUserFromRole.ts +++ b/apps/meteor/app/authorization/server/methods/removeUserFromRole.ts @@ -1,12 +1,12 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import type { IRole, IUser } from '@rocket.chat/core-typings'; +import { Roles } from '@rocket.chat/models'; import { Users } from '../../../models/server'; import { settings } from '../../../settings/server'; import { hasPermission } from '../functions/hasPermission'; import { api } from '../../../../server/sdk/api'; -import { Roles } from '../../../models/server/raw'; import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; Meteor.methods({ diff --git a/apps/meteor/app/authorization/server/methods/saveRole.ts b/apps/meteor/app/authorization/server/methods/saveRole.ts index 3b0517852865..9eb19298f7f7 100644 --- a/apps/meteor/app/authorization/server/methods/saveRole.ts +++ b/apps/meteor/app/authorization/server/methods/saveRole.ts @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { isRoleCreateProps } from '@rocket.chat/rest-typings'; +import { Roles } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; import { hasPermission } from '../functions/hasPermission'; -import { Roles } from '../../../models/server/raw'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { updateRoleAsync } from '../../../../server/lib/roles/updateRole'; import { insertRoleAsync } from '../../../../server/lib/roles/insertRole'; @@ -13,16 +13,16 @@ Meteor.methods({ methodDeprecationLogger.warn('authorization:saveRole will be deprecated in future versions of Rocket.Chat'); const userId = Meteor.userId(); - if (!userId || !hasPermission(userId, 'access-permissions')) { - throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { + if (!isRoleCreateProps(roleData)) { + throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.', { method: 'authorization:saveRole', - action: 'Accessing_permissions', }); } - if (!isRoleCreateProps(roleData)) { - throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.', { + if (!userId || !hasPermission(userId, 'access-permissions')) { + throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { method: 'authorization:saveRole', + action: 'Accessing_permissions', }); } diff --git a/apps/meteor/app/authorization/server/streamer/permissions/index.ts b/apps/meteor/app/authorization/server/streamer/permissions/index.ts index 881f4f44e7b5..866bec5fb407 100644 --- a/apps/meteor/app/authorization/server/streamer/permissions/index.ts +++ b/apps/meteor/app/authorization/server/streamer/permissions/index.ts @@ -1,7 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { check, Match } from 'meteor/check'; - -import { Permissions } from '../../../../models/server/raw'; +import { Permissions } from '@rocket.chat/models'; Meteor.methods({ async 'permissions/get'(updatedAt: Date) { diff --git a/apps/meteor/app/autotranslate/client/lib/actionButton.ts b/apps/meteor/app/autotranslate/client/lib/actionButton.ts index 772917208147..0d742d68f9ab 100644 --- a/apps/meteor/app/autotranslate/client/lib/actionButton.ts +++ b/apps/meteor/app/autotranslate/client/lib/actionButton.ts @@ -6,7 +6,7 @@ import { AutoTranslate } from './autotranslate'; import { settings } from '../../../settings/client'; import { hasAtLeastOnePermission } from '../../../authorization/client'; import { MessageAction } from '../../../ui-utils/client/lib/MessageAction'; -import { messageArgs } from '../../../ui-utils/client/lib/messageArgs'; +import { messageArgs } from '../../../../client/lib/utils/messageArgs'; import { Messages } from '../../../models/client'; Meteor.startup(() => { @@ -32,7 +32,7 @@ Meteor.startup(() => { Messages.update({ _id: message._id }, { [action]: { autoTranslateShowInverse: true } }); }, condition({ message, user }) { - return Boolean(message?.u && message.u._id !== user._id && isTranslatedMessage(message) && !message.translations.original); + return Boolean(message?.u && message.u._id !== user._id && isTranslatedMessage(message) && message.autoTranslateShowInverse); }, order: 90, }); @@ -54,7 +54,7 @@ Meteor.startup(() => { Messages.update({ _id: message._id }, { [action]: { autoTranslateShowInverse: true } }); }, condition({ message, user }) { - return Boolean(message?.u && message.u._id !== user._id && isTranslatedMessage(message) && message.translations.original); + return Boolean(message?.u && message.u._id !== user._id && isTranslatedMessage(message) && !message.autoTranslateShowInverse); }, order: 90, }); diff --git a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts index d7f8338de98f..20ead8d84dea 100644 --- a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts +++ b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import _ from 'underscore'; import mem from 'mem'; import { IRoom, ISubscription, ISupportedLanguage, ITranslatedMessage, IUser, MessageAttachmentDefault } from '@rocket.chat/core-typings'; @@ -24,7 +23,7 @@ Meteor.startup(() => { export const AutoTranslate = { initialized: false, - providersMetadata: {}, + providersMetadata: {} as { [providerNamer: string]: { name: string; displayName: string } }, messageIdsToWait: {} as { [messageId: string]: string }, supportedLanguages: [] as ISupportedLanguage[], @@ -37,22 +36,38 @@ export const AutoTranslate = { } const language = (subscription?.autoTranslateLanguage || userLanguage || window.defaultUserLanguage?.()) as string; if (language.indexOf('-') !== -1) { - if (!_.findWhere(this.supportedLanguages, { language })) { - return language.substr(0, 2); + if (!this.supportedLanguages.some((supportedLanguage) => supportedLanguage.language === language)) { + return language.slice(0, 2); } } return language; }, - translateAttachments(attachments: MessageAttachmentDefault[], language: string): MessageAttachmentDefault[] { + translateAttachments( + attachments: MessageAttachmentDefault[], + language: string, + autoTranslateShowInverse: boolean, + ): MessageAttachmentDefault[] { for (const attachment of attachments) { if (attachment.author_name !== username) { if (attachment.text && attachment.translations && attachment.translations[language]) { - attachment.text = attachment.translations[language]; + attachment.translations.original = attachment.text; + + if (autoTranslateShowInverse) { + attachment.text = attachment.translations.original; + } else { + attachment.text = attachment.translations[language]; + } } if (attachment.description && attachment.translations && attachment.translations[language]) { - attachment.description = attachment.translations[language]; + attachment.translations.original = attachment.description; + + if (autoTranslateShowInverse) { + attachment.description = attachment.translations.original; + } else { + attachment.description = attachment.translations[language]; + } } // @ts-expect-error - not sure what to do with this @@ -107,17 +122,33 @@ export const createAutoTranslateMessageRenderer = (): ((message: ITranslatedMess message.translations = {}; } if (!!subscription?.autoTranslate !== !!message.autoTranslateShowInverse) { + const hasAttachmentsTranslate = + message.attachments?.some( + (attachment) => + 'translations' in attachment && + typeof attachment.translations === 'object' && + autoTranslateLanguage in attachment.translations, + ) ?? false; + message.translations.original = message.html; - if (message.translations[autoTranslateLanguage]) { + if (message.translations[autoTranslateLanguage] && !hasAttachmentsTranslate) { message.html = message.translations[autoTranslateLanguage]; } if (message.attachments && message.attachments.length > 0) { - message.attachments = AutoTranslate.translateAttachments(message.attachments, autoTranslateLanguage); + message.attachments = AutoTranslate.translateAttachments( + message.attachments, + autoTranslateLanguage, + !!message.autoTranslateShowInverse, + ); } } } else if (message.attachments && message.attachments.length > 0) { - message.attachments = AutoTranslate.translateAttachments(message.attachments, autoTranslateLanguage); + message.attachments = AutoTranslate.translateAttachments( + message.attachments, + autoTranslateLanguage, + !!message.autoTranslateShowInverse, + ); } return message; }; diff --git a/apps/meteor/app/autotranslate/server/autotranslate.ts b/apps/meteor/app/autotranslate/server/autotranslate.ts index a5f550d777d6..d50cb15a1457 100644 --- a/apps/meteor/app/autotranslate/server/autotranslate.ts +++ b/apps/meteor/app/autotranslate/server/autotranslate.ts @@ -308,6 +308,7 @@ export abstract class AutoTranslate { const translations = this._translateAttachmentDescriptions(attachment, targetLanguages); if (!_.isEmpty(translations)) { Messages.addAttachmentTranslations(message._id, index, translations); + Messages.addTranslations(message._id, translations, TranslationProviderRegistry[Provider]); } } } diff --git a/apps/meteor/app/autotranslate/server/googleTranslate.ts b/apps/meteor/app/autotranslate/server/googleTranslate.ts index 13a861532e38..e519f56fdbb8 100644 --- a/apps/meteor/app/autotranslate/server/googleTranslate.ts +++ b/apps/meteor/app/autotranslate/server/googleTranslate.ts @@ -94,7 +94,7 @@ class GoogleAutoTranslate extends AutoTranslate { result = HTTP.get('https://translation.googleapis.com/language/translate/v2/languages', { params, }); - } catch (e) { + } catch (e: any) { // Fallback: Get the English names of the target languages if ( e.response && diff --git a/apps/meteor/app/autotranslate/server/permissions.ts b/apps/meteor/app/autotranslate/server/permissions.ts index cf712cc6d552..f75edf705c24 100644 --- a/apps/meteor/app/autotranslate/server/permissions.ts +++ b/apps/meteor/app/autotranslate/server/permissions.ts @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; - -import { Permissions } from '../../models/server/raw'; +import { Permissions } from '@rocket.chat/models'; Meteor.startup(async () => { if (!(await Permissions.findOne({ _id: 'auto-translate' }))) { diff --git a/apps/meteor/app/bigbluebutton/server/bigbluebutton-api.js b/apps/meteor/app/bigbluebutton/server/bigbluebutton-api.js deleted file mode 100644 index 8cb3f4d447c4..000000000000 --- a/apps/meteor/app/bigbluebutton/server/bigbluebutton-api.js +++ /dev/null @@ -1,188 +0,0 @@ -/* eslint-disable */ -import crypto from 'crypto'; -import { SystemLogger } from '../../../server/lib/logger/system'; - -var BigBlueButtonApi, filterCustomParameters, include, noChecksumMethods, - __indexOf = [].indexOf || function (item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; - -BigBlueButtonApi = (function () { - function BigBlueButtonApi(url, salt, debug, opts) { - var _base; - if (opts == null) { - opts = {}; - } - this.url = url; - this.salt = salt; - this.opts = opts; - if ((_base = this.opts).shaType == null) { - _base.shaType = 'sha1'; - } - } - - BigBlueButtonApi.prototype.availableApiCalls = function () { - return ['/', 'create', 'join', 'isMeetingRunning', 'getMeetingInfo', 'end', 'getMeetings', 'getDefaultConfigXML', 'setConfigXML', 'enter', 'configXML', 'signOut', 'getRecordings', 'publishRecordings', 'deleteRecordings', 'updateRecordings', 'hooks/create']; - }; - - BigBlueButtonApi.prototype.urlParamsFor = function (param) { - switch (param) { - case "create": - return [["meetingID", true], ["name", true], ["attendeePW", false], ["moderatorPW", false], ["welcome", false], ["dialNumber", false], ["voiceBridge", false], ["webVoice", false], ["logoutURL", false], ["maxParticipants", false], ["record", false], ["duration", false], ["moderatorOnlyMessage", false], ["autoStartRecording", false], ["allowStartStopRecording", false], [/meta_\w+/, false]]; - case "join": - return [["fullName", true], ["meetingID", true], ["password", true], ["createTime", false], ["userID", false], ["webVoiceConf", false], ["configToken", false], ["avatarURL", false], ["redirect", false], ["clientURL", false]]; - case "isMeetingRunning": - return [["meetingID", true]]; - case "end": - return [["meetingID", true], ["password", true]]; - case "getMeetingInfo": - return [["meetingID", true], ["password", true]]; - case "getRecordings": - return [["meetingID", false], ["recordID", false], ["state", false], [/meta_\w+/, false]]; - case "publishRecordings": - return [["recordID", true], ["publish", true]]; - case "deleteRecordings": - return [["recordID", true]]; - case "updateRecordings": - return [["recordID", true], [/meta_\w+/, false]]; - case "hooks/create": - return [["callbackURL", false], ["meetingID", false]]; - } - }; - - BigBlueButtonApi.prototype.filterParams = function (params, method) { - var filters, r; - filters = this.urlParamsFor(method); - if ((filters == null) || filters.length === 0) { - ({}); - } else { - r = include(params, function (key, value) { - var filter, _i, _len; - for (_i = 0, _len = filters.length; _i < _len; _i++) { - filter = filters[_i]; - if (filter[0] instanceof RegExp) { - if (key.match(filter[0]) || key.match(/^custom_/)) { - return true; - } - } else { - if (key.match("^" + filter[0] + "$") || key.match(/^custom_/)) { - return true; - } - } - } - return false; - }); - } - return filterCustomParameters(r); - }; - - BigBlueButtonApi.prototype.urlFor = function (method, params, filter) { - var checksum, key, keys, param, paramList, property, query, sep, url, _i, _len; - if (filter == null) { - filter = true; - } - SystemLogger.debug("Generating URL for", method); - if (filter) { - params = this.filterParams(params, method); - } else { - params = filterCustomParameters(params); - } - url = this.url; - paramList = []; - if (params != null) { - keys = []; - for (property in params) { - keys.push(property); - } - keys = keys.sort(); - for (_i = 0, _len = keys.length; _i < _len; _i++) { - key = keys[_i]; - if (key != null) { - param = params[key]; - } - if (param != null) { - paramList.push("" + (this.encodeForUrl(key)) + "=" + (this.encodeForUrl(param))); - } - } - if (paramList.length > 0) { - query = paramList.join("&"); - } - } else { - query = ''; - } - checksum = this.checksum(method, query); - if (paramList.length > 0) { - query = "" + method + "?" + query; - sep = '&'; - } else { - if (method !== '/') { - query = method; - } - sep = '?'; - } - if (__indexOf.call(noChecksumMethods(), method) < 0) { - query = "" + query + sep + "checksum=" + checksum; - } - return "" + url + "/" + query; - }; - - BigBlueButtonApi.prototype.checksum = function (method, query) { - var c, shaObj, str; - query || (query = ""); - SystemLogger.debug("- Calculating the checksum using: '" + method + "', '" + query + "', '" + this.salt + "'"); - str = method + query + this.salt; - if (this.opts.shaType === 'sha256') { - shaObj = crypto.createHash('sha256', "TEXT") - } else { - shaObj = crypto.createHash('sha1', "TEXT") - } - shaObj.update(str); - c = shaObj.digest('hex'); - SystemLogger.debug("- Checksum calculated:", c); - return c; - }; - - BigBlueButtonApi.prototype.encodeForUrl = function (value) { - return encodeURIComponent(value).replace(/%20/g, '+').replace(/[!'()]/g, escape).replace(/\*/g, "%2A"); - }; - - BigBlueButtonApi.prototype.setMobileProtocol = function (url) { - return url.replace(/http[s]?\:\/\//, "bigbluebutton://"); - }; - - return BigBlueButtonApi; - -})(); - -include = function (input, _function) { - var key, value, _match, _obj; - _obj = new Object; - _match = null; - for (key in input) { - value = input[key]; - if (_function.call(input, key, value)) { - _obj[key] = value; - } - } - return _obj; -}; - -export default BigBlueButtonApi; - -filterCustomParameters = function (params) { - var key, v; - for (key in params) { - v = params[key]; - if (key.match(/^custom_/)) { - params[key.replace(/^custom_/, "")] = v; - } - } - for (key in params) { - if (key.match(/^custom_/)) { - delete params[key]; - } - } - return params; -}; - -noChecksumMethods = function () { - return ['setConfigXML', '/', 'enter', 'configXML', 'signOut']; -}; diff --git a/apps/meteor/app/bigbluebutton/server/index.js b/apps/meteor/app/bigbluebutton/server/index.js deleted file mode 100644 index b6be696a20bd..000000000000 --- a/apps/meteor/app/bigbluebutton/server/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './bigbluebutton-api'; diff --git a/apps/meteor/app/blockstack/client/index.js b/apps/meteor/app/blockstack/client/index.js deleted file mode 100644 index 23420dbe4d70..000000000000 --- a/apps/meteor/app/blockstack/client/index.js +++ /dev/null @@ -1,53 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { ServiceConfiguration } from 'meteor/service-configuration'; -import { check, Match } from 'meteor/check'; -import { Session } from 'meteor/session'; -import './routes'; - -const handleError = (error) => error && Session.set('errorMessage', error.reason || 'Unknown error'); - -// TODO: allow serviceConfig.loginStyle == popup -Meteor.loginWithBlockstack = (options, callback = handleError) => { - if (!options || !options.redirectURI) { - options = ServiceConfiguration.configurations.findOne({ - service: 'blockstack', - }); - - options.blockstackIDHost = Meteor.Device.isDesktop() ? 'http://localhost:8888/auth' : 'https://blockstack.org/auth'; - - options.scopes = ['store_write']; - } - - try { - check( - options, - Match.ObjectIncluding({ - blockstackIDHost: String, - redirectURI: String, - manifestURI: String, - }), - ); - - import('blockstack/dist/blockstack').then(({ redirectToSignIn }) => - redirectToSignIn(options.redirectURI, options.manifestURI, options.scopes), - ); - } catch (err) { - callback.call(Meteor, err); - } -}; - -const meteorLogout = Meteor.logout; -Meteor.logout = (...args) => { - const serviceConfig = ServiceConfiguration.configurations.findOne({ - service: 'blockstack', - }); - - const blockstackAuth = Session.get('blockstack_auth'); - - if (serviceConfig && blockstackAuth) { - Session.delete('blockstack_auth'); - import('blockstack/dist/blockstack').then(({ signUserOut }) => signUserOut(window.location.href)); - } - - return meteorLogout(...args); -}; diff --git a/apps/meteor/app/blockstack/client/routes.js b/apps/meteor/app/blockstack/client/routes.js deleted file mode 100644 index 1f0e340c5eda..000000000000 --- a/apps/meteor/app/blockstack/client/routes.js +++ /dev/null @@ -1,46 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Accounts } from 'meteor/accounts-base'; -import { FlowRouter } from 'meteor/kadira:flow-router'; - -const blockstackLogin = (authResponse, userData = {}) => { - Accounts.callLoginMethod({ - methodArguments: [ - { - blockstack: true, - authResponse, - userData, - }, - ], - userCallback() { - FlowRouter.go('home'); - }, - }); -}; - -FlowRouter.route('/_blockstack/validate', { - name: 'blockstackValidate', - async action(params, queryParams) { - const blockstack = await import('blockstack/dist/blockstack'); - - if (Meteor.userId()) { - console.log('Blockstack Auth requested when already logged in. Reloading.'); - return FlowRouter.go('home'); - } - - if (queryParams.authResponse == null) { - throw new Meteor.Error('Blockstack: Auth request without response param.'); - } - - let userData; - - if (blockstack.isUserSignedIn()) { - userData = blockstack.loadUserData(); - } - - if (blockstack.isSignInPending()) { - userData = await blockstack.handlePendingSignIn(); - } - - blockstackLogin(queryParams.authResponse, userData); - }, -}); diff --git a/apps/meteor/app/blockstack/server/index.js b/apps/meteor/app/blockstack/server/index.js deleted file mode 100644 index f0cf809aaf0e..000000000000 --- a/apps/meteor/app/blockstack/server/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import './routes.js'; -import './settings.js'; -import './loginHandler.js'; diff --git a/apps/meteor/app/blockstack/server/logger.js b/apps/meteor/app/blockstack/server/logger.js deleted file mode 100644 index e88f4df9bf1c..000000000000 --- a/apps/meteor/app/blockstack/server/logger.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Logger } from '../../logger'; - -export const logger = new Logger('Blockstack'); diff --git a/apps/meteor/app/blockstack/server/loginHandler.js b/apps/meteor/app/blockstack/server/loginHandler.js deleted file mode 100644 index c1f50416d5c8..000000000000 --- a/apps/meteor/app/blockstack/server/loginHandler.js +++ /dev/null @@ -1,56 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Accounts } from 'meteor/accounts-base'; - -import { updateOrCreateUser } from './userHandler'; -import { handleAccessToken } from './tokenHandler'; -import { logger } from './logger'; -import { settings } from '../../settings/server'; -import { Users } from '../../models'; -import { setUserAvatar } from '../../lib'; - -// Blockstack login handler, triggered by a blockstack authResponse in route -Accounts.registerLoginHandler('blockstack', (loginRequest) => { - if (!loginRequest.blockstack || !loginRequest.authResponse) { - return; - } - - if (!settings.get('Blockstack_Enable')) { - return; - } - - logger.debug('Processing login request', loginRequest); - - const auth = handleAccessToken(loginRequest); - - // TODO: Fix #9484 and re-instate usage of accounts helper - // const result = Accounts.updateOrCreateUserFromExternalService('blockstack', auth.serviceData, auth.options) - const result = updateOrCreateUser(auth.serviceData, auth.options); - logger.debug('User create/update result', result); - - // Ensure processing succeeded - if (result === undefined || result.userId === undefined) { - return { - type: 'blockstack', - error: new Meteor.Error(Accounts.LoginCancelledError.numericError, 'User creation failed from Blockstack response token'), - }; - } - - if (result.isNew) { - try { - const user = Users.findOneById(result.userId, { - fields: { 'services.blockstack.image': 1, 'username': 1 }, - }); - if (user && user.services && user.services.blockstack && user.services.blockstack.image) { - Meteor.runAsUser(user._id, () => { - setUserAvatar(user, user.services.blockstack.image, undefined, 'url'); - }); - } - } catch (e) { - logger.error(e); - } - } - - delete result.isNew; - - return result; -}); diff --git a/apps/meteor/app/blockstack/server/routes.js b/apps/meteor/app/blockstack/server/routes.js deleted file mode 100644 index 9f6bc061f787..000000000000 --- a/apps/meteor/app/blockstack/server/routes.js +++ /dev/null @@ -1,31 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { WebApp } from 'meteor/webapp'; - -import { settings } from '../../settings/server'; -import { RocketChatAssets } from '../../assets/server'; - -WebApp.connectHandlers.use( - '/_blockstack/manifest', - Meteor.bindEnvironment(function (req, res) { - const name = settings.get('Site_Name'); - const startUrl = Meteor.absoluteUrl(); - const description = settings.get('Blockstack_Auth_Description'); - const iconUrl = RocketChatAssets.getURL('Assets_favicon_192'); - - res.writeHead(200, { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - }); - - res.end(`{ - "name": "${name}", - "start_url": "${startUrl}", - "description": "${description}", - "icons": [{ - "src": "${iconUrl}", - "sizes": "192x192", - "type": "image/png" - }] - }`); - }), -); diff --git a/apps/meteor/app/blockstack/server/settings.js b/apps/meteor/app/blockstack/server/settings.js deleted file mode 100644 index 640e35187a6b..000000000000 --- a/apps/meteor/app/blockstack/server/settings.js +++ /dev/null @@ -1,70 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { ServiceConfiguration } from 'meteor/service-configuration'; - -import { logger } from './logger'; -import { settings, settingsRegistry } from '../../settings/server'; - -const defaults = { - enable: false, - loginStyle: 'redirect', - generateUsername: false, - manifestURI: Meteor.absoluteUrl('_blockstack/manifest'), - redirectURI: Meteor.absoluteUrl('_blockstack/validate'), - authDescription: 'Rocket.Chat login', - buttonLabelText: 'Blockstack', - buttonColor: '#271132', - buttonLabelColor: '#ffffff', -}; - -Meteor.startup(() => { - settingsRegistry.addGroup('Blockstack', function () { - this.add('Blockstack_Enable', defaults.enable, { - type: 'boolean', - i18nLabel: 'Enable', - }); - this.add('Blockstack_Auth_Description', defaults.authDescription, { - type: 'string', - }); - this.add('Blockstack_ButtonLabelText', defaults.buttonLabelText, { - type: 'string', - }); - this.add('Blockstack_Generate_Username', defaults.generateUsername, { - type: 'boolean', - }); - }); -}); - -// Helper to return all Blockstack settings -const getSettings = () => - Object.assign({}, defaults, { - enable: settings.get('Blockstack_Enable'), - authDescription: settings.get('Blockstack_Auth_Description'), - buttonLabelText: settings.get('Blockstack_ButtonLabelText'), - generateUsername: settings.get('Blockstack_Generate_Username'), - }); - -// Add settings to auth provider configs on startup -settings.watchMultiple( - ['Blockstack_Enable', 'Blockstack_Auth_Description', 'Blockstack_ButtonLabelText', 'Blockstack_Generate_Username'], - () => { - const serviceConfig = getSettings(); - - if (!serviceConfig.enable) { - logger.debug('Blockstack not enabled', serviceConfig); - return ServiceConfiguration.configurations.remove({ - service: 'blockstack', - }); - } - - ServiceConfiguration.configurations.upsert( - { - service: 'blockstack', - }, - { - $set: serviceConfig, - }, - ); - - logger.debug('Init Blockstack auth', serviceConfig); - }, -); diff --git a/apps/meteor/app/blockstack/server/tokenHandler.js b/apps/meteor/app/blockstack/server/tokenHandler.js deleted file mode 100644 index 29079e3eb0a3..000000000000 --- a/apps/meteor/app/blockstack/server/tokenHandler.js +++ /dev/null @@ -1,63 +0,0 @@ -import { decodeToken } from 'blockstack'; -import { Meteor } from 'meteor/meteor'; -import { Accounts } from 'meteor/accounts-base'; -import { Match, check } from 'meteor/check'; - -import { logger } from './logger'; - -// Handler extracts data from JSON and tokenised reponse. -// Reflects OAuth token service, with some slight modifications for Blockstack. -// -// Uses 'iss' (issuer) as unique key (decentralised ID) for user. -// The 'did' final portion of the blockstack decentralised ID, is displayed as -// your profile ID in the service. This isn't used yet, but could be useful -// to link accounts if identity providers other than btc address are added. -export const handleAccessToken = (loginRequest) => { - logger.debug('Login request received', loginRequest); - - check( - loginRequest, - Match.ObjectIncluding({ - authResponse: String, - userData: Object, - }), - ); - - // Decode auth response for user attributes - const { username, profile } = loginRequest.userData; - const decodedToken = decodeToken(loginRequest.authResponse).payload; - - profile.username = username; - - logger.debug('User data', loginRequest.userData); - logger.debug('Login decoded', decodedToken); - - const { iss, iat, exp } = decodedToken; - - if (!iss) { - return { - type: 'blockstack', - error: new Meteor.Error(Accounts.LoginCancelledError.numericError, 'Insufficient data in auth response token'), - }; - } - - // Collect basic auth provider details - const serviceData = { - id: iss, - did: iss.split(':').pop(), - issuedAt: new Date(iat * 1000), - expiresAt: new Date(exp * 1000), - }; - - // Add Avatar image source to use for auth service suggestions - if (Array.isArray(profile.image) && profile.image.length) { - serviceData.image = profile.image[0].contentUrl; - } - - logger.debug('Login data', serviceData, profile); - - return { - serviceData, - options: { profile }, - }; -}; diff --git a/apps/meteor/app/blockstack/server/userHandler.js b/apps/meteor/app/blockstack/server/userHandler.js deleted file mode 100644 index 393129f74a99..000000000000 --- a/apps/meteor/app/blockstack/server/userHandler.js +++ /dev/null @@ -1,81 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Accounts } from 'meteor/accounts-base'; -import { ServiceConfiguration } from 'meteor/service-configuration'; - -import { logger } from './logger'; -import { settings } from '../../settings/server'; -import { generateUsernameSuggestion } from '../../lib'; - -// Updates or creates a user after we authenticate with Blockstack -// Clones Accounts.updateOrCreateUserFromExternalService with some modifications -export const updateOrCreateUser = (serviceData, options) => { - const serviceConfig = ServiceConfiguration.configurations.findOne({ service: 'blockstack' }); - logger.debug('Auth config', serviceConfig); - - // Extract user data from service / token - const { id, did } = serviceData; - const { profile } = options; - - // Look for existing Blockstack user - const user = Meteor.users.findOne({ 'services.blockstack.id': id }); - let userId; - let isNew = false; - - // Use found or create a user - if (user) { - logger.info(`User login with Blockstack ID ${id}`); - userId = user._id; - } else { - isNew = true; - let emails = []; - if (!Array.isArray(profile.emails)) { - // Fix absense of emails by adding placeholder address using decentralised - // ID at blockstack.email - a holding domain only, no MX record, does not - // process email, may be used in future to provide decentralised email via - // gaia, encrypting mail for DID user only. @TODO: document this approach. - emails.push({ address: `${did}@blockstack.email`, verified: false }); - } else { - const verified = settings.get('Accounts_Verify_Email_For_External_Accounts'); - // Reformat array of emails into expected format if they exist - emails = profile.emails.map((address) => ({ address, verified })); - } - - const newUser = { - name: profile.name, - active: true, - emails, - services: { blockstack: serviceData }, - }; - - // Set username same as in blockstack, or suggest if none - if (profile.name) { - newUser.name = profile.name; - } - - // Take profile username if exists, or generate one if enabled - if (profile.username && profile.username !== '') { - newUser.username = profile.username; - } else if (serviceConfig.generateUsername === true) { - newUser.username = generateUsernameSuggestion(newUser); - } - // If no username at this point it will suggest one from the name - - // Create and get created user to make a couple more mods before returning - logger.info(`Creating user for Blockstack ID ${id}`); - userId = Accounts.insertUserDoc({}, newUser); - logger.debug('New user ${ userId }', newUser); - } - - // Add login token for blockstack auth session (take expiration from response) - // TODO: Regquired method result format ignores `.when` - const { token } = Accounts._generateStampedLoginToken(); - const tokenExpires = serviceData.expiresAt; - - return { - type: 'blockstack', - userId, - token, - tokenExpires, - isNew, - }; -}; diff --git a/apps/meteor/app/cas/server/cas_server.js b/apps/meteor/app/cas/server/cas_server.js index 9ba95cd943bf..cf0a273c4fcc 100644 --- a/apps/meteor/app/cas/server/cas_server.js +++ b/apps/meteor/app/cas/server/cas_server.js @@ -6,12 +6,12 @@ import { WebApp } from 'meteor/webapp'; import { RoutePolicy } from 'meteor/routepolicy'; import _ from 'underscore'; import fiber from 'fibers'; -import CAS from 'cas'; +import { CredentialTokens } from '@rocket.chat/models'; +import { validate } from '@rocket.chat/cas-validate'; import { logger } from './cas_rocketchat'; -import { settings } from '../../settings'; +import { settings } from '../../settings/server'; import { Rooms } from '../../models/server'; -import { CredentialTokens } from '../../models/server/raw'; import { _setRealName } from '../../lib'; import { createRoom } from '../../lib/server/functions/createRoom'; @@ -38,13 +38,12 @@ const casTicket = function (req, token, callback) { const appUrl = Meteor.absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX; logger.debug(`Using CAS_base_url: ${baseUrl}`); - const cas = new CAS({ - base_url: baseUrl, - version: cas_version, - service: `${appUrl}/_cas/${token}`, - }); - - cas.validate( + validate( + { + base_url: baseUrl, + version: cas_version, + service: `${appUrl}/_cas/${token}`, + }, ticketId, Meteor.bindEnvironment(async function (err, status, username, details) { if (err) { diff --git a/apps/meteor/app/channel-settings/server/functions/saveReactWhenReadOnly.js b/apps/meteor/app/channel-settings/server/functions/saveReactWhenReadOnly.js index d4a6898678d6..8b118c51f01e 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveReactWhenReadOnly.js +++ b/apps/meteor/app/channel-settings/server/functions/saveReactWhenReadOnly.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Rooms, Messages } from '../../../models'; +import { Rooms, Messages } from '../../../models/server'; export const saveReactWhenReadOnly = function (rid, allowReact, user, sendMessage = true) { if (!Match.test(rid, String)) { diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomAnnouncement.js b/apps/meteor/app/channel-settings/server/functions/saveRoomAnnouncement.js index 38a5f56d612a..b3f66bf311a0 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomAnnouncement.js +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomAnnouncement.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Rooms, Messages } from '../../../models'; +import { Rooms, Messages } from '../../../models/server'; export const saveRoomAnnouncement = function (rid, roomAnnouncement, user, sendMessage = true) { if (!Match.test(rid, String)) { diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomCustomFields.js b/apps/meteor/app/channel-settings/server/functions/saveRoomCustomFields.js index 5691cd69b169..c246e01224ba 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomCustomFields.js +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomCustomFields.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Rooms, Subscriptions } from '../../../models'; +import { Rooms, Subscriptions } from '../../../models/server'; export const saveRoomCustomFields = function (rid, roomCustomFields) { if (!Match.test(rid, String)) { diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomDescription.js b/apps/meteor/app/channel-settings/server/functions/saveRoomDescription.js index 0804175f8e2a..dd6e0fb3ee1b 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomDescription.js +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomDescription.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Rooms, Messages } from '../../../models'; +import { Rooms, Messages } from '../../../models/server'; export const saveRoomDescription = function (rid, roomDescription, user) { if (!Match.test(rid, String)) { diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts index f0ccf1eb2164..7d6a906ab8f1 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts @@ -1,11 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import type { WriteOpResult } from 'mongodb'; +import type { UpdateResult } from 'mongodb'; import type { IUser } from '@rocket.chat/core-typings'; import { Rooms, Messages } from '../../../models/server'; -export const saveRoomEncrypted = function (rid: string, encrypted: boolean, user: IUser, sendMessage = true): Promise { +export const saveRoomEncrypted = function (rid: string, encrypted: boolean, user: IUser, sendMessage = true): Promise { if (!Match.test(rid, String)) { throw new Meteor.Error('invalid-room', 'Invalid room', { function: 'RocketChat.saveRoomEncrypted', diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomName.js b/apps/meteor/app/channel-settings/server/functions/saveRoomName.js index 05615a865075..941f8e86fcfc 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomName.js +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomName.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; +import { Integrations } from '@rocket.chat/models'; import { Rooms, Messages, Subscriptions } from '../../../models/server'; -import { Integrations } from '../../../models/server/raw'; import { getValidRoomName } from '../../../utils/server'; import { callbacks } from '../../../../lib/callbacks'; import { checkUsernameAvailability } from '../../../lib/server/functions'; diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomReadOnly.js b/apps/meteor/app/channel-settings/server/functions/saveRoomReadOnly.js index 7b7ed7bd0404..7cdb614e0679 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomReadOnly.js +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomReadOnly.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Rooms, Messages } from '../../../models'; +import { Rooms, Messages } from '../../../models/server'; export const saveRoomReadOnly = function (rid, readOnly, user, sendMessage = true) { if (!Match.test(rid, String)) { diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomSystemMessages.js b/apps/meteor/app/channel-settings/server/functions/saveRoomSystemMessages.js index 200b2d4eea8d..f3e30630ce5c 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomSystemMessages.js +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomSystemMessages.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Rooms } from '../../../models'; +import { Rooms } from '../../../models/server'; import { MessageTypesValues } from '../../../lib/lib/MessageTypes'; export const saveRoomSystemMessages = function (rid, systemMessages) { diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomTokens.js b/apps/meteor/app/channel-settings/server/functions/saveRoomTokens.js deleted file mode 100644 index 400da0386611..000000000000 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomTokens.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Match } from 'meteor/check'; - -import { Rooms } from '../../../models'; - -export const saveRoomTokenpass = function (rid, tokenpass) { - if (!Match.test(rid, String)) { - throw new Meteor.Error('invalid-room', 'Invalid room', { - function: 'RocketChat.saveRoomTokens', - }); - } - - return Rooms.setTokenpassById(rid, tokenpass); -}; diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.js b/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.js index 51a5a05035e2..5edf3c215f31 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.js +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomTopic.js @@ -1,7 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Rooms, Messages } from '../../../models'; +import { Rooms, Messages } from '../../../models/server'; +import { callbacks } from '../../../../lib/callbacks'; export const saveRoomTopic = function (rid, roomTopic, user, sendMessage = true) { if (!Match.test(rid, String)) { @@ -14,5 +15,6 @@ export const saveRoomTopic = function (rid, roomTopic, user, sendMessage = true) if (update && sendMessage) { Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rid, roomTopic, user); } + callbacks.run('afterRoomTopicChange', { rid, topic: roomTopic }); return update; }; diff --git a/apps/meteor/app/channel-settings/server/functions/saveStreamingOptions.js b/apps/meteor/app/channel-settings/server/functions/saveStreamingOptions.js index c5c2d2b8fb2b..b9b7cea7f5f7 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveStreamingOptions.js +++ b/apps/meteor/app/channel-settings/server/functions/saveStreamingOptions.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { Rooms } from '../../../models'; +import { Rooms } from '../../../models/server'; export const saveStreamingOptions = function (rid, options) { if (!Match.test(rid, String)) { diff --git a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.js b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.js index 3462c720a570..89dcd43fb8b8 100644 --- a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.js +++ b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.js @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; +import { Match } from 'meteor/check'; import { TEAM_TYPE } from '@rocket.chat/core-typings'; import { setRoomAvatar } from '../../../lib/server/functions/setRoomAvatar'; import { hasPermission } from '../../../authorization'; -import { Rooms } from '../../../models'; +import { Rooms } from '../../../models/server'; import { callbacks } from '../../../../lib/callbacks'; import { saveRoomName } from '../functions/saveRoomName'; import { saveRoomTopic } from '../functions/saveRoomTopic'; @@ -15,7 +15,6 @@ import { saveRoomType } from '../functions/saveRoomType'; import { saveRoomReadOnly } from '../functions/saveRoomReadOnly'; import { saveReactWhenReadOnly } from '../functions/saveReactWhenReadOnly'; import { saveRoomSystemMessages } from '../functions/saveRoomSystemMessages'; -import { saveRoomTokenpass } from '../functions/saveRoomTokens'; import { saveRoomEncrypted } from '../functions/saveRoomEncrypted'; import { saveStreamingOptions } from '../functions/saveStreamingOptions'; import { Team } from '../../../../server/sdk'; @@ -36,7 +35,6 @@ const fields = [ 'systemMessages', 'default', 'joinCode', - 'tokenpass', 'streamingOptions', 'retentionEnabled', 'retentionMaxAge', @@ -195,18 +193,6 @@ const settingSavers = { Team.update(user._id, room.teamId, { type, updateRoom: false }); } }, - tokenpass({ value, rid }) { - check(value, { - require: String, - tokens: [ - { - token: String, - balance: String, - }, - ], - }); - saveRoomTokenpass(rid, value); - }, streamingOptions({ value, rid }) { saveStreamingOptions(rid, value); }, diff --git a/apps/meteor/app/chatpal-search/client/template/result.js b/apps/meteor/app/chatpal-search/client/template/result.js index b96df425c3b4..17a91ae4f6b1 100644 --- a/apps/meteor/app/chatpal-search/client/template/result.js +++ b/apps/meteor/app/chatpal-search/client/template/result.js @@ -3,7 +3,7 @@ import { Template } from 'meteor/templating'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { getURL } from '../../../utils'; -import { Subscriptions } from '../../../models'; +import { Subscriptions } from '../../../models/client'; import { getUserAvatarURL as getAvatarUrl } from '../../../utils/lib/getUserAvatarURL'; import { formatTime } from '../../../../client/lib/utils/formatTime'; import { formatDate } from '../../../../client/lib/utils/formatDate'; diff --git a/apps/meteor/app/chatpal-search/server/provider/index.js b/apps/meteor/app/chatpal-search/server/provider/index.js index 77f88dcea5f1..77eaa2725596 100644 --- a/apps/meteor/app/chatpal-search/server/provider/index.js +++ b/apps/meteor/app/chatpal-search/server/provider/index.js @@ -3,7 +3,7 @@ import { HTTP } from 'meteor/http'; import { Random } from 'meteor/random'; import ChatpalLogger from '../utils/logger'; -import { Rooms, Messages } from '../../../models'; +import { Rooms, Messages } from '../../../models/server'; /** * Enables HTTP functions on Chatpal Backend diff --git a/apps/meteor/app/chatpal-search/server/provider/provider.js b/apps/meteor/app/chatpal-search/server/provider/provider.js index 51670a762c44..b8705cf29be3 100644 --- a/apps/meteor/app/chatpal-search/server/provider/provider.js +++ b/apps/meteor/app/chatpal-search/server/provider/provider.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { searchProviderService, SearchProvider } from '../../../search/server'; import ChatpalLogger from '../utils/logger'; -import { Subscriptions, Rooms } from '../../../models'; +import { Subscriptions, Rooms } from '../../../models/server'; import { baseUrl } from '../utils/settings'; import Index from './index'; diff --git a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts index 2b57931ec5cb..712795441ece 100644 --- a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts +++ b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts @@ -1,8 +1,8 @@ import { SettingValue } from '@rocket.chat/core-typings'; +import { Statistics } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; import { Users } from '../../../models/server'; -import { Statistics } from '../../../models/server/raw'; import { statistics } from '../../../statistics/server'; import { LICENSE_VERSION } from '../license'; diff --git a/apps/meteor/app/cloud/server/functions/checkUserHasCloudLogin.js b/apps/meteor/app/cloud/server/functions/checkUserHasCloudLogin.js index 39743627152b..e68ff1570498 100644 --- a/apps/meteor/app/cloud/server/functions/checkUserHasCloudLogin.js +++ b/apps/meteor/app/cloud/server/functions/checkUserHasCloudLogin.js @@ -1,5 +1,5 @@ import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; -import { Users } from '../../../models'; +import { Users } from '../../../models/server'; export function checkUserHasCloudLogin(userId) { const { connectToCloud, workspaceRegistered } = retrieveRegistrationStatus(); diff --git a/apps/meteor/app/cloud/server/functions/connectWorkspace.js b/apps/meteor/app/cloud/server/functions/connectWorkspace.js index 7ca7fcbb52f9..9c2a424c3221 100644 --- a/apps/meteor/app/cloud/server/functions/connectWorkspace.js +++ b/apps/meteor/app/cloud/server/functions/connectWorkspace.js @@ -2,8 +2,8 @@ import { HTTP } from 'meteor/http'; import { getRedirectUri } from './getRedirectUri'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; -import { Settings } from '../../../models'; -import { settings } from '../../../settings'; +import { Settings } from '../../../models/server'; +import { settings } from '../../../settings/server'; import { saveRegistrationData } from './saveRegistrationData'; import { SystemLogger } from '../../../../server/lib/logger/system'; diff --git a/apps/meteor/app/cloud/server/functions/disconnectWorkspace.js b/apps/meteor/app/cloud/server/functions/disconnectWorkspace.js index c1e2adda876e..c1ac36729aaa 100644 --- a/apps/meteor/app/cloud/server/functions/disconnectWorkspace.js +++ b/apps/meteor/app/cloud/server/functions/disconnectWorkspace.js @@ -1,5 +1,5 @@ import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; -import { Settings } from '../../../models'; +import { Settings } from '../../../models/server'; export function disconnectWorkspace() { const { connectToCloud } = retrieveRegistrationStatus(); diff --git a/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.js b/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.js index ec0601204795..e46bfda5cfd9 100644 --- a/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.js +++ b/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.js @@ -2,8 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { HTTP } from 'meteor/http'; import { getRedirectUri } from './getRedirectUri'; -import { settings } from '../../../settings'; -import { Users } from '../../../models'; +import { settings } from '../../../settings/server'; +import { Users } from '../../../models/server'; import { userScopes } from '../oauthScopes'; import { SystemLogger } from '../../../../server/lib/logger/system'; diff --git a/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts b/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts index 9c4533d2e79d..f83dd650d833 100644 --- a/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts +++ b/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts @@ -10,7 +10,7 @@ export async function getConfirmationPoll(deviceCode: string): Promise('Register_Server'), + workspaceRegistered: !!settings.get('Cloud_Workspace_Client_Id'), + workspaceId: settings.get('Cloud_Workspace_Id'), + uniqueId: settings.get('uniqueID'), + token: '', + email: settings.get('Organization_Email'), + }; + + if (!info.email) { + const firstUser = Users.getOldest({ emails: 1 }); + info.email = firstUser?.emails?.[0]?.address; + } + + return info; +} diff --git a/apps/meteor/app/cloud/server/functions/saveRegistrationData.js b/apps/meteor/app/cloud/server/functions/saveRegistrationData.js index 1bec537f8952..f9f5e3c05a9d 100644 --- a/apps/meteor/app/cloud/server/functions/saveRegistrationData.js +++ b/apps/meteor/app/cloud/server/functions/saveRegistrationData.js @@ -1,4 +1,5 @@ -import { Settings } from '../../../models/server/raw'; +import { Settings } from '@rocket.chat/models'; + import { callbacks } from '../../../../lib/callbacks'; export function saveRegistrationData({ diff --git a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.js b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.js index e512469248a0..9633a3835159 100644 --- a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.js +++ b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.js @@ -3,7 +3,7 @@ import { HTTP } from 'meteor/http'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { syncWorkspace } from './syncWorkspace'; import { settings } from '../../../settings/server'; -import { Settings } from '../../../models'; +import { Settings } from '../../../models/server'; import { buildWorkspaceRegistrationData } from './buildRegistrationData'; import { SystemLogger } from '../../../../server/lib/logger/system'; diff --git a/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts b/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts index d19ba9884916..75e2e3c9c51c 100644 --- a/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts +++ b/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts @@ -14,7 +14,7 @@ export async function startRegisterWorkspaceSetupWizard(resend = false, email: s result = HTTP.post(`${cloudUrl}/api/v2/register/workspace/intent?resent=${resend}`, { data: regInfo, }); - } catch (e) { + } catch (e: any) { if (e.response && e.response.data && e.response.data.error) { SystemLogger.error(`Failed to register with Rocket.Chat Cloud. ErrorCode: ${e.response.data.error}`); } else { diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace.js b/apps/meteor/app/cloud/server/functions/syncWorkspace.js index 6424c5ea867e..ad8ec96faa49 100644 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace.js +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace.js @@ -4,8 +4,8 @@ import { buildWorkspaceRegistrationData } from './buildRegistrationData'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { getWorkspaceAccessToken } from './getWorkspaceAccessToken'; import { getWorkspaceLicense } from './getWorkspaceLicense'; -import { Settings } from '../../../models'; -import { settings } from '../../../settings'; +import { Settings } from '../../../models/server'; +import { settings } from '../../../settings/server'; import { getAndCreateNpsSurvey } from '../../../../server/services/nps/getAndCreateNpsSurvey'; import { NPS, Banner } from '../../../../server/sdk'; import { SystemLogger } from '../../../../server/lib/logger/system'; diff --git a/apps/meteor/app/cloud/server/functions/unregisterWorkspace.js b/apps/meteor/app/cloud/server/functions/unregisterWorkspace.js index f28a54a55153..37895e53e248 100644 --- a/apps/meteor/app/cloud/server/functions/unregisterWorkspace.js +++ b/apps/meteor/app/cloud/server/functions/unregisterWorkspace.js @@ -1,5 +1,5 @@ import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; -import { Settings } from '../../../models'; +import { Settings } from '../../../models/server'; export function unregisterWorkspace() { const { workspaceRegistered } = retrieveRegistrationStatus(); diff --git a/apps/meteor/app/cloud/server/functions/userLoggedOut.js b/apps/meteor/app/cloud/server/functions/userLoggedOut.js index 6e08b878eb6f..ce1e6c6ac17b 100644 --- a/apps/meteor/app/cloud/server/functions/userLoggedOut.js +++ b/apps/meteor/app/cloud/server/functions/userLoggedOut.js @@ -1,4 +1,4 @@ -import { Users } from '../../../models'; +import { Users } from '../../../models/server'; export function userLoggedOut(userId) { if (!userId) { diff --git a/apps/meteor/app/cloud/server/functions/userLogout.js b/apps/meteor/app/cloud/server/functions/userLogout.js index cada35f809fc..6d2e917f347d 100644 --- a/apps/meteor/app/cloud/server/functions/userLogout.js +++ b/apps/meteor/app/cloud/server/functions/userLogout.js @@ -2,8 +2,8 @@ import { HTTP } from 'meteor/http'; import { userLoggedOut } from './userLoggedOut'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; -import { Users } from '../../../models'; -import { settings } from '../../../settings'; +import { Users } from '../../../models/server'; +import { settings } from '../../../settings/server'; import { SystemLogger } from '../../../../server/lib/logger/system'; export function userLogout(userId) { diff --git a/apps/meteor/app/custom-oauth/client/custom_oauth_client.js b/apps/meteor/app/custom-oauth/client/custom_oauth_client.js index 772aa25f9006..efb73d898dd2 100644 --- a/apps/meteor/app/custom-oauth/client/custom_oauth_client.js +++ b/apps/meteor/app/custom-oauth/client/custom_oauth_client.js @@ -7,7 +7,7 @@ import { ServiceConfiguration } from 'meteor/service-configuration'; import { OAuth } from 'meteor/oauth'; import './swapSessionStorage'; -import { isURL } from '../../utils/lib/isURL'; +import { isURL } from '../../../lib/utils/isURL'; // Request custom OAuth credentials for the user // @param options {optional} diff --git a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js index 3b9844e07547..25c5ef82ed88 100644 --- a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js +++ b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js @@ -8,8 +8,8 @@ import _ from 'underscore'; import { normalizers, fromTemplate, renameInvalidProperties } from './transform_helpers'; import { Logger } from '../../logger'; -import { Users } from '../../models'; -import { isURL } from '../../utils/lib/isURL'; +import { Users } from '../../models/server'; +import { isURL } from '../../../lib/utils/isURL'; import { registerAccessTokenService } from '../../lib/server/oauth/oauth'; import { callbacks } from '../../../lib/callbacks'; diff --git a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.js b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.js index 07c83303cb32..5ab704946619 100644 --- a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.js +++ b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.js @@ -39,6 +39,20 @@ class CustomSoundsClass { extension: 'mp3', src: getURL('sounds/telephone.mp3'), }); + this.add({ + _id: 'outbound-call-ringing', + name: 'Outbound Call Ringing', + extension: 'mp3', + src: getURL('sounds/outbound-call-ringing.mp3'), + }); + this.add({ + _id: 'call-ended', + name: 'Call Ended', + extension: 'mp3', + src: getURL('sounds/call-ended.mp3'), + }); + this.add({ _id: 'dialtone', name: 'Dialtone', extension: 'mp3', src: getURL('sounds/dialtone.mp3') }); + this.add({ _id: 'ringtone', name: 'Ringtone', extension: 'mp3', src: getURL('sounds/ringtone.mp3') }); } add(sound) { @@ -108,6 +122,12 @@ class CustomSoundsClass { audio.currentTime = 0; } }; + + isPlaying = (sound) => { + const audio = document.querySelector(`#${getCustomSoundId(sound)}`); + + return audio && audio.duration > 0 && !audio.paused; + }; } export const CustomSounds = new CustomSoundsClass(); diff --git a/apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.js b/apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.js index 9f935e1d3df4..1a51820ec6e8 100644 --- a/apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.js +++ b/apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; +import { CustomSounds } from '@rocket.chat/models'; -import { CustomSounds } from '../../../models/server/raw'; import { hasPermission } from '../../../authorization/server'; import { api } from '../../../../server/sdk/api'; import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; diff --git a/apps/meteor/app/custom-sounds/server/methods/insertOrUpdateSound.js b/apps/meteor/app/custom-sounds/server/methods/insertOrUpdateSound.js index fa36d4165b2d..d5cf8ae377bc 100644 --- a/apps/meteor/app/custom-sounds/server/methods/insertOrUpdateSound.js +++ b/apps/meteor/app/custom-sounds/server/methods/insertOrUpdateSound.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import s from 'underscore.string'; import { check } from 'meteor/check'; +import { CustomSounds } from '@rocket.chat/models'; import { hasPermission } from '../../../authorization/server'; -import { CustomSounds } from '../../../models/server/raw'; import { api } from '../../../../server/sdk/api'; import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; diff --git a/apps/meteor/app/custom-sounds/server/methods/listCustomSounds.js b/apps/meteor/app/custom-sounds/server/methods/listCustomSounds.js index 475da52286be..c22506e42ed1 100644 --- a/apps/meteor/app/custom-sounds/server/methods/listCustomSounds.js +++ b/apps/meteor/app/custom-sounds/server/methods/listCustomSounds.js @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; - -import { CustomSounds } from '../../../models/server/raw'; +import { CustomSounds } from '@rocket.chat/models'; Meteor.methods({ async listCustomSounds() { diff --git a/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts b/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts index 6d7bac1317e0..0c78294b28ce 100644 --- a/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts +++ b/apps/meteor/app/discussion/client/createDiscussionMessageAction.ts @@ -4,7 +4,7 @@ import { Tracker } from 'meteor/tracker'; import { settings } from '../../settings/client'; import { hasPermission } from '../../authorization/client'; import { MessageAction } from '../../ui-utils/client'; -import { messageArgs } from '../../ui-utils/client/lib/messageArgs'; +import { messageArgs } from '../../../client/lib/utils/messageArgs'; import { imperativeModal } from '../../../client/lib/imperativeModal'; import CreateDiscussion from '../../../client/components/CreateDiscussion/CreateDiscussion'; import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; diff --git a/apps/meteor/app/discussion/client/tabBar.ts b/apps/meteor/app/discussion/client/tabBar.ts index fc5f0934cfd5..a9bece626e54 100644 --- a/apps/meteor/app/discussion/client/tabBar.ts +++ b/apps/meteor/app/discussion/client/tabBar.ts @@ -1,16 +1,18 @@ import { useMemo, lazy } from 'react'; import { useSetting } from '@rocket.chat/ui-contexts'; +import { isRoomFederated } from '@rocket.chat/core-typings'; import { addAction } from '../../../client/views/room/lib/Toolbox'; const template = lazy(() => import('../../../client/views/room/contextualBar/Discussions')); -addAction('discussions', ({ room: { prid } }) => { +addAction('discussions', ({ room }) => { const discussionEnabled = useSetting('Discussion_enabled'); + const federated = isRoomFederated(room); return useMemo( () => - discussionEnabled && !prid + discussionEnabled && !room.prid ? { groups: ['channel', 'group', 'direct', 'direct_multiple', 'team'], id: 'discussions', @@ -18,9 +20,13 @@ addAction('discussions', ({ room: { prid } }) => { icon: 'discussion', template, full: true, + ...(federated && { + 'disabled': true, + 'data-tooltip': 'Discussions_unavailable_for_federation', + }), order: 3, } : null, - [discussionEnabled, prid], + [discussionEnabled, room.prid, federated], ); }); diff --git a/apps/meteor/app/discussion/server/permissions.ts b/apps/meteor/app/discussion/server/permissions.ts index 260b2da71035..fcd2412a77ea 100644 --- a/apps/meteor/app/discussion/server/permissions.ts +++ b/apps/meteor/app/discussion/server/permissions.ts @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; - -import { Permissions } from '../../models/server/raw'; +import { Permissions } from '@rocket.chat/models'; Meteor.startup(() => { // Add permissions for discussion diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index 223644f56796..fea71e381182 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -3,7 +3,6 @@ import { Base64 } from 'meteor/base64'; import { EJSON } from 'meteor/ejson'; import { Random } from 'meteor/random'; import { Session } from 'meteor/session'; -import { TimeSync } from 'meteor/mizzao:timesync'; import { Emitter } from '@rocket.chat/emitter'; import { e2e } from './rocketchat.e2e'; @@ -395,12 +394,7 @@ export class E2ERoom extends Emitter { // Helper function for encryption of messages encrypt(message) { - let ts; - if (isNaN(TimeSync.serverOffset())) { - ts = new Date(); - } else { - ts = new Date(Date.now() + TimeSync.serverOffset()); - } + const ts = new Date(); const data = new TextEncoder('UTF-8').encode( EJSON.stringify({ diff --git a/apps/meteor/app/e2e/server/methods/fetchMyKeys.js b/apps/meteor/app/e2e/server/methods/fetchMyKeys.js index f50e8578f815..744cb9cd9c62 100644 --- a/apps/meteor/app/e2e/server/methods/fetchMyKeys.js +++ b/apps/meteor/app/e2e/server/methods/fetchMyKeys.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { Users } from '../../../models'; +import { Users } from '../../../models/server'; Meteor.methods({ 'e2e.fetchMyKeys'() { diff --git a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.js b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.js index ee8727794f0b..91c6f6b0fc7f 100644 --- a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.js +++ b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { Users } from '../../../models'; +import { Users } from '../../../models/server'; Meteor.methods({ 'e2e.setUserPublicAndPrivateKeys'({ public_key, private_key }) { diff --git a/apps/meteor/app/e2e/server/methods/updateGroupKey.js b/apps/meteor/app/e2e/server/methods/updateGroupKey.js index e7276671c872..9aae29003ddf 100644 --- a/apps/meteor/app/e2e/server/methods/updateGroupKey.js +++ b/apps/meteor/app/e2e/server/methods/updateGroupKey.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { Subscriptions } from '../../../models'; +import { Subscriptions } from '../../../models/server'; Meteor.methods({ 'e2e.updateGroupKey'(rid, uid, key) { diff --git a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.js b/apps/meteor/app/emoji-custom/client/lib/emojiCustom.js index 46cb53fcbf56..a3df7135cc5f 100644 --- a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.js +++ b/apps/meteor/app/emoji-custom/client/lib/emojiCustom.js @@ -184,7 +184,7 @@ Meteor.startup(() => try { const { emojis: { update: emojis }, - } = await APIClient.v1.get('emoji-custom.list'); + } = await APIClient.get('/v1/emoji-custom.list'); emoji.packages.emojiCustom.emojisByCategory = { rocket: [] }; for (const currentEmoji of emojis) { diff --git a/apps/meteor/app/emoji-custom/server/methods/deleteEmojiCustom.js b/apps/meteor/app/emoji-custom/server/methods/deleteEmojiCustom.js index eceb0d933c31..4d26d9ca065a 100644 --- a/apps/meteor/app/emoji-custom/server/methods/deleteEmojiCustom.js +++ b/apps/meteor/app/emoji-custom/server/methods/deleteEmojiCustom.js @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; +import { EmojiCustom } from '@rocket.chat/models'; import { api } from '../../../../server/sdk/api'; import { hasPermission } from '../../../authorization'; -import { EmojiCustom } from '../../../models/server/raw'; import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom'; Meteor.methods({ diff --git a/apps/meteor/app/emoji-custom/server/methods/insertOrUpdateEmoji.js b/apps/meteor/app/emoji-custom/server/methods/insertOrUpdateEmoji.js index b26953b1d266..96b95f3fbc6f 100644 --- a/apps/meteor/app/emoji-custom/server/methods/insertOrUpdateEmoji.js +++ b/apps/meteor/app/emoji-custom/server/methods/insertOrUpdateEmoji.js @@ -2,9 +2,9 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import s from 'underscore.string'; import limax from 'limax'; +import { EmojiCustom } from '@rocket.chat/models'; import { hasPermission } from '../../../authorization'; -import { EmojiCustom } from '../../../models/server/raw'; import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom'; import { api } from '../../../../server/sdk/api'; diff --git a/apps/meteor/app/emoji-custom/server/methods/listEmojiCustom.js b/apps/meteor/app/emoji-custom/server/methods/listEmojiCustom.js index d66aeee1a6ad..a4fd124abe91 100644 --- a/apps/meteor/app/emoji-custom/server/methods/listEmojiCustom.js +++ b/apps/meteor/app/emoji-custom/server/methods/listEmojiCustom.js @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; - -import { EmojiCustom } from '../../../models/server/raw'; +import { EmojiCustom } from '@rocket.chat/models'; Meteor.methods({ async listEmojiCustom(options = {}) { diff --git a/apps/meteor/app/emoji/client/emojiPicker.js b/apps/meteor/app/emoji/client/emojiPicker.js index 393124a3fbef..671698280cc1 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'; @@ -152,7 +151,7 @@ Template.emojiPicker.events({ 'click .add-custom'(event) { event.stopPropagation(); event.preventDefault(); - FlowRouter.go('/admin/emoji-custom'); + FlowRouter.go('/admin/emoji-custom/new'); EmojiPicker.close(); }, 'click .category-link'(event) { diff --git a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.js b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.js index ac0f31e29679..73b7b475f892 100644 --- a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.js +++ b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { settings } from '../../../settings/server'; -import { Users, Rooms } from '../../../models'; +import { Users, Rooms, Settings } from '../../../models/server'; import { sendMessage } from '../../../lib'; class ErrorHandler { @@ -33,6 +33,7 @@ class ErrorHandler { process.on( 'uncaughtException', Meteor.bindEnvironment((error) => { + Settings.incrementValueById('Uncaught_Exceptions_Count'); if (!this.reporting) { return; } diff --git a/apps/meteor/app/favico/client/favico.js b/apps/meteor/app/favico/client/favico.js deleted file mode 100644 index b85f9b4a4ee1..000000000000 --- a/apps/meteor/app/favico/client/favico.js +++ /dev/null @@ -1,844 +0,0 @@ -/** - * @license MIT - * @fileOverview Favico animations - * @author Miroslav Magda, http://blog.ejci.net - * @version 0.3.10 - */ - -/** - * Create new favico instance - * @param {Object} Options - * @return {Object} Favico object - * @example - * var favico = new Favico({ - * bgColor : '#d00', - * textColor : '#fff', - * fontFamily : 'sans-serif', - * fontStyle : 'bold', - * position : 'down', - * type : 'circle', - * animation : 'slide', - * dataUrl: function(url){}, - * win: top - * }); - */ -/* eslint-disable */ - - export const Favico = (function(opt) { - 'use strict'; - opt = (opt) ? opt : {}; - var _def = { - bgColor: '#d00', - textColor: '#fff', - fontFamily: 'sans-serif', //Arial,Verdana,Times New Roman,serif,sans-serif,... - fontStyle: 'bold', //normal,italic,oblique,bold,bolder,lighter,100,200,300,400,500,600,700,800,900 - type: 'circle', - position: 'down', // down, up, left, leftup (upleft) - animation: 'slide', - elementId: false, - dataUrl: false, - win: window - }; - var _opt, _orig, _h, _w, _canvas, _context, _img, _ready, _lastBadge, _running, _readyCb, _stop, _browser, _animTimeout, _drawTimeout, _doc; - - _browser = {}; - _browser.ff = typeof InstallTrigger !== 'undefined'; - _browser.chrome = !!window.chrome; - _browser.opera = !!window.opera || navigator.userAgent.indexOf('Opera') >= 0; - _browser.ie = /*@cc_on!@*/ false; - _browser.safari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0; - _browser.supported = (_browser.chrome || _browser.ff || _browser.opera); - - var _queue = []; - _readyCb = function() {}; - _ready = _stop = false; - /** - * Initialize favico - */ - var init = function() { - //merge initial options - _opt = merge(_def, opt); - _opt.bgColor = hexToRgb(_opt.bgColor); - _opt.textColor = hexToRgb(_opt.textColor); - _opt.position = _opt.position.toLowerCase(); - _opt.animation = (animation.types['' + _opt.animation]) ? _opt.animation : _def.animation; - - _doc = _opt.win.document; - - var isUp = _opt.position.indexOf('up') > -1; - var isLeft = _opt.position.indexOf('left') > -1; - - //transform the animations - if (isUp || isLeft) { - for (var a in animation.types) { - for (var i = 0; i < animation.types[a].length; i++) { - var step = animation.types[a][i]; - - if (isUp) { - if (step.y < 0.6) { - step.y = step.y - 0.4; - } else { - step.y = step.y - 2 * step.y + (1 - step.w); - } - } - - if (isLeft) { - if (step.x < 0.6) { - step.x = step.x - 0.4; - } else { - step.x = step.x - 2 * step.x + (1 - step.h); - } - } - - animation.types[a][i] = step; - } - } - } - _opt.type = (type['' + _opt.type]) ? _opt.type : _def.type; - - _orig = link.getIcons(); - //create temp canvas - _canvas = document.createElement('canvas'); - //create temp image - _img = document.createElement('img'); - var lastIcon = _orig[_orig.length - 1]; - if (lastIcon.hasAttribute('href')) { - _img.setAttribute('crossOrigin', 'anonymous'); - //get width/height - _img.onload = function() { - _h = (_img.height > 0) ? _img.height : 32; - _w = (_img.width > 0) ? _img.width : 32; - _canvas.height = _h; - _canvas.width = _w; - _context = _canvas.getContext('2d'); - icon.ready(); - }; - _img.setAttribute('src', lastIcon.getAttribute('href')); - } else { - _img.onload = function() { - _h = 32; - _w = 32; - _img.height = _h; - _img.width = _w; - _canvas.height = _h; - _canvas.width = _w; - _context = _canvas.getContext('2d'); - icon.ready(); - }; - _img.setAttribute('src', ''); - } - - }; - /** - * Icon namespace - */ - var icon = {}; - /** - * Icon is ready (reset icon) and start animation (if ther is any) - */ - icon.ready = function() { - _ready = true; - icon.reset(); - _readyCb(); - }; - /** - * Reset icon to default state - */ - icon.reset = function() { - //reset - if (!_ready) { - return; - } - _queue = []; - _lastBadge = false; - _running = false; - _context.clearRect(0, 0, _w, _h); - _context.drawImage(_img, 0, 0, _w, _h); - //_stop=true; - link.setIcon(_canvas); - //webcam('stop'); - //video('stop'); - window.clearTimeout(_animTimeout); - window.clearTimeout(_drawTimeout); - }; - /** - * Start animation - */ - icon.start = function() { - if (!_ready || _running) { - return; - } - var finished = function() { - _lastBadge = _queue[0]; - _running = false; - if (_queue.length > 0) { - _queue.shift(); - icon.start(); - } - }; - if (_queue.length > 0) { - _running = true; - var run = function() { - // apply options for this animation - ['type', 'animation', 'bgColor', 'textColor', 'fontFamily', 'fontStyle'].forEach(function(a) { - if (a in _queue[0].options) { - _opt[a] = _queue[0].options[a]; - } - }); - animation.run(_queue[0].options, function() { - finished(); - }, false); - }; - if (_lastBadge) { - animation.run(_lastBadge.options, function() { - run(); - }, true); - } else { - run(); - } - } - }; - - /** - * Badge types - */ - var type = {}; - var options = function(opt) { - opt.n = ((typeof opt.n) === 'number') ? Math.abs(opt.n | 0) : opt.n; - opt.x = _w * opt.x; - opt.y = _h * opt.y; - opt.w = _w * opt.w; - opt.h = _h * opt.h; - opt.len = ('' + opt.n).length; - return opt; - }; - /** - * Generate circle - * @param {Object} opt Badge options - */ - type.circle = function(opt) { - opt = options(opt); - var more = false; - if (opt.len === 2) { - opt.x = opt.x - opt.w * 0.4; - opt.w = opt.w * 1.4; - more = true; - } else if (opt.len >= 3) { - opt.x = opt.x - opt.w * 0.65; - opt.w = opt.w * 1.65; - more = true; - } - _context.clearRect(0, 0, _w, _h); - _context.drawImage(_img, 0, 0, _w, _h); - _context.beginPath(); - _context.font = _opt.fontStyle + ' ' + Math.floor(opt.h * (opt.n > 99 ? 0.85 : 1)) + 'px ' + _opt.fontFamily; - _context.textAlign = 'center'; - if (more) { - _context.moveTo(opt.x + opt.w / 2, opt.y); - _context.lineTo(opt.x + opt.w - opt.h / 2, opt.y); - _context.quadraticCurveTo(opt.x + opt.w, opt.y, opt.x + opt.w, opt.y + opt.h / 2); - _context.lineTo(opt.x + opt.w, opt.y + opt.h - opt.h / 2); - _context.quadraticCurveTo(opt.x + opt.w, opt.y + opt.h, opt.x + opt.w - opt.h / 2, opt.y + opt.h); - _context.lineTo(opt.x + opt.h / 2, opt.y + opt.h); - _context.quadraticCurveTo(opt.x, opt.y + opt.h, opt.x, opt.y + opt.h - opt.h / 2); - _context.lineTo(opt.x, opt.y + opt.h / 2); - _context.quadraticCurveTo(opt.x, opt.y, opt.x + opt.h / 2, opt.y); - } else { - _context.arc(opt.x + opt.w / 2, opt.y + opt.h / 2, opt.h / 2, 0, 2 * Math.PI); - } - _context.fillStyle = 'rgba(' + _opt.bgColor.r + ',' + _opt.bgColor.g + ',' + _opt.bgColor.b + ',' + opt.o + ')'; - _context.fill(); - _context.closePath(); - _context.beginPath(); - _context.stroke(); - _context.fillStyle = 'rgba(' + _opt.textColor.r + ',' + _opt.textColor.g + ',' + _opt.textColor.b + ',' + opt.o + ')'; - //_context.fillText((more) ? '9+' : opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15)); - if ((typeof opt.n) === 'number' && opt.n > 999) { - _context.fillText(((opt.n > 9999) ? 9 : Math.floor(opt.n / 1000)) + 'k+', Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.2)); - } else { - _context.fillText(opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15)); - } - _context.closePath(); - }; - /** - * Generate rectangle - * @param {Object} opt Badge options - */ - type.rectangle = function(opt) { - opt = options(opt); - var more = false; - if (opt.len === 2) { - opt.x = opt.x - opt.w * 0.4; - opt.w = opt.w * 1.4; - more = true; - } else if (opt.len >= 3) { - opt.x = opt.x - opt.w * 0.65; - opt.w = opt.w * 1.65; - more = true; - } - _context.clearRect(0, 0, _w, _h); - _context.drawImage(_img, 0, 0, _w, _h); - _context.beginPath(); - _context.font = _opt.fontStyle + ' ' + Math.floor(opt.h * (opt.n > 99 ? 0.9 : 1)) + 'px ' + _opt.fontFamily; - _context.textAlign = 'center'; - _context.fillStyle = 'rgba(' + _opt.bgColor.r + ',' + _opt.bgColor.g + ',' + _opt.bgColor.b + ',' + opt.o + ')'; - _context.fillRect(opt.x, opt.y, opt.w, opt.h); - _context.fillStyle = 'rgba(' + _opt.textColor.r + ',' + _opt.textColor.g + ',' + _opt.textColor.b + ',' + opt.o + ')'; - //_context.fillText((more) ? '9+' : opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15)); - if ((typeof opt.n) === 'number' && opt.n > 999) { - _context.fillText(((opt.n > 9999) ? 9 : Math.floor(opt.n / 1000)) + 'k+', Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.2)); - } else { - _context.fillText(opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15)); - } - _context.closePath(); - }; - - /** - * Set badge - */ - var badge = function(number, opts) { - opts = ((typeof opts) === 'string' ? { - animation: opts - } : opts) || {}; - _readyCb = function() { - try { - if (typeof(number) === 'number' ? (number > 0) : (number !== '')) { - var q = { - type: 'badge', - options: { - n: number - } - }; - if ('animation' in opts && animation.types['' + opts.animation]) { - q.options.animation = '' + opts.animation; - } - if ('type' in opts && type['' + opts.type]) { - q.options.type = '' + opts.type; - } - ['bgColor', 'textColor'].forEach(function(o) { - if (o in opts) { - q.options[o] = hexToRgb(opts[o]); - } - }); - ['fontStyle', 'fontFamily'].forEach(function(o) { - if (o in opts) { - q.options[o] = opts[o]; - } - }); - _queue.push(q); - if (_queue.length > 100) { - throw new Error('Too many badges requests in queue.'); - } - icon.start(); - } else { - icon.reset(); - } - } catch (e) { - throw new Error('Error setting badge. Message: ' + e.message); - } - }; - if (_ready) { - _readyCb(); - } - }; - - /** - * Set image as icon - */ - var image = function(imageElement) { - _readyCb = function() { - try { - var w = imageElement.width; - var h = imageElement.height; - var newImg = document.createElement('img'); - var ratio = (w / _w < h / _h) ? (w / _w) : (h / _h); - newImg.setAttribute('crossOrigin', 'anonymous'); - newImg.onload = function() { - _context.clearRect(0, 0, _w, _h); - _context.drawImage(newImg, 0, 0, _w, _h); - link.setIcon(_canvas); - }; - newImg.setAttribute('src', imageElement.getAttribute('src')); - newImg.height = (h / ratio); - newImg.width = (w / ratio); - } catch (e) { - throw new Error('Error setting image. Message: ' + e.message); - } - }; - if (_ready) { - _readyCb(); - } - }; - /** - * Set video as icon - */ - var video = function(videoElement) { - _readyCb = function() { - try { - if (videoElement === 'stop') { - _stop = true; - icon.reset(); - _stop = false; - return; - } - //var w = videoElement.width; - //var h = videoElement.height; - //var ratio = (w / _w < h / _h) ? (w / _w) : (h / _h); - videoElement.addEventListener('play', function() { - drawVideo(this); - }, false); - - } catch (e) { - throw new Error('Error setting video. Message: ' + e.message); - } - }; - if (_ready) { - _readyCb(); - } - }; - /** - * Set video as icon - */ - var webcam = function(action) { - //UR - if (!window.URL || !window.URL.createObjectURL) { - window.URL = window.URL || {}; - window.URL.createObjectURL = function(obj) { - return obj; - }; - } - if (_browser.supported) { - var newVideo = false; - navigator.getUserMedia = navigator.getUserMedia || navigator.oGetUserMedia || navigator.msGetUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia; - _readyCb = function() { - try { - if (action === 'stop') { - _stop = true; - icon.reset(); - _stop = false; - return; - } - newVideo = document.createElement('video'); - newVideo.width = _w; - newVideo.height = _h; - navigator.getUserMedia({ - video: true, - audio: false - }, function(stream) { - newVideo.src = URL.createObjectURL(stream); - newVideo.play(); - drawVideo(newVideo); - }, function() {}); - } catch (e) { - throw new Error('Error setting webcam. Message: ' + e.message); - } - }; - if (_ready) { - _readyCb(); - } - } - - }; - - /** - * Draw video to context and repeat :) - */ - function drawVideo(video) { - if (video.paused || video.ended || _stop) { - return false; - } - //nasty hack for FF webcam (Thanks to Julian Ćwirko, kontakt@redsunmedia.pl) - try { - _context.clearRect(0, 0, _w, _h); - _context.drawImage(video, 0, 0, _w, _h); - } catch (e) { - - } - _drawTimeout = setTimeout(function() { - drawVideo(video); - }, animation.duration); - link.setIcon(_canvas); - } - - var link = {}; - /** - * Get icons from HEAD tag or create a new element - */ - link.getIcons = function() { - var elms = []; - //get link element - var getLinks = function() { - var icons = []; - var links = _doc.getElementsByTagName('head')[0].getElementsByTagName('link'); - for (var i = 0; i < links.length; i++) { - if ((/(^|\s)icon(\s|$)/i).test(links[i].getAttribute('rel'))) { - icons.push(links[i]); - } - } - return icons; - }; - if (_opt.element) { - elms = [_opt.element]; - } else if (_opt.elementId) { - //if img element identified by elementId - elms = [_doc.getElementById(_opt.elementId)]; - elms[0].setAttribute('href', elms[0].getAttribute('src')); - } else { - //if link element - elms = getLinks(); - if (elms.length === 0) { - elms = [_doc.createElement('link')]; - elms[0].setAttribute('rel', 'icon'); - _doc.getElementsByTagName('head')[0].appendChild(elms[0]); - } - } - elms.forEach(function(item) { - item.setAttribute('type', 'image/png'); - }); - return elms; - }; - link.setIcon = function(canvas) { - var url = canvas.toDataURL('image/png'); - if (_opt.dataUrl) { - //if using custom exporter - _opt.dataUrl(url); - } - if (_opt.element) { - _opt.element.setAttribute('href', url); - _opt.element.setAttribute('src', url); - } else if (_opt.elementId) { - //if is attached to element (image) - var elm = _doc.getElementById(_opt.elementId); - elm.setAttribute('href', url); - elm.setAttribute('src', url); - } else { - //if is attached to fav icon - if (_browser.ff || _browser.opera) { - //for FF we need to "recreate" element, atach to dom and remove old - //var originalType = _orig.getAttribute('rel'); - var old = _orig[_orig.length - 1]; - var newIcon = _doc.createElement('link'); - _orig = [newIcon]; - //_orig.setAttribute('rel', originalType); - if (_browser.opera) { - newIcon.setAttribute('rel', 'icon'); - } - newIcon.setAttribute('rel', 'icon'); - newIcon.setAttribute('type', 'image/png'); - _doc.getElementsByTagName('head')[0].appendChild(newIcon); - newIcon.setAttribute('href', url); - if (old.parentNode) { - old.parentNode.removeChild(old); - } - } else { - _orig.forEach(function(icon) { - icon.setAttribute('href', url); - }); - } - } - }; - - //http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb#answer-5624139 - //HEX to RGB convertor - function hexToRgb(hex) { - var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; - hex = hex.replace(shorthandRegex, function(m, r, g, b) { - return r + r + g + g + b + b; - }); - var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16) - } : false; - } - - /** - * Merge options - */ - function merge(def, opt) { - var mergedOpt = {}; - var attrname; - for (attrname in def) { - mergedOpt[attrname] = def[attrname]; - } - for (attrname in opt) { - mergedOpt[attrname] = opt[attrname]; - } - return mergedOpt; - } - - /** - * Cross-browser page visibility shim - * http://stackoverflow.com/questions/12536562/detect-whether-a-window-is-visible - */ - function isPageHidden() { - return _doc.hidden || _doc.msHidden || _doc.webkitHidden || _doc.mozHidden; - } - - /** - * @namespace animation - */ - var animation = {}; - /** - * Animation "frame" duration - */ - animation.duration = 40; - /** - * Animation types (none,fade,pop,slide) - */ - animation.types = {}; - animation.types.fade = [{ - x: 0.4, - y: 0.4, - w: 0.6, - h: 0.6, - o: 0.0 - }, { - x: 0.4, - y: 0.4, - w: 0.6, - h: 0.6, - o: 0.1 - }, { - x: 0.4, - y: 0.4, - w: 0.6, - h: 0.6, - o: 0.2 - }, { - x: 0.4, - y: 0.4, - w: 0.6, - h: 0.6, - o: 0.3 - }, { - x: 0.4, - y: 0.4, - w: 0.6, - h: 0.6, - o: 0.4 - }, { - x: 0.4, - y: 0.4, - w: 0.6, - h: 0.6, - o: 0.5 - }, { - x: 0.4, - y: 0.4, - w: 0.6, - h: 0.6, - o: 0.6 - }, { - x: 0.4, - y: 0.4, - w: 0.6, - h: 0.6, - o: 0.7 - }, { - x: 0.4, - y: 0.4, - w: 0.6, - h: 0.6, - o: 0.8 - }, { - x: 0.4, - y: 0.4, - w: 0.6, - h: 0.6, - o: 0.9 - }, { - x: 0.4, - y: 0.4, - w: 0.6, - h: 0.6, - o: 1.0 - }]; - animation.types.none = [{ - x: 0.4, - y: 0.4, - w: 0.6, - h: 0.6, - o: 1 - }]; - animation.types.pop = [{ - x: 1, - y: 1, - w: 0, - h: 0, - o: 1 - }, { - x: 0.9, - y: 0.9, - w: 0.1, - h: 0.1, - o: 1 - }, { - x: 0.8, - y: 0.8, - w: 0.2, - h: 0.2, - o: 1 - }, { - x: 0.7, - y: 0.7, - w: 0.3, - h: 0.3, - o: 1 - }, { - x: 0.6, - y: 0.6, - w: 0.4, - h: 0.4, - o: 1 - }, { - x: 0.5, - y: 0.5, - w: 0.5, - h: 0.5, - o: 1 - }, { - x: 0.4, - y: 0.4, - w: 0.6, - h: 0.6, - o: 1 - }]; - animation.types.popFade = [{ - x: 0.75, - y: 0.75, - w: 0, - h: 0, - o: 0 - }, { - x: 0.65, - y: 0.65, - w: 0.1, - h: 0.1, - o: 0.2 - }, { - x: 0.6, - y: 0.6, - w: 0.2, - h: 0.2, - o: 0.4 - }, { - x: 0.55, - y: 0.55, - w: 0.3, - h: 0.3, - o: 0.6 - }, { - x: 0.50, - y: 0.50, - w: 0.4, - h: 0.4, - o: 0.8 - }, { - x: 0.45, - y: 0.45, - w: 0.5, - h: 0.5, - o: 0.9 - }, { - x: 0.4, - y: 0.4, - w: 0.6, - h: 0.6, - o: 1 - }]; - animation.types.slide = [{ - x: 0.4, - y: 1, - w: 0.6, - h: 0.6, - o: 1 - }, { - x: 0.4, - y: 0.9, - w: 0.6, - h: 0.6, - o: 1 - }, { - x: 0.4, - y: 0.9, - w: 0.6, - h: 0.6, - o: 1 - }, { - x: 0.4, - y: 0.8, - w: 0.6, - h: 0.6, - o: 1 - }, { - x: 0.4, - y: 0.7, - w: 0.6, - h: 0.6, - o: 1 - }, { - x: 0.4, - y: 0.6, - w: 0.6, - h: 0.6, - o: 1 - }, { - x: 0.4, - y: 0.5, - w: 0.6, - h: 0.6, - o: 1 - }, { - x: 0.4, - y: 0.4, - w: 0.6, - h: 0.6, - o: 1 - }]; - /** - * Run animation - * @param {Object} opt Animation options - * @param {Object} cb Callabak after all steps are done - * @param {Object} revert Reverse order? true|false - * @param {Object} step Optional step number (frame bumber) - */ - animation.run = function(opt, cb, revert, step) { - var animationType = animation.types[isPageHidden() ? 'none' : _opt.animation]; - if (revert === true) { - step = (typeof step !== 'undefined') ? step : animationType.length - 1; - } else { - step = (typeof step !== 'undefined') ? step : 0; - } - cb = (cb) ? cb : function() {}; - if ((step < animationType.length) && (step >= 0)) { - type[_opt.type](merge(opt, animationType[step])); - _animTimeout = setTimeout(function() { - if (revert) { - step = step - 1; - } else { - step = step + 1; - } - animation.run(opt, cb, revert, step); - }, animation.duration); - - link.setIcon(_canvas); - } else { - cb(); - return; - } - }; - //auto init - init(); - return { - badge: badge, - video: video, - image: image, - webcam: webcam, - reset: icon.reset, - browser: { - supported: _browser.supported - } - }; - }); diff --git a/apps/meteor/app/favico/client/index.js b/apps/meteor/app/favico/client/index.js deleted file mode 100644 index 239a252e455c..000000000000 --- a/apps/meteor/app/favico/client/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Favico } from './favico'; - -export { Favico }; diff --git a/apps/meteor/app/favico/index.js b/apps/meteor/app/favico/index.js deleted file mode 100644 index 40a7340d3887..000000000000 --- a/apps/meteor/app/favico/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './client/index'; diff --git a/apps/meteor/app/federation-v2/client/Federation.ts b/apps/meteor/app/federation-v2/client/Federation.ts new file mode 100644 index 000000000000..552c3a47623b --- /dev/null +++ b/apps/meteor/app/federation-v2/client/Federation.ts @@ -0,0 +1,24 @@ +import { IRoom, IUser, ValueOf } from '@rocket.chat/core-typings'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { RoomMemberActions } from '../../../definition/IRoomTypeConfig'; + +const allowedActionsInFederatedRooms: ValueOf[] = [ + RoomMemberActions.REMOVE_USER, + RoomMemberActions.INVITE, + RoomMemberActions.JOIN, + RoomMemberActions.LEAVE, +]; + +export const actionAllowed = (room: Partial, action: ValueOf): boolean => { + return room.t === RoomType.DIRECT_MESSAGE && action === RoomMemberActions.REMOVE_USER + ? false + : allowedActionsInFederatedRooms.includes(action); +}; + +export const isEditableByTheUser = (user: IUser | undefined, room: IRoom | undefined): boolean => { + if (!user || !room) { + return false; + } + return user._id === room.u?._id; +}; diff --git a/apps/meteor/app/federation-v2/client/index.ts b/apps/meteor/app/federation-v2/client/index.ts new file mode 100644 index 000000000000..37ec361a570a --- /dev/null +++ b/apps/meteor/app/federation-v2/client/index.ts @@ -0,0 +1 @@ +import './slash-commands'; diff --git a/apps/meteor/app/federation-v2/client/slash-commands/index.ts b/apps/meteor/app/federation-v2/client/slash-commands/index.ts new file mode 100644 index 000000000000..7e32258f20c4 --- /dev/null +++ b/apps/meteor/app/federation-v2/client/slash-commands/index.ts @@ -0,0 +1,20 @@ +import { slashCommands } from '../../../utils/lib/slashCommand'; + +const callback = undefined; +const result = undefined; +const providesPreview = false; +const previewer = undefined; +const previewCallback = undefined; + +slashCommands.add({ + command: 'federation', + callback, + options: { + description: 'Federation_slash_commands', + params: '#command (dm) #user', + }, + result, + providesPreview, + previewer, + previewCallback, +}); 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..4171426bb62b --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/RoomServiceReceiver.ts @@ -0,0 +1,263 @@ +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( + protected rocketRoomAdapter: RocketChatRoomAdapter, + protected rocketUserAdapter: RocketChatUserAdapter, + protected rocketMessageAdapter: RocketChatMessageAdapter, + protected rocketSettingsAdapter: RocketChatSettingsAdapter, + protected 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 = this.bridge.isUserIdFromTheSameHomeserver( + externalInviterId, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + const isInviteeFromTheSameHomeServer = 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 FederatedUser[]; + 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) { + if ( + !(await this.rocketRoomAdapter.isUserAlreadyJoined( + federatedRoom?.internalReference?._id as string, + federatedInviteeUser?.internalReference._id as string, + )) + ) { + return; + } + + return this.rocketRoomAdapter.removeUserFromRoom( + federatedRoom as FederatedRoom, + federatedInviteeUser as FederatedUser, + federatedInviterUser as FederatedUser, + ); + } + if (affectedFederatedRoom?.isDirectMessage() && eventOrigin === EVENT_ORIGIN.REMOTE) { + const membersUsernames = [ + ...(affectedFederatedRoom.internalReference?.usernames || []), + federatedInviteeUser?.internalReference.username as string, + ]; + const newFederatedRoom = FederatedRoom.createInstance( + externalRoomId, + normalizedRoomId, + federatedInviterUser as FederatedUser, + RoomType.DIRECT_MESSAGE, + externalRoomName, + ); + if (affectedFederatedRoom.internalReference?.usernames?.includes(federatedInviteeUser?.internalReference.username || '')) { + return; + } + await this.rocketRoomAdapter.removeDirectMessageRoom(affectedFederatedRoom); + await this.rocketRoomAdapter.createFederatedRoomForDirectMessage(newFederatedRoom, membersUsernames); + return; + } + + 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, externalSenderId } = roomChangeNameInput; + + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + if (federatedRoom.isDirectMessage()) { + return; + } + + if (federatedRoom.internalReference?.name === normalizedRoomName) { + return; + } + + const federatedUser = await this.rocketUserAdapter.getFederatedUserByExternalId(externalSenderId); + if (!federatedUser) { + return; + } + + federatedRoom.changeRoomName(normalizedRoomName); + + await this.rocketRoomAdapter.updateRoomName(federatedRoom, federatedUser); + } + + public async changeRoomTopic(roomChangeTopicInput: FederationRoomChangeTopicDto): Promise { + const { externalRoomId, roomTopic, externalSenderId } = roomChangeTopicInput; + + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByExternalId(externalRoomId); + if (!federatedRoom) { + return; + } + + if (federatedRoom.internalReference?.topic === roomTopic) { + return; + } + + if (federatedRoom.isDirectMessage()) { + return; + } + + const federatedUser = await this.rocketUserAdapter.getFederatedUserByExternalId(externalSenderId); + if (!federatedUser) { + return; + } + + federatedRoom.changeRoomTopic(roomTopic); + + await this.rocketRoomAdapter.updateRoomTopic(federatedRoom, federatedUser); + } +} 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..8d21f8241648 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/RoomServiceSender.ts @@ -0,0 +1,198 @@ +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 { RocketChatRoomAdapter } from '../infrastructure/rocket-chat/adapters/Room'; +import { RocketChatSettingsAdapter } from '../infrastructure/rocket-chat/adapters/Settings'; +import { RocketChatUserAdapter } from '../infrastructure/rocket-chat/adapters/User'; +import { + FederationAfterLeaveRoomDto, + FederationCreateDMAndInviteUserDto, + FederationRoomSendExternalMessageDto, +} from './input/RoomSenderDto'; + +export class FederationRoomServiceSender { + constructor( + protected rocketRoomAdapter: RocketChatRoomAdapter, + protected rocketUserAdapter: RocketChatUserAdapter, + protected rocketSettingsAdapter: RocketChatSettingsAdapter, + protected bridge: IFederationBridge, + ) {} // eslint-disable-line no-empty-function + + public async createDirectMessageRoomAndInviteUser(roomCreateDMAndInviteUserInput: FederationCreateDMAndInviteUserDto): Promise { + const { normalizedInviteeId, rawInviteeId, internalInviterId, inviteeUsernameOnly } = roomCreateDMAndInviteUserInput; + + 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 = this.bridge.isUserIdFromTheSameHomeserver( + rawInviteeId, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + const internalRoomId = FederatedRoom.buildRoomIdForDirectMessages(federatedInviterUser, federatedInviteeUser); + + if (!(await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId))) { + const externalRoomId = await this.bridge.createDirectMessageRoom(federatedInviterUser.externalId, [federatedInviteeUser.externalId]); + const newFederatedRoom = FederatedRoom.createInstance( + externalRoomId, + externalRoomId, + federatedInviterUser, + RoomType.DIRECT_MESSAGE, + '', + [federatedInviterUser, federatedInviteeUser] as any[], + ); + await this.rocketRoomAdapter.createFederatedRoom(newFederatedRoom); + } + + const federatedRoom = (await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId)) as FederatedRoom; + if (isInviteeFromTheSameHomeServer) { + await this.bridge.createUser( + inviteeUsernameOnly, + federatedInviteeUser?.internalReference?.name || normalizedInviteeId, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + await this.bridge.inviteToRoom(federatedRoom.externalId, federatedInviterUser.externalId, federatedInviteeUser.externalId); + await this.bridge.joinRoom(federatedRoom.externalId, federatedInviteeUser.externalId); + } + await this.rocketRoomAdapter.addUserToRoom(federatedRoom, federatedInviteeUser, federatedInviterUser); + } + + public async leaveRoom(afterLeaveRoomInput: FederationAfterLeaveRoomDto): Promise { + const { internalRoomId, internalUserId, whoRemovedInternalId } = afterLeaveRoomInput; + + const federatedRoom = await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoomId); + if (!federatedRoom) { + return; + } + + const federatedUser = await this.rocketUserAdapter.getFederatedUserByInternalId(internalUserId); + if (!federatedUser) { + return; + } + + if (whoRemovedInternalId) { + const who = await this.rocketUserAdapter.getFederatedUserByInternalId(whoRemovedInternalId); + await this.bridge.kickUserFromRoom(federatedRoom.externalId, federatedUser.externalId, who?.externalId as string); + return; + } + + await this.bridge.leaveRoom(federatedRoom.externalId, federatedUser.externalId); + } + + 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()); + } + + public async canAddThisUserToTheRoom(internalUser: IUser | string, internalRoom: IRoom): Promise { + const newUserBeingAdded = typeof internalUser === 'string'; + if (newUserBeingAdded) { + return; + } + + if (internalRoom.federated) { + return; + } + + const user = await this.rocketUserAdapter.getFederatedUserByInternalId((internalUser as IUser)._id); + if (user && !user.existsOnlyOnProxyServer) { + throw new Error('error-cant-add-federated-users'); + } + } + + public async canAddUsersToTheRoom(internalUser: IUser | string, internalInviter: IUser, internalRoom: IRoom): Promise { + if (!internalRoom.federated) { + return; + } + const tryingToAddNewFederatedUser = typeof internalUser === 'string'; + if (tryingToAddNewFederatedUser) { + throw new Error('error-this-is-an-ee-feature'); + } + + const invitee = await this.rocketUserAdapter.getFederatedUserByInternalId((internalUser as IUser)._id); + const inviter = await this.rocketUserAdapter.getFederatedUserByInternalId((internalInviter as IUser)._id); + const externalRoom = await this.rocketRoomAdapter.getFederatedRoomByInternalId(internalRoom._id); + if (!externalRoom || !inviter) { + return; + } + + const isARoomFromTheProxyServer = this.bridge.isRoomFromTheSameHomeserver( + externalRoom.externalId, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + const isInviterFromTheProxyServer = this.bridge.isUserIdFromTheSameHomeserver( + inviter.externalId, + this.rocketSettingsAdapter.getHomeServerDomain(), + ); + + if (!isARoomFromTheProxyServer && !isInviterFromTheProxyServer) { + return; + } + if (invitee && !invitee.existsOnlyOnProxyServer && internalRoom.t !== RoomType.DIRECT_MESSAGE) { + throw new Error('error-this-is-an-ee-feature'); + } + } + + public async beforeCreateDirectMessageFromUI(internalUsers: (IUser | string)[]): Promise { + const usernames = internalUsers.map((user) => { + if (typeof user === 'string') { + return user; + } + return user.username; + }); + const isThereAnyFederatedUser = + usernames.some((username) => username?.includes(':')) || + internalUsers.filter((user) => typeof user !== 'string').some((user) => (user as IUser).federated); + if (isThereAnyFederatedUser) { + throw new Error('error-this-is-an-ee-feature'); + } + } +} 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..ae0b2ace785e --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/input/RoomReceiverDto.ts @@ -0,0 +1,67 @@ +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; + + externalSenderId: string; +} + +export class FederationRoomChangeTopicDto extends BaseRoom { + roomTopic: string; + + externalSenderId: 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..af4c9de94d0b --- /dev/null +++ b/apps/meteor/app/federation-v2/server/application/input/RoomSenderDto.ts @@ -0,0 +1,29 @@ +import { IMessage } from '@rocket.chat/core-typings'; + +export class FederationCreateDMAndInviteUserDto { + internalInviterId: string; + + internalRoomId: string; + + rawInviteeId: string; + + normalizedInviteeId: string; + + inviteeUsernameOnly: string; +} + +export class FederationRoomSendExternalMessageDto { + internalRoomId: string; + + internalSenderId: string; + + message: IMessage; +} + +export class FederationAfterLeaveRoomDto { + internalRoomId: string; + + internalUserId: string; + + whoRemovedInternalId?: string; +} 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/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts b/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts deleted file mode 100644 index 6180c0356200..000000000000 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface IMatrixEventContentCreateRoom { - creator: string; - room_version: string; - was_programatically_created?: boolean; -} diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts b/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts deleted file mode 100644 index 920f9bb53777..000000000000 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum SetRoomJoinRules { - JOIN = 'public', - INVITE = 'invite', -} - -export interface IMatrixEventContentSetRoomJoinRules { - join_rule: SetRoomJoinRules; -} diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/index.ts b/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/index.ts deleted file mode 100644 index 7615779282e1..000000000000 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MatrixEventType } from '../MatrixEventType'; -import { IMatrixEventContentCreateRoom } from './IMatrixEventContentCreateRoom'; -import { IMatrixEventContentAddMemberToRoom } from './IMatrixEventContentAddMemberToRoom'; -import { IMatrixEventContentSendMessage } from './IMatrixEventContentSendMessage'; -import { IMatrixEventContentSetRoomJoinRules } from './IMatrixEventContentSetRoomJoinRules'; -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; -}; diff --git a/apps/meteor/app/federation-v2/server/definitions/MatrixEventType.ts b/apps/meteor/app/federation-v2/server/definitions/MatrixEventType.ts deleted file mode 100644 index 14d4f0bb0ecb..000000000000 --- a/apps/meteor/app/federation-v2/server/definitions/MatrixEventType.ts +++ /dev/null @@ -1,12 +0,0 @@ -export enum MatrixEventType { - CREATE_ROOM = 'm.room.create', - ROOM_MEMBERSHIP = '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', - // 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', -} 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..aec246f23788 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/domain/FederatedRoom.ts @@ -0,0 +1,88 @@ +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 + protected constructor() {} + + protected static generateTemporaryName(normalizedExternalId: string): string { + return `Federation-${normalizedExternalId}`; + } + + public static createInstance( + externalId: string, + normalizedExternalId: string, + creator: FederatedUser, + type: RoomType, + name?: string, + members?: FederatedUser[], + ): FederatedRoom { + const roomName = name || FederatedRoom.generateTemporaryName(normalizedExternalId); + return Object.assign(new FederatedRoom(), { + externalId, + members, + internalReference: { + t: type, + name: roomName, + fname: roomName, + u: creator.internalReference, + }, + }); + } + + public static build(): FederatedRoom { + return new FederatedRoom(); + } + + public getMembers(): IUser[] { + return this.members && this.members.length > 0 ? this.members.map((user) => user.internalReference) : []; + } + + public isFederated(): boolean { + return this.internalReference?.federated === true; + } + + public isDirectMessage(): boolean { + return this.internalReference?.t === RoomType.DIRECT_MESSAGE; + } + + public static buildRoomIdForDirectMessages(inviter: FederatedUser, invitee: FederatedUser): string { + if (!inviter.internalReference || !invitee.internalReference) { + throw new Error('Cannot create room Id without the user ids'); + } + return [inviter.internalReference, invitee.internalReference] + .map(({ _id }) => _id) + .sort() + .join(''); + } + + 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.topic = topic; + } +} 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..681f298038c9 --- /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 + protected 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..66af6beabf01 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/domain/IFederationBridge.ts @@ -0,0 +1,20 @@ +export interface IFederationBridge { + start(): Promise; + stop(): Promise; + onFederationAvailabilityChanged(enabled: boolean): Promise; + getUserProfileInformation(externalUserId: string): Promise; + joinRoom(externalRoomId: string, externalUserId: string): Promise; + createDirectMessageRoom(externalCreatorId: string, externalInviteeIds: 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; + isRoomFromTheSameHomeserver(externalRoomId: string, domain: string): boolean; + leaveRoom(externalRoomId: string, externalUserId: string): Promise; + kickUserFromRoom(externalRoomId: string, externalUserId: string, externalOwnerId: string): Promise; +} + +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..7ba7a631da0b 100644 --- a/apps/meteor/app/federation-v2/server/index.ts +++ b/apps/meteor/app/federation-v2/server/index.ts @@ -1,4 +1,53 @@ -import './settings'; -import { startBridge } from './startup'; +import { FederationFactory } from './infrastructure/Factory'; -startBridge(); +export const FEDERATION_PROCESSING_CONCURRENCY = 1; + +const rocketSettingsAdapter = FederationFactory.buildRocketSettingsAdapter(); +rocketSettingsAdapter.initialize(); +export const federationQueueInstance = FederationFactory.buildQueue(); +const federation = FederationFactory.buildBridge(rocketSettingsAdapter, federationQueueInstance); +const rocketRoomAdapter = FederationFactory.buildRocketRoomAdapter(); +const rocketUserAdapter = FederationFactory.buildRocketUserAdapter(); +const rocketMessageAdapter = FederationFactory.buildRocketMessageAdapter(); + +const federationRoomServiceReceiver = FederationFactory.buildRoomServiceReceiver( + rocketRoomAdapter, + rocketUserAdapter, + rocketMessageAdapter, + rocketSettingsAdapter, + federation, +); + +const federationEventsHandler = FederationFactory.buildEventHandlers(federationRoomServiceReceiver, rocketSettingsAdapter); + +export const federationRoomServiceSender = FederationFactory.buildRoomServiceSender( + rocketRoomAdapter, + rocketUserAdapter, + rocketSettingsAdapter, + federation, +); + +FederationFactory.setupListeners(federationRoomServiceSender); +let cancelSettingsObserver: Function; + +export const runFederation = async (): Promise => { + federationQueueInstance.setHandler(federationEventsHandler.handleEvent.bind(federationEventsHandler), FEDERATION_PROCESSING_CONCURRENCY); + cancelSettingsObserver = rocketSettingsAdapter.onFederationEnabledStatusChanged( + federation.onFederationAvailabilityChanged.bind(federation), + ); + if (!rocketSettingsAdapter.isFederationEnabled()) { + return; + } + await federation.start(); + require('./infrastructure/rocket-chat/slash-commands'); +}; + +export const stopFederation = async (): Promise => { + FederationFactory.removeListeners(); + await federation.stop(); + cancelSettingsObserver(); +}; + +(async (): Promise => { + await runFederation(); +})(); 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..e5f6370814bf --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/Factory.ts @@ -0,0 +1,114 @@ +import { IRoom, IUser } from '@rocket.chat/core-typings'; + +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 { FederationHooks } from './rocket-chat/hooks'; +import { FederationRoomSenderConverter } from './rocket-chat/converters/RoomSender'; + +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 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, + bridge: IFederationBridge, + ): FederationRoomServiceSender { + return new FederationRoomServiceSender(rocketRoomAdapter, rocketUserAdapter, rocketSettingsAdapter, 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, + rocketSettingsAdapter: RocketChatSettingsAdapter, + ): MatrixEventsHandler { + return new MatrixEventsHandler(FederationFactory.getEventHandlers(roomServiceReceive, rocketSettingsAdapter)); + } + + public static getEventHandlers( + roomServiceReceiver: FederationRoomServiceReceiver, + rocketSettingsAdapter: RocketChatSettingsAdapter, + ): any[] { + return [ + new MatrixRoomCreatedHandler(roomServiceReceiver), + new MatrixRoomMembershipChangedHandler(roomServiceReceiver, rocketSettingsAdapter), + new MatrixRoomMessageSentHandler(roomServiceReceiver), + new MatrixRoomJoinRulesChangedHandler(roomServiceReceiver), + new MatrixRoomNameChangedHandler(roomServiceReceiver), + new MatrixRoomTopicChangedHandler(roomServiceReceiver), + ]; + } + + public static setupListeners(roomServiceSender: FederationRoomServiceSender): void { + FederationHooks.afterLeaveRoom(async (user: IUser, room: IRoom) => + roomServiceSender.leaveRoom(FederationRoomSenderConverter.toAfterLeaveRoom(user._id, room._id)), + ); + FederationHooks.afterRemoveFromRoom(async (user: IUser, room: IRoom, userWhoRemoved: IUser) => + roomServiceSender.leaveRoom(FederationRoomSenderConverter.toAfterLeaveRoom(user._id, room._id, userWhoRemoved._id)), + ); + FederationHooks.canAddTheUserToTheRoom((user: IUser | string, room: IRoom) => roomServiceSender.canAddThisUserToTheRoom(user, room)); + FederationHooks.canAddUsersToTheRoom((user: IUser | string, inviter: IUser, room: IRoom) => + roomServiceSender.canAddUsersToTheRoom(user, inviter, room), + ); + FederationHooks.beforeCreateDirectMessage((members: (IUser | string)[]) => roomServiceSender.beforeCreateDirectMessageFromUI(members)); + } + + public static removeListeners(): void { + FederationHooks.removeCEValidation(); + } +} 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..01ed4bcb87a2 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/Bridge.ts @@ -0,0 +1,190 @@ +import type { AppServiceOutput, Bridge } from '@rocket.chat/forked-matrix-appservice-bridge'; + +import { IFederationBridge } from '../../domain/IFederationBridge'; +import { bridgeLogger } from '../rocket-chat/adapters/logger'; +import { IMatrixEvent } from './definitions/IMatrixEvent'; +import { MatrixEventType } from './definitions/MatrixEventType'; +import { MatrixRoomType } from './definitions/MatrixRoomType'; +import { MatrixRoomVisibility } from './definitions/MatrixRoomVisibility'; + +let MatrixUserInstance: any; + +export class MatrixBridge implements IFederationBridge { + protected bridgeInstance: Bridge; + + protected isRunning = false; + + protected isUpdatingBridgeStatus = false; + + constructor( + protected appServiceId: string, + protected homeServerUrl: string, + protected homeServerDomain: string, + protected bridgeUrl: string, + protected bridgePort: number, + protected homeServerRegistrationFile: Record, + protected eventHandler: Function, + ) { + this.logInfo(); + } + + public async onFederationAvailabilityChanged(enabled: boolean): Promise { + if (!enabled) { + await this.stop(); + return; + } + await this.start(); + } + + public async start(): Promise { + if (this.isUpdatingBridgeStatus) { + return; + } + this.isUpdatingBridgeStatus = true; + 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'); + } finally { + this.isUpdatingBridgeStatus = false; + } + } + + public async stop(): Promise { + if (!this.isRunning) { + return; + } + this.isRunning = false; + // the http server might take some minutes to shutdown, and this promise can take some time to be resolved + await this.bridgeInstance?.close(); + } + + public async getUserProfileInformation(externalUserId: string): Promise { + try { + return await 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 { + try { + await this.bridgeInstance.getIntent(externalInviterId).invite(externalRoomId, externalInviteeId); + } catch (e) { + // no-op + } + } + + public async createUser(username: string, name: string, domain: string): Promise { + if (!MatrixUserInstance) { + throw new Error('Error loading the Matrix User instance from the external library'); + } + const matrixUserId = `@${username?.toLowerCase()}:${domain}`; + const newUser = new MatrixUserInstance(matrixUserId); + await this.bridgeInstance.provisionUser(newUser, { name: `${username} (${name})` }); + + return matrixUserId; + } + + public async createDirectMessageRoom(externalCreatorId: string, externalInviteeIds: string[]): Promise { + const intent = this.bridgeInstance.getIntent(externalCreatorId); + + const visibility = MatrixRoomVisibility.PRIVATE; + const preset = MatrixRoomType.PRIVATE; + const matrixRoom = await intent.createRoom({ + createAsClient: true, + options: { + visibility, + preset, + // eslint-disable-next-line @typescript-eslint/camelcase + is_direct: true, + invite: externalInviteeIds, + // 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 { + try { + await this.bridgeInstance.getIntent(externaSenderId).sendText(externalRoomId, text); + } catch (e) { + throw new Error('User is not part of the room.'); + } + } + + public isUserIdFromTheSameHomeserver(externalUserId: string, domain: string): boolean { + const userDomain = externalUserId.includes(':') ? externalUserId.split(':').pop() : ''; + + return userDomain === domain; + } + + public getInstance(): IFederationBridge { + return this; + } + + protected logInfo(): void { + bridgeLogger.info(`Running Federation V2: + id: ${this.appServiceId} + bridgeUrl: ${this.bridgeUrl} + homeserverURL: ${this.homeServerUrl} + homeserverDomain: ${this.homeServerDomain} + `); + } + + public async leaveRoom(externalRoomId: string, externalUserId: string): Promise { + try { + await this.bridgeInstance.getIntent(externalUserId).leave(externalRoomId); + } catch (e) { + // no-op + } + } + + public async kickUserFromRoom(externalRoomId: string, externalUserId: string, externalOwnerId: string): Promise { + this.bridgeInstance.getIntent(externalOwnerId).kick(externalRoomId, externalUserId); + } + + public isRoomFromTheSameHomeserver(externalRoomId: string, domain: string): boolean { + return this.isUserIdFromTheSameHomeserver(externalRoomId, domain); + } + + protected 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, MatrixUser } = await import('@rocket.chat/forked-matrix-appservice-bridge'); + MatrixUserInstance = MatrixUser; + + this.bridgeInstance = new Bridge({ + homeserverUrl: this.homeServerUrl, + domain: this.homeServerDomain, + registration: AppServiceRegistration.fromObject(this.homeServerRegistrationFile as AppServiceOutput), + disableStores: true, + controller: { + 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..e6e75c872f5a --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/converters/RoomReceiver.ts @@ -0,0 +1,155 @@ +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, + homeServerDomain: string, + ): 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, homeServerDomain), + 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), + externalSenderId: externalEvent.sender, + normalizedRoomName: MatrixRoomReceiverConverter.normalizeRoomNameToRCFormat(externalEvent.content?.name), + }); + } + + public static toRoomChangeTopicDto(externalEvent: IMatrixEvent): FederationRoomChangeTopicDto { + return Object.assign(new FederationRoomChangeTopicDto(), { + ...MatrixRoomReceiverConverter.getBasicRoomsFields(externalEvent.room_id), + externalSenderId: externalEvent.sender, + roomTopic: externalEvent.content?.topic, + }); + } + + private static normalizeRoomNameToRCFormat(matrixRoomName = ''): string { + return matrixRoomName.replace('@', ''); + } + + protected static convertMatrixUserIdFormatToRCFormat(matrixUserId = ''): string { + return matrixUserId.replace('@', ''); + } + + protected static convertMatrixRoomIdFormatToRCFormat(matrixRoomId = ''): string { + const prefixedRoomIdOnly = matrixRoomId.split(':')[0]; + const prefix = '!'; + + return prefixedRoomIdOnly?.replace(prefix, ''); + } + + protected static formatMatrixUserIdToRCUsernameFormat(matrixUserId = ''): string { + return matrixUserId.split(':')[0]?.replace('@', ''); + } + + protected static getEventOrigin(inviterId = '', homeServerDomain: string): EVENT_ORIGIN { + const fromADifferentServer = MatrixRoomReceiverConverter.extractServerNameFromMatrixUserId(inviterId) !== homeServerDomain; + + return fromADifferentServer ? EVENT_ORIGIN.REMOTE : EVENT_ORIGIN.LOCAL; + } + + protected static extractServerNameFromMatrixUserId(matrixUserId = ''): string { + const splitted = matrixUserId.split(':'); + + return splitted.length > 1 ? splitted[1] : ''; + } + + protected static getBasicRoomsFields(externalRoomId: string): Record { + return { + externalRoomId, + normalizedRoomId: MatrixRoomReceiverConverter.convertMatrixRoomIdFormatToRCFormat(externalRoomId), + }; + } + + protected static convertMatrixJoinRuleToRCRoomType(matrixJoinRule: RoomJoinRules, matrixRoomIsDirect = false): RoomType { + if (matrixRoomIsDirect) { + return RoomType.DIRECT_MESSAGE; + } + const mapping: Record = { + [RoomJoinRules.JOIN]: RoomType.CHANNEL, + [RoomJoinRules.INVITE]: RoomType.PRIVATE_GROUP, + }; + + return mapping[matrixJoinRule] || RoomType.CHANNEL; + } + + protected 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/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts new file mode 100644 index 000000000000..f9e80f615808 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts @@ -0,0 +1,5 @@ +export interface IMatrixEventContentCreateRoom { + creator: string; + room_version: string; + 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/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts new file mode 100644 index 000000000000..f97fa09d8aa7 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules.ts @@ -0,0 +1,8 @@ +export enum RoomJoinRules { + JOIN = 'public', + INVITE = 'invite', +} + +export interface IMatrixEventContentSetRoomJoinRules { + 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/infrastructure/matrix/definitions/IMatrixEventContent/index.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/index.ts new file mode 100644 index 000000000000..e260e62ac5eb --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/IMatrixEventContent/index.ts @@ -0,0 +1,16 @@ +import { MatrixEventType } from '../MatrixEventType'; +import { IMatrixEventContentCreateRoom } from './IMatrixEventContentCreateRoom'; +import { IMatrixEventContentAddMemberToRoom } from './IMatrixEventContentAddMemberToRoom'; +import { IMatrixEventContentSendMessage } from './IMatrixEventContentSendMessage'; +import { IMatrixEventContentSetRoomJoinRules } from './IMatrixEventContentSetRoomJoinRules'; +import { IMatrixEventContentSetRoomName } from './IMatrixEventContentSetRoomName'; +import { IMatrixEventContentSetRoomTopic } from './IMatrixEventContentSetRoomTopic'; + +export type EventContent = { + [MatrixEventType.ROOM_CREATED]: IMatrixEventContentCreateRoom; + [MatrixEventType.ROOM_MEMBERSHIP_CHANGED]: IMatrixEventContentAddMemberToRoom; + [MatrixEventType.ROOM_MESSAGE_SENT]: IMatrixEventContentSendMessage; + [MatrixEventType.ROOM_JOIN_RULES_CHANGED]: IMatrixEventContentSetRoomJoinRules; + [MatrixEventType.ROOM_NAME_CHANGED]: IMatrixEventContentSetRoomName; + [MatrixEventType.ROOM_TOPIC_CHANGED]: IMatrixEventContentSetRoomTopic; +}; diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType.ts new file mode 100644 index 000000000000..fd0fa4b21924 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixEventType.ts @@ -0,0 +1,12 @@ +export enum MatrixEventType { + ROOM_CREATED = 'm.room.create', + ROOM_MEMBERSHIP_CHANGED = 'm.room.member', + ROOM_MESSAGE_SENT = 'm.room.message', + ROOM_JOIN_RULES_CHANGED = 'm.room.join_rules', + ROOM_NAME_CHANGED = 'm.room.name', + // SET_ROOM_POWER_LEVELS = 'm.room.power_levels', + // SET_ROOM_CANONICAL_ALIAS = 'm.room.canonical_alias', + // SET_ROOM_HISTORY_VISIBILITY = 'm.room.history_visibility', + // SET_ROOM_GUEST_ACCESS = 'm.room.guest_access', + ROOM_TOPIC_CHANGED = 'm.room.topic', +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixRoomType.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixRoomType.ts new file mode 100644 index 000000000000..7c591bbe590d --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixRoomType.ts @@ -0,0 +1,4 @@ +export enum MatrixRoomType { + PRIVATE = 'private_chat', + PUBLIC = 'public_chat', +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixRoomVisibility.ts b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixRoomVisibility.ts new file mode 100644 index 000000000000..0256caac480c --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/definitions/MatrixRoomVisibility.ts @@ -0,0 +1,4 @@ +export enum MatrixRoomVisibility { + PRIVATE = 'private', + PUBLIC = 'public ', +} 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..5e6b916eef50 --- /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 { + protected 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..0c7460d5a251 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/matrix/handlers/Room.ts @@ -0,0 +1,68 @@ +import { FederationRoomServiceReceiver } from '../../../application/RoomServiceReceiver'; +import { RocketChatSettingsAdapter } from '../../rocket-chat/adapters/Settings'; +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, private rocketSettingsAdapter: RocketChatSettingsAdapter) { + super(MatrixEventType.ROOM_MEMBERSHIP_CHANGED); + } + + public async handle(externalEvent: IMatrixEvent): Promise { + await this.roomService.changeRoomMembership( + MatrixRoomReceiverConverter.toChangeRoomMembershipDto(externalEvent, this.rocketSettingsAdapter.getHomeServerDomain()), + ); + } +} + +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)); + } +} + +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)); + } +} 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..5394f96e0282 --- /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(protected 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/Federation.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/Federation.ts new file mode 100644 index 000000000000..a3b3b0b3bf13 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/Federation.ts @@ -0,0 +1,27 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { IRoom, ValueOf } from '@rocket.chat/core-typings'; + +import { RoomMemberActions } from '../../../../../definition/IRoomTypeConfig'; + +const allowedActionsInFederatedRooms: ValueOf[] = [ + RoomMemberActions.REMOVE_USER, + RoomMemberActions.INVITE, + RoomMemberActions.JOIN, + RoomMemberActions.LEAVE, +]; + +export class Federation { + public static isAFederatedRoom(room: IRoom): boolean { + return room.federated === true; + } + + public static actionAllowed(room: IRoom, action: ValueOf): boolean { + return room.t === RoomType.DIRECT_MESSAGE && action === RoomMemberActions.REMOVE_USER + ? false + : allowedActionsInFederatedRooms.includes(action); + } + + public static isAFederatedUsername(username: string): boolean { + return username.includes('@') && username.includes(':'); + } +} 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/Room.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Room.ts new file mode 100644 index 000000000000..53269fb090a8 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Room.ts @@ -0,0 +1,119 @@ +import { ICreatedRoom, IRoom } from '@rocket.chat/core-typings'; +import { Rooms, Subscriptions as SubscriptionsRaw } from '@rocket.chat/models'; + +import { MatrixBridgedRoom, Subscriptions } from '../../../../../models/server'; +import { FederatedRoom } from '../../../domain/FederatedRoom'; +import { createRoom, addUserToRoom, removeUserFromRoom } from '../../../../../lib/server'; +import { FederatedUser } from '../../../domain/FederatedUser'; +import { saveRoomName } from '../../../../../channel-settings/server/functions/saveRoomName'; +import { saveRoomTopic } from '../../../../../channel-settings/server/functions/saveRoomTopic'; + +export class RocketChatRoomAdapter { + public async getFederatedRoomByExternalId(externalRoomId: string): Promise { + const internalBridgedRoomId = MatrixBridgedRoom.getId(externalRoomId); + if (!internalBridgedRoomId) { + return; + } + const room = await Rooms.findOneById(internalBridgedRoomId); + if (room) { + 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); + + if (room) { + 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, + false, + undefined, + { creator: members[0]?._id as string }, + ) as ICreatedRoom; + const roomId = rid || _id; + MatrixBridgedRoom.upsert({ rid: roomId }, { rid: roomId, mri: federatedRoom.externalId }); + await Rooms.setAsFederated(roomId); + } + + public async removeDirectMessageRoom(federatedRoom: FederatedRoom): Promise { + const roomId = federatedRoom.internalReference._id; + await Rooms.removeById(roomId); + await Subscriptions.removeByRoomId(roomId); + await MatrixBridgedRoom.remove({ rid: roomId }); + } + + public async createFederatedRoomForDirectMessage(federatedRoom: FederatedRoom, membersUsernames: string[]): Promise { + const { rid, _id } = createRoom( + federatedRoom.internalReference.t, + federatedRoom.internalReference.name, + federatedRoom.internalReference.u.username as string, + membersUsernames, + false, + undefined, + { creator: federatedRoom.internalReference.u._id }, + ) as ICreatedRoom; + const roomId = rid || _id; + MatrixBridgedRoom.upsert({ rid: roomId }, { rid: roomId, mri: federatedRoom.externalId }); + await Rooms.setAsFederated(roomId); + } + + 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, + ), + ); + } + + private createFederatedRoomInstance(externalRoomId: string, room: IRoom): FederatedRoom { + const federatedRoom = FederatedRoom.build(); + federatedRoom.externalId = externalRoomId; + federatedRoom.internalReference = room; + + return federatedRoom; + } + + public async isUserAlreadyJoined(internalRoomId: string, internalUserId: string): Promise { + const subscription = await Subscriptions.findOneByRoomIdAndUserId(internalRoomId, internalUserId, { projection: { _id: 1 } }); + + return Boolean(subscription); + } + + public async updateRoomType(federatedRoom: FederatedRoom): Promise { + await Rooms.setRoomTypeById(federatedRoom.internalReference._id, federatedRoom.internalReference.t); + await SubscriptionsRaw.updateAllRoomTypesByRoomId(federatedRoom.internalReference._id, federatedRoom.internalReference.t); + } + + public async updateRoomName(federatedRoom: FederatedRoom, federatedUser: FederatedUser): Promise { + await saveRoomName(federatedRoom.internalReference._id, federatedRoom.internalReference.name, federatedUser.internalReference); + } + + public async updateRoomTopic(federatedRoom: FederatedRoom, federatedUser: FederatedUser): Promise { + await saveRoomTopic(federatedRoom.internalReference._id, federatedRoom.internalReference.topic, federatedUser.internalReference); + } +} 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..8b8f263bf81e --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/Settings.ts @@ -0,0 +1,199 @@ +import yaml from 'js-yaml'; +import { SHA256 } from 'meteor/sha'; +import { v4 as uuidv4 } from 'uuid'; +import { Settings } from '@rocket.chat/models'; + +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(':'); + + // The port should be 3300 if none is specified on the URL + return parseInt(port || '3300'); + } + + 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 isFederationEnabled(): boolean { + return settings.get('Federation_Matrix_enabled') === true; + } + + public onFederationEnabledStatusChanged(callback: Function): Function { + return 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', + public: true, + }); + + const uniqueId = settings.get('uniqueID') || uuidv4().slice(0, 15).replace(new RegExp('-', 'g'), '_'); + 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..4dd7ada4bad0 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/User.ts @@ -0,0 +1,93 @@ +import { IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; + +import { MatrixBridgedUser } 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); + + if (user) { + return this.createFederatedUserInstance(externalUserId, user); + } + } + + public async getFederatedUserByInternalId(internalUserId: string): Promise { + const internalBridgedUserId = MatrixBridgedUser.getById(internalUserId); + if (!internalBridgedUserId) { + return; + } + const { uid: userId, mui: externalUserId, remote } = internalBridgedUserId; + const user = await Users.findOneById(userId); + + if (user) { + return this.createFederatedUserInstance(externalUserId, user, remote); + } + } + + 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, remote } = internalBridgedUserId; + + return this.createFederatedUserInstance(externalUserId, user, remote); + } + + 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 { insertedId } = await Users.insertOne({ + 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, + createdAt: new Date(), + federated: true, + }); + MatrixBridgedUser.upsert( + { uid: insertedId }, + { + uid: insertedId, + mui: federatedUser.externalId, + remote: !federatedUser.existsOnlyOnProxyServer, + }, + ); + } + + private createFederatedUserInstance(externalUserId: string, user: IUser, remote = true): FederatedUser { + const federatedUser = FederatedUser.build(); + federatedUser.externalId = externalUserId; + federatedUser.internalReference = user; + federatedUser.existsOnlyOnProxyServer = !remote; + + return federatedUser; + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/logger.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/logger.ts new file mode 100644 index 000000000000..331ed9f5f4b4 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/adapters/logger.ts @@ -0,0 +1,6 @@ +import { Logger } from '../../../../../logger/server'; + +const logger = new Logger('Federation_Matrix'); + +export const bridgeLogger = logger.section('bridge'); +export const setupLogger = logger.section('setup'); 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..e6a25fff9e1d --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/converters/RoomSender.ts @@ -0,0 +1,50 @@ +import { IMessage } from '@rocket.chat/core-typings'; + +import { + FederationAfterLeaveRoomDto, + FederationCreateDMAndInviteUserDto, + FederationRoomSendExternalMessageDto, +} from '../../../application/input/RoomSenderDto'; + +export class FederationRoomSenderConverter { + public static toCreateDirectMessageRoomDto( + internalInviterId: string, + internalRoomId: string, + externalInviteeId: string, + ): FederationCreateDMAndInviteUserDto { + const normalizedInviteeId = externalInviteeId.replace('@', ''); + const inviteeUsernameOnly = externalInviteeId.split(':')[0]?.replace('@', ''); + + return Object.assign(new FederationCreateDMAndInviteUserDto(), { + internalInviterId, + internalRoomId, + rawInviteeId: externalInviteeId, + normalizedInviteeId, + inviteeUsernameOnly, + }); + } + + public static toSendExternalMessageDto( + internalSenderId: string, + internalRoomId: string, + message: IMessage, + ): FederationRoomSendExternalMessageDto { + return Object.assign(new FederationRoomSendExternalMessageDto(), { + internalRoomId, + internalSenderId, + message, + }); + } + + public static toAfterLeaveRoom( + internalUserId: string, + internalRoomId: string, + whoRemovedInternalId?: string, + ): FederationAfterLeaveRoomDto { + return Object.assign(new FederationAfterLeaveRoomDto(), { + internalRoomId, + internalUserId, + whoRemovedInternalId, + }); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/hooks/index.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/hooks/index.ts new file mode 100644 index 000000000000..d7e75c817adc --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/hooks/index.ts @@ -0,0 +1,71 @@ +import { IRoom, IUser } from '@rocket.chat/core-typings'; + +import { callbacks } from '../../../../../../lib/callbacks'; + +export class FederationHooks { + public static afterLeaveRoom(callback: Function): void { + callbacks.add( + 'afterLeaveRoom', + (user: IUser, room: IRoom | undefined): void => { + if (!room?.federated) { + return; + } + Promise.await(callback(user, room)); + }, + callbacks.priority.HIGH, + 'federation-v2-after-leave-room', + ); + } + + public static afterRemoveFromRoom(callback: Function): void { + callbacks.add( + 'afterRemoveFromRoom', + (params: { removedUser: IUser; userWhoRemoved: IUser }, room: IRoom | undefined): void => { + if (!room?.federated) { + return; + } + Promise.await(callback(params.removedUser, room, params.userWhoRemoved)); + }, + callbacks.priority.HIGH, + 'federation-v2-after-remove-from-room', + ); + } + + public static canAddTheUserToTheRoom(callback: Function): void { + callbacks.add( + 'federation.beforeAddUserAToRoom', + (params: { user: IUser | string }, room: IRoom): void => { + Promise.await(callback(params.user, room)); + }, + callbacks.priority.HIGH, + 'federation-v2-can-add-user-to-the-room', + ); + } + + public static canAddUsersToTheRoom(callback: Function): void { + callbacks.add( + 'federation.beforeAddUserAToRoom', + (params: { user: IUser | string; inviter: IUser }, room: IRoom): void => { + Promise.await(callback(params.user, params.inviter, room)); + }, + callbacks.priority.HIGH, + 'federation-v2-can-add-users-to-the-room', + ); + } + + public static beforeCreateDirectMessage(callback: Function): void { + callbacks.add( + 'federation.beforeCreateDirectMessage', + (members: IUser[]): void => { + Promise.await(callback(members)); + }, + callbacks.priority.HIGH, + 'federation-v2-before-create-direct-message-ce', + ); + } + + public static removeCEValidation(): void { + callbacks.remove('federation.beforeAddUserAToRoom', 'federation-v2-can-add-users-to-the-room'); + callbacks.remove('federation.beforeCreateDirectMessage', 'federation-v2-before-create-direct-message-ce'); + } +} diff --git a/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/slash-commands/index.ts b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/slash-commands/index.ts new file mode 100644 index 000000000000..2aa502c3ee88 --- /dev/null +++ b/apps/meteor/app/federation-v2/server/infrastructure/rocket-chat/slash-commands/index.ts @@ -0,0 +1,62 @@ +import { Meteor } from 'meteor/meteor'; +import { Users } from '@rocket.chat/models'; + +import { federationRoomServiceSender } from '../../..'; +import { FederationRoomSenderConverter } from '../converters/RoomSender'; +import { slashCommands } from '../../../../../utils/lib/slashCommand'; + +const FEDERATION_COMMANDS: Record = { + dm: async (currentUserId: string, roomId: string, invitee: string) => + federationRoomServiceSender.createDirectMessageRoomAndInviteUser( + FederationRoomSenderConverter.toCreateDirectMessageRoomDto(currentUserId, roomId, invitee), + ), +}; + +export const normalizeUserId = (rawUserId: string): string => `@${rawUserId.replace('@', '')}`; + +const validateUserIdFormat = async (rawUserId: string, inviterId: string): Promise => { + const inviter = await Users.findOneById(inviterId); + const isInviterExternal = inviter?.federated === true || inviter?.username?.includes(':'); + if (!rawUserId.includes(':') && !isInviterExternal) { + throw new Error('Invalid userId format for federation command.'); + } +}; + +const executeSlashCommand = async ( + providedCommand: string, + stringParams: string | undefined, + item: Record, + commands: Record, +): Promise => { + if (providedCommand !== 'federation' || !stringParams) { + return; + } + + const [command, ...params] = stringParams.trim().split(' '); + const [rawUserId] = params; + const currentUserId = Meteor.userId(); + if (!currentUserId || !commands[command]) { + return; + } + + await validateUserIdFormat(rawUserId, currentUserId); + + const invitee = normalizeUserId(rawUserId); + + const { rid: roomId } = item; + + await commands[command](currentUserId, roomId, invitee); +}; + +function federation(providedCommand: string, stringParams: string | undefined, item: Record): void { + Promise.await(executeSlashCommand(providedCommand, stringParams, item, FEDERATION_COMMANDS)); +} + +slashCommands.add({ + command: 'federation', + callback: federation, + options: { + description: 'Federation_slash_commands', + params: '#command (dm) #user', + }, +}); diff --git a/apps/meteor/app/federation-v2/server/logger.ts b/apps/meteor/app/federation-v2/server/logger.ts deleted file mode 100644 index 0b88f48bfde6..000000000000 --- a/apps/meteor/app/federation-v2/server/logger.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Logger } from '../../logger/server'; - -const logger = new Logger('Federation_Matrix'); - -export const bridgeLogger = logger.section('bridge'); -export const setupLogger = logger.section('setup'); 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/federation/server/endpoints/dispatch.js b/apps/meteor/app/federation/server/endpoints/dispatch.js index 3c841ca577a6..3e1f478ad39d 100644 --- a/apps/meteor/app/federation/server/endpoints/dispatch.js +++ b/apps/meteor/app/federation/server/endpoints/dispatch.js @@ -1,10 +1,10 @@ import { EJSON } from 'meteor/ejson'; +import { FederationServers } from '@rocket.chat/models'; import { API } from '../../../api/server'; import { serverLogger } from '../lib/logger'; import { contextDefinitions, eventTypes } from '../../../models/server/models/FederationEvents'; import { FederationRoomEvents, Messages, Rooms, Subscriptions, Users } from '../../../models/server'; -import { FederationServers } from '../../../models/server/raw'; import { normalizers } from '../normalizers'; import { deleteRoom } from '../../../lib/server/functions'; import { api } from '../../../../server/sdk/api'; diff --git a/apps/meteor/app/federation/server/endpoints/uploads.js b/apps/meteor/app/federation/server/endpoints/uploads.js index 146ac27517a4..2edbbbdb2e62 100644 --- a/apps/meteor/app/federation/server/endpoints/uploads.js +++ b/apps/meteor/app/federation/server/endpoints/uploads.js @@ -1,5 +1,6 @@ +import { Uploads } from '@rocket.chat/models'; + import { API } from '../../../api/server'; -import { Uploads } from '../../../models/server/raw'; import { FileUpload } from '../../../file-upload/server'; import { isFederationEnabled } from '../lib/isFederationEnabled'; diff --git a/apps/meteor/app/federation/server/functions/addUser.js b/apps/meteor/app/federation/server/functions/addUser.js index 314b7893fbc1..866185354d6a 100644 --- a/apps/meteor/app/federation/server/functions/addUser.js +++ b/apps/meteor/app/federation/server/functions/addUser.js @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; +import { FederationServers } from '@rocket.chat/models'; import * as federationErrors from './errors'; import { Users } from '../../../models/server'; -import { FederationServers } from '../../../models/server/raw'; import { getUserByUsername } from '../handler'; export async function addUser(query) { diff --git a/apps/meteor/app/federation/server/functions/dashboard.js b/apps/meteor/app/federation/server/functions/dashboard.js index 3868d98d821b..149e2215f73f 100644 --- a/apps/meteor/app/federation/server/functions/dashboard.js +++ b/apps/meteor/app/federation/server/functions/dashboard.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; +import { FederationServers } from '@rocket.chat/models'; import { FederationRoomEvents, Users } from '../../../models/server'; -import { FederationServers } from '../../../models/server/raw'; export async function getStatistics() { const numberOfEvents = FederationRoomEvents.find().count(); diff --git a/apps/meteor/app/federation/server/functions/helpers.ts b/apps/meteor/app/federation/server/functions/helpers.ts index 5d76fa4cd363..494934b6a18d 100644 --- a/apps/meteor/app/federation/server/functions/helpers.ts +++ b/apps/meteor/app/federation/server/functions/helpers.ts @@ -1,8 +1,8 @@ import { IRoom, isDirectMessageRoom } from '@rocket.chat/core-typings'; import type { ISubscription, IRegisterUser, IUser } from '@rocket.chat/core-typings'; +import { Settings } from '@rocket.chat/models'; import { Subscriptions, Users } from '../../../models/server'; -import { Settings } from '../../../models/server/raw'; import { STATUS_ENABLED, STATUS_REGISTERING } from '../constants'; export const getNameAndDomain = (fullyQualifiedName: string): string[] => fullyQualifiedName.split('@'); diff --git a/apps/meteor/app/federation/server/lib/crypt.js b/apps/meteor/app/federation/server/lib/crypt.js index 5a7685a2e9e0..4abe78248a8b 100644 --- a/apps/meteor/app/federation/server/lib/crypt.js +++ b/apps/meteor/app/federation/server/lib/crypt.js @@ -1,4 +1,5 @@ -import { FederationKeys } from '../../../models/server/raw'; +import { FederationKeys } from '@rocket.chat/models'; + import { getFederationDomain } from './getFederationDomain'; import { search } from './dns'; import { cryptLogger } from './logger'; diff --git a/apps/meteor/app/federation/server/startup/generateKeys.js b/apps/meteor/app/federation/server/startup/generateKeys.js index 5d5bbba64e00..da372335a662 100644 --- a/apps/meteor/app/federation/server/startup/generateKeys.js +++ b/apps/meteor/app/federation/server/startup/generateKeys.js @@ -1,4 +1,4 @@ -import { FederationKeys } from '../../../models/server/raw'; +import { FederationKeys } from '@rocket.chat/models'; // Create key pair if needed (async () => { diff --git a/apps/meteor/app/federation/server/startup/settings.ts b/apps/meteor/app/federation/server/startup/settings.ts index 04a9c6a49add..ee216054db04 100644 --- a/apps/meteor/app/federation/server/startup/settings.ts +++ b/apps/meteor/app/federation/server/startup/settings.ts @@ -1,3 +1,5 @@ +import { FederationKeys } from '@rocket.chat/models'; + import { settingsRegistry, settings } from '../../../settings/server'; import { updateStatus, updateEnabled, isRegisteringOrEnabled } from '../functions/helpers'; import { getFederationDomain } from '../lib/getFederationDomain'; @@ -5,7 +7,6 @@ import { getFederationDiscoveryMethod } from '../lib/getFederationDiscoveryMetho import { registerWithHub } from '../lib/dns'; import { enableCallbacks, disableCallbacks } from '../lib/callbacks'; import { setupLogger } from '../lib/logger'; -import { FederationKeys } from '../../../models/server/raw'; import { STATUS_ENABLED, STATUS_REGISTERING, STATUS_ERROR_REGISTERING, STATUS_DISABLED } from '../constants'; settingsRegistry.addGroup('Federation', function () { diff --git a/apps/meteor/app/file-upload/client/lib/fileUploadHandler.js b/apps/meteor/app/file-upload/client/lib/fileUploadHandler.js index 619cc70cb0aa..2764be6781dc 100644 --- a/apps/meteor/app/file-upload/client/lib/fileUploadHandler.js +++ b/apps/meteor/app/file-upload/client/lib/fileUploadHandler.js @@ -4,7 +4,7 @@ import { Tracker } from 'meteor/tracker'; import { UploadFS } from 'meteor/jalik:ufs'; import { FileUploadBase } from '../../lib/FileUploadBase'; -import { Uploads, Avatars } from '../../../models'; +import { Uploads, Avatars } from '../../../models/client'; new UploadFS.Store({ collection: Uploads.model, diff --git a/apps/meteor/app/file-upload/server/config/AmazonS3.js b/apps/meteor/app/file-upload/server/config/AmazonS3.js index 04f27c099da9..e265311786d6 100644 --- a/apps/meteor/app/file-upload/server/config/AmazonS3.js +++ b/apps/meteor/app/file-upload/server/config/AmazonS3.js @@ -3,7 +3,7 @@ import https from 'https'; import _ from 'underscore'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import '../../ufs/AmazonS3/server.js'; import { SystemLogger } from '../../../../server/lib/logger/system'; diff --git a/apps/meteor/app/file-upload/server/config/GoogleStorage.js b/apps/meteor/app/file-upload/server/config/GoogleStorage.js index f3f0de3e29d8..ce900d9811aa 100644 --- a/apps/meteor/app/file-upload/server/config/GoogleStorage.js +++ b/apps/meteor/app/file-upload/server/config/GoogleStorage.js @@ -4,7 +4,7 @@ import https from 'https'; import _ from 'underscore'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import '../../ufs/GoogleStorage/server.js'; import { SystemLogger } from '../../../../server/lib/logger/system'; diff --git a/apps/meteor/app/file-upload/server/config/Webdav.js b/apps/meteor/app/file-upload/server/config/Webdav.js index f3fe904ccc56..7ca78cad5da6 100644 --- a/apps/meteor/app/file-upload/server/config/Webdav.js +++ b/apps/meteor/app/file-upload/server/config/Webdav.js @@ -1,7 +1,7 @@ import _ from 'underscore'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import '../../ufs/Webdav/server.js'; import { SystemLogger } from '../../../../server/lib/logger/system'; diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.js b/apps/meteor/app/file-upload/server/lib/FileUpload.js index 5bba041a6f2d..7d7afd4c8dc0 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.js +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.js @@ -12,9 +12,9 @@ import { Match } from 'meteor/check'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import filesize from 'filesize'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; +import { Avatars, UserDataFiles, Uploads } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; -import { Avatars, UserDataFiles, Uploads } from '../../../models/server/raw'; import Users from '../../../models/server/models/Users'; import Rooms from '../../../models/server/models/Rooms'; import Settings from '../../../models/server/models/Settings'; diff --git a/apps/meteor/app/file-upload/server/lib/requests.js b/apps/meteor/app/file-upload/server/lib/requests.js index e4d8bb2e36b4..87f934f4a4f3 100644 --- a/apps/meteor/app/file-upload/server/lib/requests.js +++ b/apps/meteor/app/file-upload/server/lib/requests.js @@ -1,7 +1,7 @@ import { WebApp } from 'meteor/webapp'; +import { Uploads } from '@rocket.chat/models'; import { FileUpload } from './FileUpload'; -import { Uploads } from '../../../models/server/raw'; WebApp.connectHandlers.use(FileUpload.getPath(), async function (req, res, next) { const match = /^\/([^\/]+)\/(.*)/.exec(req.url); diff --git a/apps/meteor/app/file-upload/server/methods/getS3FileUrl.js b/apps/meteor/app/file-upload/server/methods/getS3FileUrl.js index a4156a35451d..6a538caee41c 100644 --- a/apps/meteor/app/file-upload/server/methods/getS3FileUrl.js +++ b/apps/meteor/app/file-upload/server/methods/getS3FileUrl.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import { UploadFS } from 'meteor/jalik:ufs'; +import { Uploads } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; -import { Uploads } from '../../../models/server/raw'; import { canAccessRoom } from '../../../authorization/server'; let protectedFiles; diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 9bf8c0ea6129..ef0b86012e42 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -4,8 +4,8 @@ import { Match, check } from 'meteor/check'; import _ from 'underscore'; import { MessageAttachment, FileAttachmentProps } from '@rocket.chat/core-typings'; import type { IUser } from '@rocket.chat/core-typings'; +import { Rooms, Uploads } from '@rocket.chat/models'; -import { Rooms, Uploads } from '../../../models/server/raw'; import { callbacks } from '../../../../lib/callbacks'; import { FileUpload } from '../lib/FileUpload'; import { canAccessRoom } from '../../../authorization/server/functions/canAccessRoom'; @@ -21,6 +21,9 @@ Meteor.methods({ } const room = await Rooms.findOneById(roomId); + if (!room) { + return false; + } if (user?.type !== 'app' && !canAccessRoom(room, user)) { return false; @@ -126,12 +129,12 @@ Meteor.methods({ const msg = Meteor.call('sendMessage', { rid: roomId, ts: new Date(), - msg: '', file: files[0], files, - groupable: false, attachments, ...msgData, + msg: msgData.msg ?? '', + groupable: msgData.groupable ?? false, }); callbacks.runAsync('afterFileUpload', { user, room, message: msg }); diff --git a/apps/meteor/app/file/server/file.server.js b/apps/meteor/app/file/server/file.server.js index 8c3bb86723de..ae3c4a3c796a 100644 --- a/apps/meteor/app/file/server/file.server.js +++ b/apps/meteor/app/file/server/file.server.js @@ -4,13 +4,10 @@ import path from 'path'; import { Meteor } from 'meteor/meteor'; import { MongoInternals } from 'meteor/mongo'; -import Grid from 'gridfs-stream'; import mkdirp from 'mkdirp'; -// Fix problem with usernames being converted to object id -Grid.prototype.tryParseObjectId = function () { - return false; -}; +const mongo = MongoInternals.NpmModule; +const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; const RocketChatFile = {}; @@ -40,37 +37,30 @@ RocketChatFile.GridFS = class { this.name = name; this.transformWrite = transformWrite; - const mongo = MongoInternals.NpmModule; - const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; - this.store = new Grid(db, mongo); - this.findOneSync = Meteor.wrapAsync(this.store.collection(this.name).findOne.bind(this.store.collection(this.name))); - this.removeSync = Meteor.wrapAsync(this.store.remove.bind(this.store)); - this.countSync = Meteor.wrapAsync(this.store._col.count.bind(this.store._col)); + + this.bucket = new mongo.GridFSBucket(db, { bucketName: this.name }); + this.getFileSync = Meteor.wrapAsync(this.getFile.bind(this)); } - findOne(fileName) { - return this.findOneSync({ - _id: fileName, - }); + findOne(filename) { + const file = Promise.await(this.bucket.find({ filename }).limit(1).toArray()); + if (!file) { + return; + } + return file[0]; } - remove(fileName) { - return this.removeSync({ - _id: fileName, - root: this.name, - }); + remove(fileId) { + Promise.await(this.bucket.delete(fileId)); } createWriteStream(fileName, contentType) { const self = this; - let ws = this.store.createWriteStream({ - _id: fileName, - filename: fileName, - mode: 'w', - root: this.name, - content_type: contentType, + let ws = this.bucket.openUploadStream(fileName, { + contentType, }); + if (self.transformWrite != null) { ws = RocketChatFile.addPassThrough(ws, function (rs, ws) { const file = { @@ -88,10 +78,7 @@ RocketChatFile.GridFS = class { } createReadStream(fileName) { - return this.store.createReadStream({ - _id: fileName, - root: this.name, - }); + return this.bucket.openDownloadStreamByName(fileName); } getFileWithReadStream(fileName) { @@ -138,7 +125,7 @@ RocketChatFile.GridFS = class { if (file == null) { return undefined; } - return this.remove(fileName); + return this.remove(file._id); } }; diff --git a/apps/meteor/app/importer-csv/server/importer.js b/apps/meteor/app/importer-csv/server/importer.js index d4d3d014dbfa..1d2ce78e38dd 100644 --- a/apps/meteor/app/importer-csv/server/importer.js +++ b/apps/meteor/app/importer-csv/server/importer.js @@ -7,7 +7,9 @@ export class CsvImporter extends Base { constructor(info, importRecord) { super(info, importRecord); - this.csvParser = require('csv-parse/lib/sync'); + const { parse } = require('csv-parse/lib/sync'); + + this.csvParser = parse; } prepareUsingLocalFile(fullFilePath) { diff --git a/apps/meteor/app/importer-pending-avatars/server/importer.js b/apps/meteor/app/importer-pending-avatars/server/importer.js index 2726cb35401b..39fb6fd8b6ae 100644 --- a/apps/meteor/app/importer-pending-avatars/server/importer.js +++ b/apps/meteor/app/importer-pending-avatars/server/importer.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Base, ProgressStep, Selection } from '../../importer/server'; -import { Users } from '../../models'; +import { Users } from '../../models/server'; export class PendingAvatarImporter extends Base { prepareFileCount() { diff --git a/apps/meteor/app/importer-pending-files/server/importer.js b/apps/meteor/app/importer-pending-files/server/importer.js index a0f6899a3f9d..dd29caf8dc10 100644 --- a/apps/meteor/app/importer-pending-files/server/importer.js +++ b/apps/meteor/app/importer-pending-files/server/importer.js @@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { Base, ProgressStep, Selection } from '../../importer/server'; -import { Messages } from '../../models'; +import { Messages } from '../../models/server'; import { FileUpload } from '../../file-upload'; export class PendingFileImporter extends Base { diff --git a/apps/meteor/app/importer-slack-users/server/importer.js b/apps/meteor/app/importer-slack-users/server/importer.js index 5fcf3924499b..da35a8d1bfc4 100644 --- a/apps/meteor/app/importer-slack-users/server/importer.js +++ b/apps/meteor/app/importer-slack-users/server/importer.js @@ -4,14 +4,15 @@ import { Random } from 'meteor/random'; import { RawImports, Base, ProgressStep, Selection, SelectionUser } from '../../importer/server'; import { RocketChatFile } from '../../file'; -import { Users } from '../../models'; -import { Settings as SettingsRaw } from '../../models/server'; +import { Users, Settings as SettingsRaw } from '../../models/server'; export class SlackUsersImporter extends Base { constructor(info, importRecord) { super(info, importRecord); - this.csvParser = require('csv-parse/lib/sync'); + const { parse } = require('csv-parse/lib/sync'); + + this.csvParser = parse; this.userMap = new Map(); this.admins = []; // Array of ids of the users which are admins } diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index 8c58b7a61f8d..ff174cb26696 100644 --- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts @@ -13,8 +13,8 @@ import type { IUser, IUserEmail, } from '@rocket.chat/core-typings'; +import { ImportData as ImportDataRaw } from '@rocket.chat/models'; -import { ImportData as ImportDataRaw } from '../../../models/server/raw'; import { IConversionCallbacks } from '../definitions/IConversionCallbacks'; import { Users, Rooms, Subscriptions, ImportData } from '../../../models/server'; import { generateUsernameSuggestion, insertMessage, saveUserIdentity, addUserToDefaultChannels } from '../../../lib/server'; @@ -367,7 +367,7 @@ export class ImportDataConverter { } } catch (e) { this._logger.error(e); - this.saveError(_id, e); + this.saveError(_id, e instanceof Error ? e : new Error(String(e))); } }); } @@ -622,7 +622,7 @@ export class ImportDataConverter { afterImportFn(data, 'message', true); } } catch (e) { - this.saveError(_id, e); + this.saveError(_id, e instanceof Error ? e : new Error(String(e))); } }); @@ -932,7 +932,7 @@ export class ImportDataConverter { afterImportFn(data, 'channel', !existingRoom); } } catch (e) { - this.saveError(_id, e); + this.saveError(_id, e instanceof Error ? e : new Error(String(e))); } }); } diff --git a/apps/meteor/app/importer/server/classes/ImporterBase.js b/apps/meteor/app/importer/server/classes/ImporterBase.js index fd4e7a9972c8..73ebfdd161a6 100644 --- a/apps/meteor/app/importer/server/classes/ImporterBase.js +++ b/apps/meteor/app/importer/server/classes/ImporterBase.js @@ -11,10 +11,9 @@ import { ImporterWebsocket } from './ImporterWebsocket'; import { ProgressStep } from '../../lib/ImporterProgressStep'; import { ImporterInfo } from '../../lib/ImporterInfo'; import { RawImports } from '../models/RawImports'; -import { Settings, Imports } from '../../../models'; +import { Settings, Imports, ImportData } from '../../../models/server'; import { Logger } from '../../../logger'; import { ImportDataConverter } from './ImportDataConverter'; -import { ImportData } from '../../../models/server'; import { t } from '../../../utils/server'; import { Selection, SelectionChannel, SelectionUser } from '..'; diff --git a/apps/meteor/app/importer/server/methods/downloadPublicImportFile.js b/apps/meteor/app/importer/server/methods/downloadPublicImportFile.js index 6424c633bdbe..6facd718f0e4 100644 --- a/apps/meteor/app/importer/server/methods/downloadPublicImportFile.js +++ b/apps/meteor/app/importer/server/methods/downloadPublicImportFile.js @@ -57,7 +57,7 @@ Meteor.methods({ importer.instance = new importer.importer(importer); // eslint-disable-line new-cap - const oldFileName = fileUrl.substring(fileUrl.lastIndexOf('/') + 1); + const oldFileName = fileUrl.substring(fileUrl.lastIndexOf('/') + 1).split('?')[0]; const date = new Date(); const dateStr = `${date.getUTCFullYear()}${date.getUTCMonth()}${date.getUTCDate()}${date.getUTCHours()}${date.getUTCMinutes()}${date.getUTCSeconds()}`; const newFileName = `${dateStr}_${userId}_${oldFileName}`; diff --git a/apps/meteor/app/importer/server/methods/getImportFileData.js b/apps/meteor/app/importer/server/methods/getImportFileData.js index 32c9aa6d0cd3..750729b39e76 100644 --- a/apps/meteor/app/importer/server/methods/getImportFileData.js +++ b/apps/meteor/app/importer/server/methods/getImportFileData.js @@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { RocketChatImportFileInstance } from '../startup/store'; import { hasPermission } from '../../../authorization'; -import { Imports } from '../../../models'; +import { Imports } from '../../../models/server'; import { ProgressStep } from '../../lib/ImporterProgressStep'; import { Importers } from '..'; diff --git a/apps/meteor/app/importer/server/methods/getImportProgress.js b/apps/meteor/app/importer/server/methods/getImportProgress.js index 31c9b2d4a5d5..b6a157ad37f0 100644 --- a/apps/meteor/app/importer/server/methods/getImportProgress.js +++ b/apps/meteor/app/importer/server/methods/getImportProgress.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; -import { Imports } from '../../../models'; +import { Imports } from '../../../models/server'; import { Importers } from '..'; Meteor.methods({ diff --git a/apps/meteor/app/importer/server/methods/startImport.js b/apps/meteor/app/importer/server/methods/startImport.js index 45abfe5988f0..c9ef7e0a04c3 100644 --- a/apps/meteor/app/importer/server/methods/startImport.js +++ b/apps/meteor/app/importer/server/methods/startImport.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; -import { Imports } from '../../../models'; +import { Imports } from '../../../models/server'; import { Importers, Selection, SelectionChannel, SelectionUser } from '..'; Meteor.methods({ diff --git a/apps/meteor/app/importer/server/methods/uploadImportFile.js b/apps/meteor/app/importer/server/methods/uploadImportFile.js index 115559664982..b23183210395 100644 --- a/apps/meteor/app/importer/server/methods/uploadImportFile.js +++ b/apps/meteor/app/importer/server/methods/uploadImportFile.js @@ -6,6 +6,38 @@ import { hasPermission } from '../../../authorization'; import { ProgressStep } from '../../lib/ImporterProgressStep'; import { Importers } from '..'; +export const executeUploadImportFile = (userId, binaryContent, contentType, fileName, importerKey) => { + const importer = Importers.get(importerKey); + if (!importer) { + throw new Meteor.Error('error-importer-not-defined', `The importer (${importerKey}) has no import class defined.`, { + method: 'uploadImportFile', + }); + } + + importer.instance = new importer.importer(importer); // eslint-disable-line new-cap + + const date = new Date(); + const dateStr = `${date.getUTCFullYear()}${date.getUTCMonth()}${date.getUTCDate()}${date.getUTCHours()}${date.getUTCMinutes()}${date.getUTCSeconds()}`; + const newFileName = `${dateStr}_${userId}_${fileName}`; + + // Store the file name and content type on the imports collection + importer.instance.startFileUpload(newFileName, contentType); + + // Save the file on the File Store + const file = Buffer.from(binaryContent, 'base64'); + const readStream = RocketChatFile.bufferToStream(file); + const writeStream = RocketChatImportFileInstance.createWriteStream(newFileName, contentType); + + writeStream.on( + 'end', + Meteor.bindEnvironment(() => { + importer.instance.updateProgress(ProgressStep.FILE_LOADED); + }), + ); + + readStream.pipe(writeStream); +}; + Meteor.methods({ uploadImportFile(binaryContent, contentType, fileName, importerKey) { const userId = Meteor.userId(); @@ -20,34 +52,6 @@ Meteor.methods({ }); } - const importer = Importers.get(importerKey); - if (!importer) { - throw new Meteor.Error('error-importer-not-defined', `The importer (${importerKey}) has no import class defined.`, { - method: 'uploadImportFile', - }); - } - - importer.instance = new importer.importer(importer); // eslint-disable-line new-cap - - const date = new Date(); - const dateStr = `${date.getUTCFullYear()}${date.getUTCMonth()}${date.getUTCDate()}${date.getUTCHours()}${date.getUTCMinutes()}${date.getUTCSeconds()}`; - const newFileName = `${dateStr}_${userId}_${fileName}`; - - // Store the file name and content type on the imports collection - importer.instance.startFileUpload(newFileName, contentType); - - // Save the file on the File Store - const file = Buffer.from(binaryContent, 'base64'); - const readStream = RocketChatFile.bufferToStream(file); - const writeStream = RocketChatImportFileInstance.createWriteStream(newFileName, contentType); - - writeStream.on( - 'end', - Meteor.bindEnvironment(() => { - importer.instance.updateProgress(ProgressStep.FILE_LOADED); - }), - ); - - readStream.pipe(writeStream); + executeUploadImportFile(userId, binaryContent, contentType, fileName, importerKey); }, }); diff --git a/apps/meteor/app/importer/server/models/RawImports.js b/apps/meteor/app/importer/server/models/RawImports.js index 168b73b3e07b..470c1a4ff7cc 100644 --- a/apps/meteor/app/importer/server/models/RawImports.js +++ b/apps/meteor/app/importer/server/models/RawImports.js @@ -1,4 +1,4 @@ -import { Base } from '../../../models'; +import { Base } from '../../../models/server'; class RawImportsModel extends Base { constructor() { diff --git a/apps/meteor/app/importer/server/startup/setImportsToInvalid.js b/apps/meteor/app/importer/server/startup/setImportsToInvalid.js index 691a42cd19b7..9a4ec9b728f3 100644 --- a/apps/meteor/app/importer/server/startup/setImportsToInvalid.js +++ b/apps/meteor/app/importer/server/startup/setImportsToInvalid.js @@ -29,11 +29,11 @@ Meteor.startup(function () { Imports.invalidateOperationsExceptId(idToKeep); // Clean up all the raw import data, except for the last operation - runDrop(() => RawImports.model.rawCollection().remove({ import: { $ne: idToKeep } })); + runDrop(() => RawImports.model.rawCollection().deleteMany({ import: { $ne: idToKeep } })); } else { Imports.invalidateAllOperations(); // Clean up all the raw import data - runDrop(() => RawImports.model.rawCollection().remove({})); + runDrop(() => RawImports.model.rawCollection().deleteMany({})); } }); diff --git a/apps/meteor/app/importer/server/startup/store.js b/apps/meteor/app/importer/server/startup/store.js index 2026945ba3f2..22ba5eadce34 100644 --- a/apps/meteor/app/importer/server/startup/store.js +++ b/apps/meteor/app/importer/server/startup/store.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { RocketChatFile } from '../../../file'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; export let RocketChatImportFileInstance; diff --git a/apps/meteor/app/integrations/server/api/api.js b/apps/meteor/app/integrations/server/api/api.js index 57a4c2b132bd..724bfea9ad80 100644 --- a/apps/meteor/app/integrations/server/api/api.js +++ b/apps/meteor/app/integrations/server/api/api.js @@ -8,12 +8,12 @@ import Future from 'fibers/future'; import _ from 'underscore'; import s from 'underscore.string'; import moment from 'moment'; +import { Integrations } from '@rocket.chat/models'; import { incomingLogger } from '../logger'; import { processWebhookMessage } from '../../../lib/server'; import { API, APIClass, defaultRateLimiterOptions } from '../../../api/server'; import * as Models from '../../../models/server'; -import { Integrations } from '../../../models/server/raw'; import { settings } from '../../../settings/server'; const compiledScripts = {}; @@ -67,7 +67,7 @@ function getIntegrationScript(integration) { const script = integration.scriptCompiled; const { sandbox, store } = buildSandbox(); try { - incomingLogger.info({ msg: 'Will evaluate script of Trigger', name: integration.name }); + incomingLogger.info({ msg: 'Will evaluate script of Trigger', integration: integration.name }); incomingLogger.debug(script); const vmScript = new VMScript(`${script}; Script;`, 'script.js'); @@ -89,20 +89,20 @@ function getIntegrationScript(integration) { } catch (err) { incomingLogger.error({ msg: 'Error evaluating Script in Trigger', - name: integration.name, + integration: integration.name, script, err, }); throw API.v1.failure('error-evaluating-script'); } - incomingLogger.error({ msg: 'Class "Script" not in Trigger', name: integration.name }); + incomingLogger.error({ msg: 'Class "Script" not in Trigger', integration: integration.name }); throw API.v1.failure('class-script-not-found'); } function createIntegration(options, user) { - incomingLogger.info({ msg: 'Add integration', name: options.name }); - incomingLogger.debug(options); + incomingLogger.info({ msg: 'Add integration', integration: options.name }); + incomingLogger.debug({ options }); Meteor.runAsUser(user._id, function () { switch (options.event) { @@ -139,7 +139,7 @@ function createIntegration(options, user) { function removeIntegration(options, user) { incomingLogger.info('Remove integration'); - incomingLogger.debug(options); + incomingLogger.debug({ options }); const integrationToRemove = Promise.await(Integrations.findOneByUrl(options.target_url)); if (!integrationToRemove) { @@ -152,7 +152,7 @@ function removeIntegration(options, user) { } function executeIntegrationRest() { - incomingLogger.info({ msg: 'Post integration:', name: this.integration.name }); + incomingLogger.info({ msg: 'Post integration:', integration: this.integration.name }); incomingLogger.debug({ urlParams: this.urlParams, bodyParams: this.bodyParams }); if (this.integration.enabled !== true) { @@ -230,7 +230,7 @@ function executeIntegrationRest() { if (!result) { incomingLogger.debug({ msg: 'Process Incoming Request result of Trigger has no data', - name: this.integration.name, + integration: this.integration.name, }); return API.v1.success(); } @@ -246,13 +246,13 @@ function executeIntegrationRest() { incomingLogger.debug({ msg: 'Process Incoming Request result of Trigger', - name: this.integration.name, + integration: this.integration.name, result: this.bodyParams, }); } catch (err) { incomingLogger.error({ msg: 'Error running Script in Trigger', - name: this.integration.name, + integration: this.integration.name, script: this.integration.scriptCompiled, err, }); diff --git a/apps/meteor/app/integrations/server/lib/triggerHandler.js b/apps/meteor/app/integrations/server/lib/triggerHandler.js index bc78c8595b83..a44deaa537d3 100644 --- a/apps/meteor/app/integrations/server/lib/triggerHandler.js +++ b/apps/meteor/app/integrations/server/lib/triggerHandler.js @@ -7,9 +7,9 @@ import s from 'underscore.string'; import moment from 'moment'; import Fiber from 'fibers'; import Future from 'fibers/future'; +import { Integrations, IntegrationHistory } from '@rocket.chat/models'; import * as Models from '../../../models/server'; -import { Integrations, IntegrationHistory } from '../../../models/server/raw'; import { settings } from '../../../settings/server'; import { getRoomByNameOrIdWithOptionToJoin, processWebhookMessage } from '../../../lib/server'; import { outgoingLogger } from '../logger'; @@ -274,7 +274,7 @@ export class RocketChatIntegrationHandler { const { store, sandbox } = this.buildSandbox(); try { - outgoingLogger.info({ msg: 'Will evaluate script of Trigger', name: integration.name }); + outgoingLogger.info({ msg: 'Will evaluate script of Trigger', integration: integration.name }); outgoingLogger.debug(script); const vmScript = new VMScript(`${script}; Script;`, 'script.js'); @@ -296,7 +296,7 @@ export class RocketChatIntegrationHandler { } catch (err) { outgoingLogger.error({ msg: 'Error evaluating Script in Trigger', - name: integration.name, + integration: integration.name, script, err, }); @@ -385,12 +385,12 @@ export class RocketChatIntegrationHandler { }); outgoingLogger.error({ msg: 'Error running Script in the Integration', - name: integration.name, + integration: integration.name, err, }); outgoingLogger.debug({ msg: 'Error running Script in the Integration', - name: integration.name, + integration: integration.name, script: integration.scriptCompiled, }); // Only output the compiled script if debugging is enabled, so the logs don't get spammed. } @@ -708,7 +708,7 @@ export class RocketChatIntegrationHandler { this.updateHistory({ historyId, step: 'mapped-args-to-data', data, triggerWord: word }); outgoingLogger.info(`Will be executing the Integration "${trigger.name}" to the url: ${url}`); - outgoingLogger.debug(data); + outgoingLogger.debug({ data }); let opts = { params: {}, diff --git a/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts index 44bc2594baf7..cf78e9ed79de 100644 --- a/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts @@ -182,7 +182,7 @@ export const validateOutgoingIntegration = function ( integrationData.scriptError = undefined; } catch (e) { integrationData.scriptCompiled = undefined; - integrationData.scriptError = _.pick(e, 'name', 'message', 'stack'); + integrationData.scriptError = e instanceof Error ? _.pick(e, 'name', 'message', 'stack') : undefined; } } diff --git a/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts b/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts index 499c8b806ac3..0c9564ea5027 100644 --- a/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts +++ b/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; +import { Integrations, IntegrationHistory } from '@rocket.chat/models'; import { hasPermission } from '../../../authorization/server'; -import { IntegrationHistory, Integrations } from '../../../models/server/raw'; import notifications from '../../../notifications/server/lib/Notifications'; Meteor.methods({ diff --git a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts index 43e063876a2e..1c40d62dcec8 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts @@ -5,10 +5,10 @@ import { Babel } from 'meteor/babel-compiler'; import _ from 'underscore'; import s from 'underscore.string'; import type { INewIncomingIntegration, IIncomingIntegration } from '@rocket.chat/core-typings'; +import { Integrations, Roles } from '@rocket.chat/models'; import { hasPermission, hasAllPermission } from '../../../../authorization/server'; import { Users, Rooms, Subscriptions } from '../../../../models/server'; -import { Integrations, Roles } from '../../../../models/server/raw'; const validChannelChars = ['@', '#']; @@ -93,7 +93,7 @@ Meteor.methods({ integrationData.scriptError = undefined; } catch (e) { integrationData.scriptCompiled = undefined; - integrationData.scriptError = _.pick(e, 'name', 'message', 'stack'); + integrationData.scriptError = e instanceof Error ? _.pick(e, 'name', 'message', 'stack') : undefined; } } diff --git a/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts index 81b1e719e40b..ef90340167be 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; +import { Integrations } from '@rocket.chat/models'; import { hasPermission } from '../../../../authorization/server'; -import { Integrations } from '../../../../models/server/raw'; Meteor.methods({ async deleteIncomingIntegration(integrationId) { diff --git a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.js b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.js index 49ea3faf54ce..f3d161f53976 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.js +++ b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.js @@ -2,9 +2,9 @@ import { Meteor } from 'meteor/meteor'; import { Babel } from 'meteor/babel-compiler'; import _ from 'underscore'; import s from 'underscore.string'; +import { Integrations, Roles } from '@rocket.chat/models'; import { Rooms, Users, Subscriptions } from '../../../../models/server'; -import { Integrations, Roles } from '../../../../models/server/raw'; import { hasAllPermission, hasPermission } from '../../../../authorization/server'; const validChannelChars = ['@', '#']; diff --git a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts index 8514793d9213..ee670807e3aa 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import type { INewOutgoingIntegration, IOutgoingIntegration } from '@rocket.chat/core-typings'; +import { Integrations } from '@rocket.chat/models'; import { hasPermission } from '../../../../authorization/server'; -import { Integrations } from '../../../../models/server/raw'; import { validateOutgoingIntegration } from '../../lib/validateOutgoingIntegration'; Meteor.methods({ diff --git a/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts index 6926e18b1c7c..7209ae909c74 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; +import { Integrations, IntegrationHistory } from '@rocket.chat/models'; import { hasPermission } from '../../../../authorization/server'; -import { IntegrationHistory, Integrations } from '../../../../models/server/raw'; Meteor.methods({ async deleteOutgoingIntegration(integrationId) { diff --git a/apps/meteor/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts index 07e47bbf4ca5..00617c5d392b 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; +import { Integrations, IntegrationHistory } from '@rocket.chat/models'; import { hasPermission } from '../../../../authorization/server'; -import { Integrations, IntegrationHistory } from '../../../../models/server/raw'; import { triggerHandler } from '../../lib/triggerHandler'; Meteor.methods({ diff --git a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js index b357063cac9a..8ddae4b48cab 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js +++ b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; +import { Integrations } from '@rocket.chat/models'; import { hasPermission } from '../../../../authorization/server'; import { Users } from '../../../../models/server'; -import { Integrations } from '../../../../models/server/raw'; import { validateOutgoingIntegration } from '../../lib/validateOutgoingIntegration'; Meteor.methods({ diff --git a/apps/meteor/app/invites/server/functions/findOrCreateInvite.js b/apps/meteor/app/invites/server/functions/findOrCreateInvite.js index 3af45671cf7d..7d17dfae9c24 100644 --- a/apps/meteor/app/invites/server/functions/findOrCreateInvite.js +++ b/apps/meteor/app/invites/server/functions/findOrCreateInvite.js @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; +import { Invites } from '@rocket.chat/models'; import { hasPermission } from '../../../authorization/server'; import { api } from '../../../../server/sdk/api'; import { Subscriptions, Rooms } from '../../../models/server'; -import { Invites } from '../../../models/server/raw'; import { settings } from '../../../settings/server'; import { getURL } from '../../../utils/lib/getURL'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; diff --git a/apps/meteor/app/invites/server/functions/listInvites.js b/apps/meteor/app/invites/server/functions/listInvites.js index 10d67435237d..9ee3fb979204 100644 --- a/apps/meteor/app/invites/server/functions/listInvites.js +++ b/apps/meteor/app/invites/server/functions/listInvites.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; +import { Invites } from '@rocket.chat/models'; import { hasPermission } from '../../../authorization/server'; -import { Invites } from '../../../models/server/raw'; export const listInvites = async (userId) => { if (!userId) { diff --git a/apps/meteor/app/invites/server/functions/removeInvite.js b/apps/meteor/app/invites/server/functions/removeInvite.js index 3e51ded42e4b..d79f0c644bfa 100644 --- a/apps/meteor/app/invites/server/functions/removeInvite.js +++ b/apps/meteor/app/invites/server/functions/removeInvite.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; +import { Invites } from '@rocket.chat/models'; import { hasPermission } from '../../../authorization'; -import { Invites } from '../../../models/server/raw'; export const removeInvite = async (userId, invite) => { if (!userId || !invite) { diff --git a/apps/meteor/app/invites/server/functions/useInviteToken.js b/apps/meteor/app/invites/server/functions/useInviteToken.js index ad020b9db251..6514ebe4e92f 100644 --- a/apps/meteor/app/invites/server/functions/useInviteToken.js +++ b/apps/meteor/app/invites/server/functions/useInviteToken.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; +import { Invites } from '@rocket.chat/models'; import { Users, Subscriptions } from '../../../models/server'; -import { Invites } from '../../../models/server/raw'; import { validateInviteToken } from './validateInviteToken'; import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; diff --git a/apps/meteor/app/invites/server/functions/validateInviteToken.js b/apps/meteor/app/invites/server/functions/validateInviteToken.js index 385d55ca0ee1..623761324b30 100644 --- a/apps/meteor/app/invites/server/functions/validateInviteToken.js +++ b/apps/meteor/app/invites/server/functions/validateInviteToken.js @@ -1,7 +1,5 @@ import { Meteor } from 'meteor/meteor'; - -import { Rooms } from '../../../models'; -import { Invites } from '../../../models/server/raw'; +import { Invites, Rooms } from '@rocket.chat/models'; export const validateInviteToken = async (token) => { if (!token || typeof token !== 'string') { @@ -19,7 +17,7 @@ export const validateInviteToken = async (token) => { }); } - const room = Rooms.findOneById(inviteData.rid); + const room = await Rooms.findOneById(inviteData.rid); if (!room) { throw new Meteor.Error('error-invalid-room', 'The invite token is invalid.', { method: 'validateInviteToken', diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateRoom.js b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateRoom.js index 42e01bd78b9f..07caf87bd140 100644 --- a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateRoom.js +++ b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateRoom.js @@ -1,4 +1,4 @@ -import { Users } from '../../../../models'; +import { Users } from '../../../../models/server'; export default function handleOnCreateRoom(user, room) { const users = Users.findByRoomId(room._id); diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateUser.js b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateUser.js index 851c16c628ad..5e1e9442b0e5 100644 --- a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateUser.js +++ b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateUser.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { Users, Rooms } from '../../../../models'; +import { Users, Rooms } from '../../../../models/server'; export default function handleOnCreateUser(newUser) { if (!newUser) { diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogin.js b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogin.js index a10ae0950f04..7d8d350dd99d 100644 --- a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogin.js +++ b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogin.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { Users, Rooms } from '../../../../models'; +import { Users, Rooms } from '../../../../models/server'; export default function handleOnLogin(login) { if (login.user === null) { diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onSaveMessage.js b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onSaveMessage.js index e720091d630f..c9a1499a02d0 100644 --- a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onSaveMessage.js +++ b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onSaveMessage.js @@ -1,5 +1,5 @@ import { SystemLogger } from '../../../../../server/lib/logger/system'; -import { Subscriptions, Users } from '../../../../models'; +import { Subscriptions, Users } from '../../../../models/server'; export default function handleOnSaveMessage(message, to) { let toIdentification = ''; diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.js index e5ed2214f05d..fc432446b5fb 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { Users } from '../../../../models'; +import { Users } from '../../../../models/server'; export default function handleQUIT(args) { const user = Users.findOne({ diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js index 6555807b5a0b..b17009e54eb5 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js @@ -1,4 +1,4 @@ -import { Users, Rooms } from '../../../../models'; +import { Users, Rooms } from '../../../../models/server'; import { createRoom, addUserToRoom } from '../../../../lib'; export default function handleJoinedChannel(args) { diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/leftChannel.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/leftChannel.js index 13c29e828c46..e6b8f46f6301 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/leftChannel.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/leftChannel.js @@ -1,4 +1,4 @@ -import { Users, Rooms } from '../../../../models'; +import { Users, Rooms } from '../../../../models/server'; import { removeUserFromRoom } from '../../../../lib'; export default function handleLeftChannel(args) { diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.js index 9ebc16057f3c..de2956e3302d 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.js @@ -1,4 +1,4 @@ -import { Users } from '../../../../models'; +import { Users } from '../../../../models/server'; export default function handleNickChanged(args) { const user = Users.findOne({ diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/sentMessage.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/sentMessage.js index d4f4bb8ae5d6..cfad046fd725 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/sentMessage.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/sentMessage.js @@ -1,4 +1,4 @@ -import { Users, Rooms } from '../../../../models'; +import { Users, Rooms } from '../../../../models/server'; import { sendMessage, createDirectRoom } from '../../../../lib'; /* * diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.js index 2f42f790e8f5..29c0dad9019d 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { Users } from '../../../../models'; +import { Users } from '../../../../models/server'; export default async function handleUserRegistered(args) { // Check if there is an user with the given username diff --git a/apps/meteor/app/irc/server/irc.js b/apps/meteor/app/irc/server/irc.js index dd6c4449ab01..de3638169607 100644 --- a/apps/meteor/app/irc/server/irc.js +++ b/apps/meteor/app/irc/server/irc.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import Bridge from './irc-bridge'; -import { settings } from '../../settings'; +import { settings } from '../../settings/server'; if (!!settings.get('IRC_Enabled') === true) { // Normalize the config values diff --git a/apps/meteor/app/irc/server/methods/resetIrcConnection.js b/apps/meteor/app/irc/server/methods/resetIrcConnection.js index cef9299c84d8..9c033c27017f 100644 --- a/apps/meteor/app/irc/server/methods/resetIrcConnection.js +++ b/apps/meteor/app/irc/server/methods/resetIrcConnection.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Settings } from '../../../models/server'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import Bridge from '../irc-bridge'; Meteor.methods({ diff --git a/apps/meteor/app/katex/client/index.js b/apps/meteor/app/katex/client/index.js deleted file mode 100644 index 139b420af26b..000000000000 --- a/apps/meteor/app/katex/client/index.js +++ /dev/null @@ -1,191 +0,0 @@ -import { Random } from 'meteor/random'; -import katex from 'katex'; -import { unescapeHTML, escapeHTML } from '@rocket.chat/string-helpers'; - -import 'katex/dist/katex.min.css'; -import './style.css'; - -class Boundary { - length() { - return this.end - this.start; - } - - extract(str) { - return str.substr(this.start, this.length()); - } -} - -class Katex { - constructor(katex, { dollarSyntax, parenthesisSyntax }) { - this.katex = katex; - this.delimitersMap = [ - { - opener: '\\[', - closer: '\\]', - displayMode: true, - enabled: () => parenthesisSyntax, - }, - { - opener: '\\(', - closer: '\\)', - displayMode: false, - enabled: () => parenthesisSyntax, - }, - { - opener: '$$', - closer: '$$', - displayMode: true, - enabled: () => dollarSyntax, - }, - { - opener: '$', - closer: '$', - displayMode: false, - enabled: () => dollarSyntax, - }, - ]; - } - - findOpeningDelimiter(str, start) { - const matches = this.delimitersMap - .filter((options) => options.enabled()) - .map((options) => ({ - options, - pos: str.indexOf(options.opener, start), - })); - - const positions = matches.filter(({ pos }) => pos >= 0).map(({ pos }) => pos); - - // No opening delimiters were found - if (positions.length === 0) { - return null; - } - - // Take the first delimiter found - const minPos = Math.min(...positions); - - const matchIndex = matches.findIndex(({ pos }) => pos === minPos); - - const match = matches[matchIndex]; - return match; - } - - getLatexBoundaries(str, { options: { closer }, pos }) { - const closerIndex = str.substr(pos + closer.length).indexOf(closer); - if (closerIndex < 0) { - return null; - } - - const inner = new Boundary(); - const outer = new Boundary(); - - inner.start = pos + closer.length; - inner.end = inner.start + closerIndex; - - outer.start = pos; - outer.end = inner.end + closer.length; - - return { - outer, - inner, - }; - } - - // Searches for the first latex block in the given string - findLatex(str) { - 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; - } - } - - return null; - } - - // Breaks a message to what comes before, after and to the content of a - // matched latex block - extractLatex(str, match) { - const before = str.substr(0, match.outer.start); - const after = str.substr(match.outer.end); - let latex = match.inner.extract(str); - latex = unescapeHTML(latex); - return { - before, - latex, - after, - }; - } - - // Takes a latex math string and the desired display mode and renders it - // to HTML using the KaTeX library - renderLatex = (latex, displayMode) => { - try { - return this.katex.renderToString(latex, { - displayMode, - macros: { - '\\href': '\\@secondoftwo', - }, - }); - } catch ({ message }) { - return `

${escapeHTML(message)}
`; - } - }; - - // Takes a string and renders all latex blocks inside it - render(str, renderFunction) { - let result = ''; - while (this.findLatex(str) != null) { - // Find the first latex block in the string - const match = this.findLatex(str); - const parts = this.extractLatex(str, match); - - // Add to the reuslt what comes before the latex block as well as - // the rendered latex content - const rendered = renderFunction(parts.latex, match.options.displayMode); - result += parts.before + rendered; - // Set what comes after the latex block to be examined next - str = parts.after; - } - result += str; - return result; - } - - renderMessage = (message) => { - if (typeof message === 'string') { - return this.render(message, this.renderLatex); - } - - if (!message.html?.trim()) { - return message; - } - - if (!message.tokens) { - message.tokens = []; - } - - message.html = this.render(message.html, (latex, displayMode) => { - const token = `=!=${Random.id()}=!=`; - message.tokens.push({ - token, - text: this.renderLatex(latex, displayMode), - }); - return token; - }); - - return message; - }; -} - -export const createKatexMessageRendering = (options) => { - const instance = new Katex(katex, options); - return (message) => instance.renderMessage(message); -}; - -export const getKatexHtml = (text, katex) => { - return createKatexMessageRendering({ dollarSyntax: katex.dollarSyntaxEnabled, parenthesisSyntax: katex.parenthesisSyntaxEnabled })(text); -}; diff --git a/apps/meteor/app/katex/client/index.ts b/apps/meteor/app/katex/client/index.ts new file mode 100644 index 000000000000..7baf7098d0c3 --- /dev/null +++ b/apps/meteor/app/katex/client/index.ts @@ -0,0 +1,248 @@ +import { Random } from 'meteor/random'; +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 { + start: number; + + end: number; + + length(): number { + return this.end - this.start; + } + + 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 { + katex: KatexPackage; + + delimitersMap: Delimiter[]; + + constructor(katex: KatexPackage, { dollarSyntax, parenthesisSyntax }: { dollarSyntax: boolean; parenthesisSyntax: boolean }) { + this.katex = katex; + this.delimitersMap = [ + { + opener: '\\[', + closer: '\\]', + displayMode: true, + enabled: (): boolean => parenthesisSyntax, + }, + { + opener: '\\(', + closer: '\\)', + displayMode: false, + enabled: (): boolean => parenthesisSyntax, + }, + { + opener: '$$', + closer: '$$', + displayMode: true, + enabled: (): boolean => dollarSyntax, + }, + { + opener: '$', + closer: '$', + displayMode: false, + enabled: (): boolean => dollarSyntax, + }, + ]; + } + + findOpeningDelimiter(str: string, start: number): OpeningDelimiter | null { + const matches = this.delimitersMap + .filter((options) => options.enabled()) + .map((options) => ({ + options, + pos: str.indexOf(options.opener, start), + })); + + const positions = matches.filter(({ pos }) => pos >= 0).map(({ pos }) => pos); + + // No opening delimiters were found + if (positions.length === 0) { + return null; + } + + // Take the first delimiter found + const minPos = Math.min(...positions); + + const matchIndex = matches.findIndex(({ pos }) => pos === minPos); + + const match = matches[matchIndex]; + return match; + } + + getLatexBoundaries(str: string, { options: { closer }, pos }: OpeningDelimiter): LatexBoundary | null { + const closerIndex = str.substr(pos + closer.length).indexOf(closer); + if (closerIndex < 0) { + return null; + } + + const inner = new Boundary(); + const outer = new Boundary(); + + inner.start = pos + closer.length; + inner.end = inner.start + closerIndex; + + outer.start = pos; + outer.end = inner.end + closer.length; + + return { + outer, + inner, + }; + } + + // Searches for the first latex block in the given string + 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?.inner.extract(str).trim().length) { + return { + ...match, + options: openingDelimiterMatch.options, + }; + } + } + + return null; + } + + // Breaks a message to what comes before, after and to the content of a + // matched latex block + 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); + latex = unescapeHTML(latex); + return { + before, + latex, + after, + }; + } + + // Takes a latex math string and the desired display mode and renders it + // to HTML using the KaTeX library + renderLatex = (latex: string, displayMode: Delimiter['displayMode']): string => { + try { + return KatexPackage.renderToString(latex, { + displayMode, + macros: { + '\\href': '\\@secondoftwo', + }, + }); + } catch (e) { + return `
${escapeHTML( + e instanceof Error ? e.message : String(e), + )}
`; + } + }; + + // Takes a string and renders all latex blocks inside it + 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 + // the rendered latex content + const rendered = renderFunction(parts.latex, match.options.displayMode); + result += parts.before + rendered; + // Set what comes after the latex block to be examined next + str = parts.after; + } + result += str; + return result; + } + + 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); + } + + if (!message.html?.trim()) { + return message; + } + + if (!message.tokens) { + message.tokens = []; + } + + message.html = this.render(message.html, (latex, displayMode) => { + const token = `=!=${Random.id()}=!=`; + message.tokens?.push({ + token, + text: this.renderLatex(latex, displayMode), + }); + return token; + }); + + return 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: 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..dad647d5be03 100644 --- a/apps/meteor/app/lib/client/methods/sendMessage.js +++ b/apps/meteor/app/lib/client/methods/sendMessage.js @@ -1,8 +1,7 @@ import { Meteor } from 'meteor/meteor'; -import { TimeSync } from 'meteor/mizzao:timesync'; import s from 'underscore.string'; -import { ChatMessage, Rooms } from '../../../models'; +import { ChatMessage, Rooms } from '../../../models/client'; import { settings } from '../../../settings'; import { callbacks } from '../../../../lib/callbacks'; import { t } from '../../../utils/client'; @@ -19,7 +18,7 @@ Meteor.methods({ return dispatchToastMessage({ type: 'error', message: t('Message_Already_Sent') }); } const user = Meteor.user(); - message.ts = isNaN(TimeSync.serverOffset()) ? new Date() : new Date(Date.now() + TimeSync.serverOffset()); + message.ts = new Date(); message.u = { _id: Meteor.userId(), username: user.username, @@ -32,9 +31,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/functions/addUserToRoom.ts b/apps/meteor/app/lib/server/functions/addUserToRoom.ts index 0bdaa33c8193..c219f32ad9a9 100644 --- a/apps/meteor/app/lib/server/functions/addUserToRoom.ts +++ b/apps/meteor/app/lib/server/functions/addUserToRoom.ts @@ -4,14 +4,14 @@ import type { IUser, IRoom } from '@rocket.chat/core-typings'; import { AppEvents, Apps } from '../../../apps/server'; import { callbacks } from '../../../../lib/callbacks'; -import { Messages, Rooms, Subscriptions } from '../../../models/server'; +import { Messages, Rooms, Subscriptions, Users } from '../../../models/server'; import { Team } from '../../../../server/sdk'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; export const addUserToRoom = function ( rid: string, - user: Pick, + user: Pick | string, inviter?: Pick, silenced?: boolean, ): boolean | unknown { @@ -26,14 +26,22 @@ export const addUserToRoom = function ( return; } + try { + callbacks.run('federation.beforeAddUserAToRoom', { user, inviter }, room); + } catch (error) { + throw new Meteor.Error((error as any)?.message); + } + + const userToBeAdded = typeof user !== 'string' ? user : Users.findOneByUsername(user.replace('@', '')); + // Check if user is already in room - const subscription = Subscriptions.findOneByRoomIdAndUserId(rid, user._id); + const subscription = Subscriptions.findOneByRoomIdAndUserId(rid, userToBeAdded._id); if (subscription) { return; } try { - Promise.await(Apps.triggerEvent(AppEvents.IPreRoomUserJoined, room, user, inviter)); + Promise.await(Apps.triggerEvent(AppEvents.IPreRoomUserJoined, room, userToBeAdded, inviter)); } catch (error) { if (error instanceof AppsEngineException) { throw new Meteor.Error('error-app-prevented', error.message); @@ -44,14 +52,14 @@ export const addUserToRoom = function ( if (room.t === 'c' || room.t === 'p' || room.t === 'l') { // Add a new event, with an optional inviter - callbacks.run('beforeAddedToRoom', { user, inviter }, room); + callbacks.run('beforeAddedToRoom', { user: userToBeAdded, inviter }, room); // Keep the current event - callbacks.run('beforeJoinRoom', user, room); + callbacks.run('beforeJoinRoom', userToBeAdded, room); } Promise.await( - Apps.triggerEvent(AppEvents.IPreRoomUserJoined, room, user, inviter).catch((error) => { + Apps.triggerEvent(AppEvents.IPreRoomUserJoined, room, userToBeAdded, inviter).catch((error) => { if (error instanceof AppsEngineException) { throw new Meteor.Error('error-app-prevented', error.message); } @@ -60,7 +68,7 @@ export const addUserToRoom = function ( }), ); - Subscriptions.createWithRoomAndUser(room, user, { + Subscriptions.createWithRoomAndUser(room, userToBeAdded, { ts: now, open: true, alert: true, @@ -79,34 +87,34 @@ export const addUserToRoom = function ( }, }; if (room.teamMain) { - Messages.createUserAddedToTeamWithRoomIdAndUser(rid, user, extraData); + Messages.createUserAddedToTeamWithRoomIdAndUser(rid, userToBeAdded, extraData); } else { - Messages.createUserAddedWithRoomIdAndUser(rid, user, extraData); + Messages.createUserAddedWithRoomIdAndUser(rid, userToBeAdded, extraData); } } else if (room.prid) { - Messages.createUserJoinWithRoomIdAndUserDiscussion(rid, user, { ts: now }); + Messages.createUserJoinWithRoomIdAndUserDiscussion(rid, userToBeAdded, { ts: now }); } else if (room.teamMain) { - Messages.createUserJoinTeamWithRoomIdAndUser(rid, user, { ts: now }); + Messages.createUserJoinTeamWithRoomIdAndUser(rid, userToBeAdded, { ts: now }); } else { - Messages.createUserJoinWithRoomIdAndUser(rid, user, { ts: now }); + Messages.createUserJoinWithRoomIdAndUser(rid, userToBeAdded, { ts: now }); } } if (room.t === 'c' || room.t === 'p') { Meteor.defer(function () { // Add a new event, with an optional inviter - callbacks.run('afterAddedToRoom', { user, inviter }, room); + callbacks.run('afterAddedToRoom', { user: userToBeAdded, inviter }, room); // Keep the current event - callbacks.run('afterJoinRoom', user, room); + callbacks.run('afterJoinRoom', userToBeAdded, room); - Apps.triggerEvent(AppEvents.IPostRoomUserJoined, room, user, inviter); + Apps.triggerEvent(AppEvents.IPostRoomUserJoined, room, userToBeAdded, inviter); }); } if (room.teamMain && room.teamId && inviter) { // if user is joining to main team channel, create a membership - Promise.await(Team.addMember(inviter, user._id, room.teamId)); + Promise.await(Team.addMember(inviter, userToBeAdded._id, room.teamId)); } return true; diff --git a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts index 6c61539a1ac5..90df732bf4cf 100644 --- a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts +++ b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts @@ -1,4 +1,4 @@ -import { Cursor } from 'mongodb'; +import { FindCursor } from 'mongodb'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import type { IMessage, IMessageDiscussion } from '@rocket.chat/core-typings'; @@ -48,7 +48,7 @@ export const cleanRoomHistory = function ({ Messages.findDiscussionByRoomIdPinnedTimestampAndUsers(rid, excludePinned, ts, fromUsers, { fields: { drid: 1 }, ...(limit && { limit }), - }) as Cursor + }) as FindCursor ).forEach(({ drid }) => deleteRoom(drid)), ); } diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index b5ad346eb9b6..4e01a3660270 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -1,14 +1,14 @@ import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; -import type { IUser } from '@rocket.chat/core-typings'; +import type { ICreatedRoom, IUser } from '@rocket.chat/core-typings'; +import { Subscriptions } from '@rocket.chat/models'; +import { Users, Rooms } from '../../../models/server'; import { Apps } from '../../../apps/server'; import { callbacks } from '../../../../lib/callbacks'; -import { Rooms } from '../../../models/server'; import { settings } from '../../../settings/server'; import { getDefaultSubscriptionPref } from '../../../utils/server'; -import { Users, Subscriptions } from '../../../models/server/raw'; import { ICreateRoomParams } from '../../../../server/sdk/types/IRoomService'; const generateSubscription = (fname: string, name: string, user: IUser, extra: {}): any => ({ @@ -32,16 +32,31 @@ const generateSubscription = (fname: string, name: string, user: IUser, extra: { const getFname = (members: IUser[]): string => members.map(({ name, username }) => name || username).join(', '); const getName = (members: IUser[]): string => members.map(({ username }) => username).join(', '); -export const createDirectRoom = function (members: IUser[], roomExtraData = {}, options: ICreateRoomParams['options']): unknown { +export const createDirectRoom = function ( + members: IUser[] | string[], + roomExtraData = {}, + options: ICreateRoomParams['options'], +): ICreatedRoom { if (members.length > (settings.get('DirectMesssage_maxUsers') || 1)) { throw new Error('error-direct-message-max-user-exceeded'); } + callbacks.run('beforeCreateDirectRoom', members); + const membersUsernames = members.map((member) => { + if (typeof member === 'string') { + return member.replace('@', ''); + } + return member.username; + }); + + const roomMembers: IUser[] = Users.findUsersByUsernames(membersUsernames, { + fields: { _id: 1, name: 1, username: 1, settings: 1, customFields: 1 }, + }).fetch(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const sortedMembers = members.sort((u1, u2) => (u1.name! || u1.username!).localeCompare(u2.name! || u2.username!)); + const sortedMembers = roomMembers.sort((u1, u2) => (u1.name! || u1.username!).localeCompare(u2.name! || u2.username!)); - const usernames = sortedMembers.map(({ username }) => username); - const uids = members.map(({ _id }) => _id).sort(); + const usernames: string[] = sortedMembers.map(({ username }) => username as string).filter(Boolean); + const uids = roomMembers.map(({ _id }) => _id).sort(); // Deprecated: using users' _id to compose the room _id is deprecated const room = @@ -63,7 +78,7 @@ export const createDirectRoom = function (members: IUser[], roomExtraData = {}, }; if (isNewRoom) { - const tmpRoom = { + const tmpRoom: { _USERNAMES?: (string | undefined)[] } & typeof roomInfo = { ...roomInfo, _USERNAMES: usernames, }; @@ -88,26 +103,31 @@ export const createDirectRoom = function (members: IUser[], roomExtraData = {}, if (typeof result === 'object') { Object.assign(roomInfo, result); } + + delete tmpRoom._USERNAMES; } const rid = room?._id || Rooms.insert(roomInfo); - if (members.length === 1) { + if (roomMembers.length === 1) { // dm to yourself Subscriptions.updateOne( - { rid, 'u._id': members[0]._id }, + { rid, 'u._id': roomMembers[0]._id }, { $set: { open: true }, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - $setOnInsert: generateSubscription(members[0].name! || members[0].username!, members[0].username!, members[0], { + $setOnInsert: generateSubscription(roomMembers[0].name! || roomMembers[0].username!, roomMembers[0].username!, roomMembers[0], { ...options?.subscriptionExtra, }), }, { upsert: true }, ); } else { - const memberIds = members.map((member) => member._id); - const membersWithPreferences = Users.find({ _id: { $in: memberIds } }, { projection: { 'username': 1, 'settings.preferences': 1 } }); + const memberIds = roomMembers.map((member) => member._id); + const membersWithPreferences: IUser[] = Users.find( + { _id: { $in: memberIds } }, + { projection: { 'username': 1, 'settings.preferences': 1 } }, + ); membersWithPreferences.forEach((member) => { const otherMembers = sortedMembers.filter(({ _id }) => _id !== member._id); @@ -129,15 +149,16 @@ export const createDirectRoom = function (members: IUser[], roomExtraData = {}, if (isNewRoom) { const insertedRoom = Rooms.findOneById(rid); - callbacks.run('afterCreateDirectRoom', insertedRoom, { members }); + callbacks.run('afterCreateDirectRoom', insertedRoom, { members: roomMembers, creatorId: options?.creator }); Apps.triggerEvent('IPostRoomCreate', insertedRoom); } return { - _id: rid, + _id: String(rid), usernames, t: 'd', inserted: isNewRoom, + ...room, }; }; diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 903d942dba19..28fcf4b13c04 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -2,7 +2,7 @@ import { AppsEngineException } from '@rocket.chat/apps-engine/definition/excepti import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import s from 'underscore.string'; -import type { IUser } from '@rocket.chat/core-typings'; +import type { ICreatedRoom, IUser } from '@rocket.chat/core-typings'; import { IRoom, RoomType } from '@rocket.chat/core-typings'; import { Apps } from '../../../apps/server'; @@ -26,12 +26,12 @@ export const createRoom = function ( readOnly?: boolean, roomExtraData?: Partial, options?: ICreateRoomParams['options'], -): unknown { +): ICreatedRoom { const { teamId, ...extraData } = roomExtraData || ({} as IRoom); callbacks.run('beforeCreateRoom', { type, name, owner: ownerUsername, members, readOnly, extraData, options }); if (type === 'd') { - return createDirectRoom(members as IUser[], extraData, options); + return createDirectRoom(members as IUser[], extraData, { ...options, creator: options?.creator || ownerUsername }); } if (!isValidName(name)) { @@ -59,7 +59,7 @@ export const createRoom = function ( const now = new Date(); - const roomProps: Omit = { + const roomProps: Omit = { fname: name, ...extraData, name: getValidRoomName(name.trim(), undefined, { @@ -114,28 +114,46 @@ export const createRoom = function ( callbacks.run('beforeCreateChannel', owner, roomProps); } const room = Rooms.createWithFullRoomData(roomProps); - - for (const username of [...new Set(members as string[])]) { - const member = Users.findOneByUsername(username, { - fields: { 'username': 1, 'settings.preferences': 1 }, - }); - if (!member) { - continue; - } - + const shouldBeHandledByFederation = room.federated === true || ownerUsername.includes(':'); + if (shouldBeHandledByFederation) { const extra: Partial = options?.subscriptionExtra || {}; - extra.open = true; + extra.ls = now; if (room.prid) { extra.prid = room.prid; } - if (username === owner.username) { - extra.ls = now; - } + Subscriptions.createWithRoomAndUser(room, owner, extra); + } else { + for (const username of [...new Set(members as string[])]) { + const member = Users.findOneByUsername(username, { + fields: { 'username': 1, 'settings.preferences': 1, 'federated': 1 }, + }); + if (!member) { + continue; + } + + try { + callbacks.run('federation.beforeAddUserAToRoom', { user: member, inviter: owner }, room); + } catch (error) { + throw new Meteor.Error((error as any)?.message); + } - Subscriptions.createWithRoomAndUser(room, member, extra); + const extra: Partial = options?.subscriptionExtra || {}; + + extra.open = true; + + if (room.prid) { + extra.prid = room.prid; + } + + if (username === owner.username) { + extra.ls = now; + } + + Subscriptions.createWithRoomAndUser(room, member, extra); + } } addUserRoles(owner._id, ['owner'], room._id); @@ -150,6 +168,9 @@ export const createRoom = function ( callbacks.runAsync('afterCreatePrivateGroup', owner, room); } callbacks.runAsync('afterCreateRoom', owner, room); + if (shouldBeHandledByFederation) { + callbacks.run('federation.afterCreateFederatedRoom', room, { owner, originalMemberList: members as string[] }); + } Apps.triggerEvent('IPostRoomCreate', room); diff --git a/apps/meteor/app/lib/server/functions/deleteMessage.ts b/apps/meteor/app/lib/server/functions/deleteMessage.ts index dfb7f473a0ae..b80a513c59f6 100644 --- a/apps/meteor/app/lib/server/functions/deleteMessage.ts +++ b/apps/meteor/app/lib/server/functions/deleteMessage.ts @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; import type { IMessage, IUser } from '@rocket.chat/core-typings'; +import { Uploads } from '@rocket.chat/models'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; import { Messages, Rooms } from '../../../models/server'; -import { Uploads } from '../../../models/server/raw'; import { api } from '../../../../server/sdk/api'; import { callbacks } from '../../../../lib/callbacks'; import { Apps } from '../../../apps/server'; @@ -37,7 +37,7 @@ export const deleteMessage = async function (message: IMessage, user: IUser): Pr } for await (const file of files) { - file?._id && (await Uploads.update({ _id: file._id }, { $set: { _hidden: true } })); + file?._id && (await Uploads.updateOne({ _id: file._id }, { $set: { _hidden: true } })); } } else { if (!showDeletedStatus) { diff --git a/apps/meteor/app/lib/server/functions/deleteRoom.ts b/apps/meteor/app/lib/server/functions/deleteRoom.ts index e74a54ebb9c7..46f247a4e797 100644 --- a/apps/meteor/app/lib/server/functions/deleteRoom.ts +++ b/apps/meteor/app/lib/server/functions/deleteRoom.ts @@ -1,15 +1,13 @@ -import { DeleteWriteOpResultObject } from 'mongodb'; - import { Messages, Subscriptions, Rooms } from '../../../models/server'; import { callbacks } from '../../../../lib/callbacks'; import { FileUpload } from '../../../file-upload/server'; -export const deleteRoom = function (rid: string): Promise { +export const deleteRoom = function (rid: string): void { FileUpload.removeFilesByRoomId(rid); Messages.removeByRoomId(rid); callbacks.run('beforeDeleteRoom', rid); Subscriptions.removeByRoomId(rid); FileUpload.getStore('Avatars').deleteByRoomId(rid); callbacks.run('afterDeleteRoom', rid); - return Rooms.removeById(rid); + Rooms.removeById(rid); }; diff --git a/apps/meteor/app/lib/server/functions/deleteUser.ts b/apps/meteor/app/lib/server/functions/deleteUser.ts index f9901701628a..568cfd370924 100644 --- a/apps/meteor/app/lib/server/functions/deleteUser.ts +++ b/apps/meteor/app/lib/server/functions/deleteUser.ts @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { FileProp } from '@rocket.chat/core-typings'; +import { Integrations, FederationServers, LivechatVisitors } from '@rocket.chat/models'; import { FileUpload } from '../../../file-upload/server'; -import { Users, Subscriptions, Messages, Rooms, LivechatDepartmentAgents, LivechatVisitors } from '../../../models/server'; -import { FederationServers, Integrations } from '../../../models/server/raw'; +import { Users, Subscriptions, Messages, Rooms, LivechatDepartmentAgents } from '../../../models/server'; import { settings } from '../../../settings/server'; import { updateGroupDMsName } from './updateGroupDMsName'; import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; @@ -65,17 +65,17 @@ export async function deleteUser(userId: string, confirmRelinquish = false): Pro if (user.roles.includes('livechat-agent')) { // Remove user as livechat agent LivechatDepartmentAgents.removeByAgentId(userId); - LivechatVisitors.removeContactManagerByUsername(user.username); + await LivechatVisitors.removeContactManagerByUsername(user.username); } if (user.roles.includes('livechat-monitor')) { // Remove user as Unit Monitor LivechatUnitMonitors.removeByMonitorId(userId); - LivechatVisitors.removeContactManagerByUsername(user.username); + await LivechatVisitors.removeContactManagerByUsername(user.username); } // removes user's avatar - if (user.avatarOrigin === 'upload' || user.avatarOrigin === 'url') { + if (user.avatarOrigin === 'upload' || user.avatarOrigin === 'url' || user.avatarOrigin === 'rest') { FileUpload.getStore('Avatars').deleteByName(user.username); } diff --git a/apps/meteor/app/lib/server/functions/getAvatarSuggestionForUser.js b/apps/meteor/app/lib/server/functions/getAvatarSuggestionForUser.js index 57d788d70649..e7bf5755f362 100644 --- a/apps/meteor/app/lib/server/functions/getAvatarSuggestionForUser.js +++ b/apps/meteor/app/lib/server/functions/getAvatarSuggestionForUser.js @@ -74,15 +74,6 @@ const avatarProviders = { } }, - blockstack(user) { - if (user.services && user.services.blockstack && user.services.blockstack.image && settings.get('Blockstack_Enable')) { - return { - service: 'blockstack', - url: user.services.blockstack.image, - }; - } - }, - customOAuth(user) { const avatars = []; for (const service in user.services) { diff --git a/apps/meteor/app/lib/server/functions/getFullUserData.ts b/apps/meteor/app/lib/server/functions/getFullUserData.ts index 6725bedc1da8..c4db7e3f20cf 100644 --- a/apps/meteor/app/lib/server/functions/getFullUserData.ts +++ b/apps/meteor/app/lib/server/functions/getFullUserData.ts @@ -20,7 +20,7 @@ const defaultFields = { statusText: 1, avatarETag: 1, extension: 1, -}; +} as const; const fullFields = { emails: 1, @@ -32,12 +32,12 @@ const fullFields = { requirePasswordChange: 1, requirePasswordChangeReason: 1, roles: 1, -}; +} as const; -let publicCustomFields: Record = {}; -let customFields: Record = {}; +let publicCustomFields: Record = {}; +let customFields: Record = {}; -settings.watch('Accounts_CustomFields', (settingValue: string) => { +settings.watch('Accounts_CustomFields', (settingValue) => { publicCustomFields = {}; customFields = {}; @@ -60,28 +60,33 @@ settings.watch('Accounts_CustomFields', (settingValue: string) => { } }); -export function getFullUserDataByIdOrUsername({ - userId, - filterId, - filterUsername, -}: { - userId: string; - filterId?: string; - filterUsername?: string; -}): IUser | null { +const getCustomFields = (canViewAllInfo: boolean): Record => (canViewAllInfo ? customFields : publicCustomFields); + +const getFields = (canViewAllInfo: boolean): Record => ({ + ...defaultFields, + ...(canViewAllInfo && fullFields), + ...getCustomFields(canViewAllInfo), +}); + +export async function getFullUserDataByIdOrUsername( + userId: string, + { filterId, filterUsername }: { filterId: string; filterUsername?: undefined } | { filterId?: undefined; filterUsername: string }, +): Promise { const caller = Users.findOneById(userId, { fields: { username: 1 } }); const targetUser = filterId || filterUsername; const myself = (filterId && targetUser === userId) || (filterUsername && targetUser === caller.username); const canViewAllInfo = !!myself || hasPermission(userId, 'view-full-other-user-info'); - const fields = { - ...defaultFields, - ...(canViewAllInfo && fullFields), - ...(canViewAllInfo ? customFields : publicCustomFields), - ...(myself && { services: 1 }), + const fields = getFields(canViewAllInfo); + + const options = { + fields: { + ...fields, + ...(myself && { services: 1 }), + }, }; - const user = Users.findOneByIdOrUsername(targetUser, { fields }); + const user = Users.findOneByIdOrUsername(targetUser, options); if (!user) { return null; } diff --git a/apps/meteor/app/lib/server/functions/getRoomsWithSingleOwner.ts b/apps/meteor/app/lib/server/functions/getRoomsWithSingleOwner.ts index ad74510c470c..b525ac05e695 100644 --- a/apps/meteor/app/lib/server/functions/getRoomsWithSingleOwner.ts +++ b/apps/meteor/app/lib/server/functions/getRoomsWithSingleOwner.ts @@ -1,4 +1,4 @@ -import { Cursor } from 'mongodb'; +import { FindCursor } from 'mongodb'; import type { IUser, ISubscription } from '@rocket.chat/core-typings'; import { subscriptionHasRole } from '../../../authorization/server'; @@ -24,7 +24,8 @@ export function getSubscribedRoomsForUserWithDetails( ): SubscribedRoomsForUserWithDetails[] { const subscribedRooms: SubscribedRoomsForUserWithDetails[] = []; - const cursor: Cursor = + // TODO this is not really a FindCursor since it is using Meteor Models -> migrate to raw models + const cursor: FindCursor = roomIds.length > 0 ? Subscriptions.findByUserIdAndRoomIds(userId, roomIds) : Subscriptions.findByUserIdExceptType(userId, 'd'); // Iterate through all the rooms the user is subscribed to, to check if he is the last owner of any of them. @@ -46,7 +47,7 @@ export function getSubscribedRoomsForUserWithDetails( if (numOwners === 1 && assignNewOwner) { // Let's check how many subscribers the room has. const options = { projection: { 'u._id': 1 }, sort: { ts: 1 } }; - const subscribersCursor: Cursor = Subscriptions.findByRoomId(subscription.rid, options); + const subscribersCursor: FindCursor = Subscriptions.findByRoomId(subscription.rid, options); subscribersCursor.forEach(({ u: { _id: uid } }) => { // If we already changed the owner or this subscription is for the user we are removing, then don't try to give it ownership diff --git a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts index 6698b4da5221..ddfac9ba884f 100644 --- a/apps/meteor/app/lib/server/functions/loadMessageHistory.ts +++ b/apps/meteor/app/lib/server/functions/loadMessageHistory.ts @@ -4,7 +4,7 @@ import { Messages, Rooms } from '../../../models/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { getHiddenSystemMessages } from '../lib/getHiddenSystemMessages'; -export const loadMessageHistory = function loadMessageHistory({ +export function loadMessageHistory({ userId, rid, end, @@ -48,8 +48,7 @@ export const loadMessageHistory = function loadMessageHistory({ if (ls != null) { const firstMessage = messages[messages.length - 1]; - if ((firstMessage != null ? firstMessage.ts : undefined) > ls) { - // delete options.limit; + if (firstMessage && new Date(firstMessage.ts) > new Date(ls)) { const unreadMessages = Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes( rid, ls, @@ -64,8 +63,17 @@ export const loadMessageHistory = function loadMessageHistory({ showThreadMessages, ); + const totalCursor = Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes( + rid, + ls, + firstMessage.ts, + hiddenMessageTypes, + {}, + showThreadMessages, + ); + firstUnread = unreadMessages.fetch()[0]; - unreadNotLoaded = unreadMessages.count(); + unreadNotLoaded = totalCursor.count(); } } @@ -74,4 +82,4 @@ export const loadMessageHistory = function loadMessageHistory({ firstUnread, unreadNotLoaded, }; -}; +} diff --git a/apps/meteor/app/lib/server/functions/notifications/email.js b/apps/meteor/app/lib/server/functions/notifications/email.js index 8126497b28af..d9f9cf17d581 100644 --- a/apps/meteor/app/lib/server/functions/notifications/email.js +++ b/apps/meteor/app/lib/server/functions/notifications/email.js @@ -164,7 +164,7 @@ export function getEmailData({ message, receiver, sender, subscription, room, em // Reply-To header with format "username+messageId@domain" email.headers['Reply-To'] = `${replyto.split('@')[0].split(settings.get('Direct_Reply_Separator'))[0]}${settings.get( 'Direct_Reply_Separator', - )}${message._id}@${replyto.split('@')[1]}`; + )}${message.tmid || message._id}@${replyto.split('@')[1]}`; } metrics.notificationsSent.inc({ notification_type: 'email' }); diff --git a/apps/meteor/app/lib/server/functions/notifications/index.js b/apps/meteor/app/lib/server/functions/notifications/index.js index 16d45582614f..80ad6378df2d 100644 --- a/apps/meteor/app/lib/server/functions/notifications/index.js +++ b/apps/meteor/app/lib/server/functions/notifications/index.js @@ -3,7 +3,7 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { callbacks } from '../../../../../lib/callbacks'; -import { settings } from '../../../../settings'; +import { settings } from '../../../../settings/server'; /** * This function returns a string ready to be shown in the notification diff --git a/apps/meteor/app/lib/server/functions/notifications/mobile.js b/apps/meteor/app/lib/server/functions/notifications/mobile.js index 9c3b75cdfb28..88d125473639 100644 --- a/apps/meteor/app/lib/server/functions/notifications/mobile.js +++ b/apps/meteor/app/lib/server/functions/notifications/mobile.js @@ -1,7 +1,7 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { Subscriptions } from '@rocket.chat/models'; -import { settings } from '../../../../settings'; -import { Subscriptions } from '../../../../models/server/raw'; +import { settings } from '../../../../settings/server'; import { roomCoordinator } from '../../../../../server/lib/rooms/roomCoordinator'; const CATEGORY_MESSAGE = 'MESSAGE'; diff --git a/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts b/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts index 9c68ac5f9c5d..fd113c836563 100644 --- a/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts +++ b/apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts @@ -1,6 +1,7 @@ +import { Roles } from '@rocket.chat/models'; + import { FileUpload } from '../../../file-upload/server'; import { Subscriptions, Messages, Rooms } from '../../../models/server'; -import { Roles } from '../../../models/server/raw'; import { SubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner'; const bulkRoomCleanUp = (rids: string[]): unknown => { diff --git a/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.js b/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.js index aaacb5062499..17fcbbb90f45 100644 --- a/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.js +++ b/apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.js @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; import s from 'underscore.string'; -import { settings } from '../../../settings'; -import { Users, Subscriptions } from '../../../models'; +import { settings } from '../../../settings/server'; +import { Users, Subscriptions } from '../../../models/server'; export const saveCustomFieldsWithoutValidation = function (userId, formData) { if (s.trim(settings.get('Accounts_CustomFields')) !== '') { diff --git a/apps/meteor/app/lib/server/functions/saveUser.js b/apps/meteor/app/lib/server/functions/saveUser.js index 771f22d4d536..5b4ca2f43c2b 100644 --- a/apps/meteor/app/lib/server/functions/saveUser.js +++ b/apps/meteor/app/lib/server/functions/saveUser.js @@ -6,7 +6,7 @@ import { Gravatar } from 'meteor/jparker:gravatar'; import * as Mailer from '../../../mailer'; import { getRoles, hasPermission } from '../../../authorization'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { passwordPolicy } from '../lib/passwordPolicy'; import { validateEmailDomain } from '../lib'; import { getNewUserRoles } from '../../../../server/services/user/lib/getNewUserRoles'; diff --git a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts index a5d6313b1be3..470b13b3d82e 100644 --- a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts +++ b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import type { IMessage } from '@rocket.chat/core-typings'; +import { VideoConference } from '@rocket.chat/models'; import { _setUsername } from './setUsername'; import { _setRealName } from './setRealName'; @@ -70,9 +71,13 @@ export function saveUserIdentity({ LivechatDepartmentAgents.replaceUsernameOfAgentByUserId(user._id, username); const fileStore = FileUpload.getStore('Avatars'); - const file = fileStore.model.findOneByName(previousUsername); + const previousFile = Promise.await(fileStore.model.findOneByName(previousUsername)); + const file = Promise.await(fileStore.model.findOneByName(username)); if (file) { - fileStore.model.updateFileNameById(file._id, username); + fileStore.model.deleteFile(file._id); + } + if (previousFile) { + fileStore.model.updateFileNameById(previousFile._id, username); } } @@ -83,6 +88,9 @@ export function saveUserIdentity({ // update name and fname of group direct messages updateGroupDMsName(user); + + // update name and username of users on video conferences + Promise.await(VideoConference.updateUserReferences(user._id, username || previousUsername, name || previousName)); } } diff --git a/apps/meteor/app/lib/server/functions/sendMessage.js b/apps/meteor/app/lib/server/functions/sendMessage.js index cb389e13a66c..e80b527573f3 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.js +++ b/apps/meteor/app/lib/server/functions/sendMessage.js @@ -1,14 +1,15 @@ import { Match, check } from 'meteor/check'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { callbacks } from '../../../../lib/callbacks'; -import { Messages } from '../../../models'; +import { Messages } from '../../../models/server'; import { Apps } from '../../../apps/server'; -import { isURL, isRelativeURL } from '../../../utils/lib/isURL'; +import { isURL } from '../../../../lib/utils/isURL'; import { FileUpload } from '../../../file-upload/server'; import { hasPermission } from '../../../authorization/server'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { parseUrlsInMessage } from './parseUrlsInMessage'; +import { isRelativeURL } from '../../../../lib/utils/isRelativeURL'; /** * IMPORTANT diff --git a/apps/meteor/app/lib/server/functions/setRoomAvatar.ts b/apps/meteor/app/lib/server/functions/setRoomAvatar.ts index 1cd2a2442b75..d67eafd1c51c 100644 --- a/apps/meteor/app/lib/server/functions/setRoomAvatar.ts +++ b/apps/meteor/app/lib/server/functions/setRoomAvatar.ts @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; import type { IUser } from '@rocket.chat/core-typings'; +import { Avatars } from '@rocket.chat/models'; import { RocketChatFile } from '../../../file'; import { FileUpload } from '../../../file-upload/server'; import { Rooms, Messages } from '../../../models/server'; -import { Avatars } from '../../../models/server/raw'; import { api } from '../../../../server/sdk/api'; export const setRoomAvatar = async function (rid: string, dataURI: string, user: IUser): Promise { diff --git a/apps/meteor/app/lib/server/functions/setStatusText.ts b/apps/meteor/app/lib/server/functions/setStatusText.ts index 23bda1463602..3077061f48b7 100644 --- a/apps/meteor/app/lib/server/functions/setStatusText.ts +++ b/apps/meteor/app/lib/server/functions/setStatusText.ts @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import s from 'underscore.string'; import type { IUser } from '@rocket.chat/core-typings'; +import { Users as UsersRaw } from '@rocket.chat/models'; import { Users } from '../../../models/server'; -import { Users as UsersRaw } from '../../../models/server/raw'; import { hasPermission } from '../../../authorization/server'; import { RateLimiter } from '../lib'; import { api } from '../../../../server/sdk/api'; diff --git a/apps/meteor/app/lib/server/functions/setUserAvatar.ts b/apps/meteor/app/lib/server/functions/setUserAvatar.ts index e72b847c5cee..4718303b21ca 100644 --- a/apps/meteor/app/lib/server/functions/setUserAvatar.ts +++ b/apps/meteor/app/lib/server/functions/setUserAvatar.ts @@ -8,12 +8,26 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { api } from '../../../../server/sdk/api'; import { fetch } from '../../../../server/lib/http/fetch'; -export const setUserAvatar = function ( +export function setUserAvatar( + user: Pick, + dataURI: Buffer, + contentType: string, + service: 'rest', + etag?: string, +): void; +export function setUserAvatar( user: Pick, dataURI: string, contentType: string, service: 'initials' | 'url' | 'rest' | string, etag?: string, +): void; +export function setUserAvatar( + user: Pick, + dataURI: string | Buffer, + contentType: string, + service: 'initials' | 'url' | 'rest' | string, + etag?: string, ): void { if (service === 'initials') { Users.setAvatarData(user._id, service, null); @@ -22,7 +36,7 @@ export const setUserAvatar = function ( const { buffer, type } = Promise.await( (async (): Promise<{ buffer: Buffer; type: string }> => { - if (service === 'url') { + if (service === 'url' && typeof dataURI === 'string') { let response: Response; try { response = await fetch(dataURI); @@ -69,7 +83,7 @@ export const setUserAvatar = function ( if (service === 'rest') { return { - buffer: Buffer.from(dataURI, 'binary'), + buffer: dataURI instanceof Buffer ? dataURI : Buffer.from(dataURI, 'binary'), type: contentType, }; } @@ -103,4 +117,4 @@ export const setUserAvatar = function ( avatarETag, }); }, 500); -}; +} diff --git a/apps/meteor/app/lib/server/functions/setUsername.ts b/apps/meteor/app/lib/server/functions/setUsername.ts index 71cbc2afd557..6dc5eb4b0fb1 100644 --- a/apps/meteor/app/lib/server/functions/setUsername.ts +++ b/apps/meteor/app/lib/server/functions/setUsername.ts @@ -2,10 +2,10 @@ import { Meteor } from 'meteor/meteor'; import s from 'underscore.string'; import { Accounts } from 'meteor/accounts-base'; import type { IUser } from '@rocket.chat/core-typings'; +import { Invites } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; import { Users } from '../../../models/server'; -import { Invites } from '../../../models/server/raw'; import { hasPermission } from '../../../authorization/server'; import { RateLimiter } from '../lib'; import { addUserToRoom } from './addUserToRoom'; diff --git a/apps/meteor/app/lib/server/functions/validateCustomFields.js b/apps/meteor/app/lib/server/functions/validateCustomFields.js index 37e953497134..b9580ce559d6 100644 --- a/apps/meteor/app/lib/server/functions/validateCustomFields.js +++ b/apps/meteor/app/lib/server/functions/validateCustomFields.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import s from 'underscore.string'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; export const validateCustomFields = function (fields) { // Special Case: diff --git a/apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.js b/apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.js index f00b0fc5762f..ba8783369de6 100644 --- a/apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.js +++ b/apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.js @@ -1,13 +1,11 @@ -import { Meteor } from 'meteor/meteor'; -import POP3Lib from 'poplib'; +import POP3Lib from '@rocket.chat/poplib'; import { simpleParser } from 'mailparser'; -import { settings } from '../../../settings/server'; -import { SystemLogger } from '../../../../server/lib/logger/system'; +import { settings } from '../../../settings'; import { IMAPInterceptor } from '../../../../server/email/IMAPInterceptor'; import { processDirectEmail } from '.'; -export class IMAPIntercepter extends IMAPInterceptor { +export class DirectReplyIMAPInterceptor extends IMAPInterceptor { constructor(imapConfig, options = {}) { imapConfig = { user: settings.get('Direct_Reply_Username'), @@ -23,10 +21,7 @@ export class IMAPIntercepter extends IMAPInterceptor { super(imapConfig, options); - this.on( - 'email', - Meteor.bindEnvironment((email) => processDirectEmail(email)), - ); + this.on('email', (email) => processDirectEmail(email)); } } @@ -34,143 +29,120 @@ export class POP3Intercepter { constructor() { this.pop3 = new POP3Lib(settings.get('Direct_Reply_Port'), settings.get('Direct_Reply_Host'), { enabletls: !settings.get('Direct_Reply_IgnoreTLS'), - debug: settings.get('Direct_Reply_Debug') ? console.log : false, + // debug: settings.get('Direct_Reply_Debug') ? console.log : false, + debug: console.log, }); this.totalMsgCount = 0; this.currentMsgCount = 0; - this.pop3.on( - 'connect', - Meteor.bindEnvironment(() => { - this.pop3.login(settings.get('Direct_Reply_Username'), settings.get('Direct_Reply_Password')); - }), - ); - - this.pop3.on( - 'login', - Meteor.bindEnvironment((status) => { - if (status) { - // run on start - this.pop3.list(); - } else { - SystemLogger.info('Unable to Log-in ....'); - } - }), - ); + this.pop3.on('connect', () => { + console.log('Pop connect'); + this.pop3.login(settings.get('Direct_Reply_Username'), settings.get('Direct_Reply_Password')); + }); + + this.pop3.on('login', (status) => { + if (!status) { + return console.log('Unable to Log-in ....'); + } + console.log('Pop logged'); + // run on start + this.pop3.list(); + }); // on getting list of all emails - this.pop3.on( - 'list', - Meteor.bindEnvironment((status, msgcount) => { - if (status) { - if (msgcount > 0) { - this.totalMsgCount = msgcount; - this.currentMsgCount = 1; - // Retrieve email - this.pop3.retr(this.currentMsgCount); - } else { - this.pop3.quit(); - } - } else { - SystemLogger.info('Cannot Get Emails ....'); - } - }), - ); + this.pop3.on('list', (status, msgcount) => { + if (!status) { + console.log('Cannot Get Emails ....'); + } + if (msgcount === 0) { + return this.pop3.quit(); + } + + this.totalMsgCount = msgcount; + this.currentMsgCount = 1; + // Retrieve email + this.pop3.retr(this.currentMsgCount); + }); // on retrieved email - this.pop3.on( - 'retr', - Meteor.bindEnvironment((status, msgnumber, data) => { - if (status) { - // parse raw email data to JSON object - simpleParser( - data, - Meteor.bindEnvironment((err, mail) => { - this.initialProcess(mail); - }), - ); - - this.currentMsgCount += 1; - - // delete email - this.pop3.dele(msgnumber); - } else { - SystemLogger.info('Cannot Retrieve Message ....'); - } - }), - ); + this.pop3.on('retr', (status, msgnumber, data) => { + if (!status) { + return console.log('Cannot Retrieve Message ....'); + } + + // parse raw email data to JSON object + simpleParser(data, (err, mail) => { + processDirectEmail(mail); + }); + + this.currentMsgCount += 1; + + // delete email + this.pop3.dele(msgnumber); + }); // on email deleted - this.pop3.on( - 'dele', - Meteor.bindEnvironment((status) => { - if (status) { - // get next email - if (this.currentMsgCount <= this.totalMsgCount) { - this.pop3.retr(this.currentMsgCount); - } else { - // parsed all messages.. so quitting - this.pop3.quit(); - } - } else { - SystemLogger.info('Cannot Delete Message....'); - } - }), - ); + this.pop3.on('dele', (status) => { + if (!status) { + return console.log('Cannot Delete Message....'); + } + + // get next email + if (this.currentMsgCount <= this.totalMsgCount) { + return this.pop3.retr(this.currentMsgCount); + } + + // parsed all messages.. so quitting + this.pop3.quit(); + }); // invalid server state this.pop3.on('invalid-state', function (cmd) { - SystemLogger.info(`Invalid state. You tried calling ${cmd}`); + console.log(`Invalid state. You tried calling ${cmd}`); + }); + + this.pop3.on('error', function (cmd) { + console.log(`error state. You tried calling ${cmd}`); }); // locked => command already running, not finished yet this.pop3.on('locked', function (cmd) { - SystemLogger.info(`Current command has not finished yet. You tried calling ${cmd}`); + console.log(`Current command has not finished yet. You tried calling ${cmd}`); }); } - - initialProcess(mail) { - const email = { - headers: { - 'from': mail.from.text, - 'to': mail.to.text, - 'date': mail.date, - 'message-id': mail.messageId, - }, - body: mail.text, - }; - - processDirectEmail(email); - } } -export let POP3; export class POP3Helper { - constructor() { + constructor(frequency) { + this.frequency = frequency; this.running = false; - } - start() { - // run every x-minutes - if (settings.get('Direct_Reply_Frequency')) { - POP3 = new POP3Intercepter(); - - this.running = Meteor.setInterval(() => { - // get new emails and process - POP3 = new POP3Intercepter(); - }, Math.max(settings.get('Direct_Reply_Frequency') * 60 * 1000, 2 * 60 * 1000)); - } + this.POP3 = new POP3Intercepter(); } isActive() { return this.running; } + start() { + this.log('POP3 started'); + this.running = setInterval(() => { + // get new emails and process + this.POP3 = new POP3Intercepter(); + }, Math.max(this.frequency * 60 * 1000, 2 * 60 * 1000)); + } + + log(...args) { + console.log(...args); + } + stop(callback = new Function()) { + this.log('POP3 stop called'); if (this.isActive()) { - Meteor.clearInterval(this.running); + clearInterval(this.running); } callback(); + this.log('POP3 stopped'); } } diff --git a/apps/meteor/app/lib/server/lib/meteorFixes.js b/apps/meteor/app/lib/server/lib/meteorFixes.js index abc7a78855cb..0d7af6675f9a 100644 --- a/apps/meteor/app/lib/server/lib/meteorFixes.js +++ b/apps/meteor/app/lib/server/lib/meteorFixes.js @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { MongoInternals } from 'meteor/mongo'; +import { Settings } from '@rocket.chat/models'; const timeoutQuery = parseInt(process.env.OBSERVERS_CHECK_TIMEOUT) || 2 * 60 * 1000; const interval = parseInt(process.env.OBSERVERS_CHECK_INTERVAL) || 60 * 1000; @@ -58,7 +59,9 @@ Meteor.setInterval(() => { * we will start respecting this and exit the process to prevent these kind of problems. */ -process.on('unhandledRejection', (error) => { +process.on('unhandledRejection', async (error) => { + await Settings.incrementValueById('Uncaught_Exceptions_Count'); + console.error('=== UnHandledPromiseRejection ==='); console.error(error); console.error('---------------------------------'); diff --git a/apps/meteor/app/lib/server/lib/processDirectEmail.js b/apps/meteor/app/lib/server/lib/processDirectEmail.js deleted file mode 100644 index 6bec346c8889..000000000000 --- a/apps/meteor/app/lib/server/lib/processDirectEmail.js +++ /dev/null @@ -1,128 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { EmailReplyParser as reply } from 'emailreplyparser'; -import moment from 'moment'; - -import { settings } from '../../../settings/server'; -import { Rooms, Messages, Users, Subscriptions } from '../../../models/server'; -import { metrics } from '../../../metrics/server'; -import { canAccessRoom, hasPermission } from '../../../authorization/server'; -import { SystemLogger } from '../../../../server/lib/logger/system'; -import { sendMessage as _sendMessage } from '../functions'; - -export const processDirectEmail = function (email) { - function sendMessage(email) { - const message = { - ts: new Date(email.headers.date), - msg: email.body, - sentByEmail: true, - groupable: false, - }; - - if (message.ts) { - const tsDiff = Math.abs(moment(message.ts).diff()); - if (tsDiff > 10000) { - message.ts = new Date(); - } - } else { - message.ts = new Date(); - } - - if (message.msg && message.msg.length > settings.get('Message_MaxAllowedSize')) { - return false; - } - - // reduce new lines in multiline message - message.msg = message.msg.split('\n\n').join('\n'); - - const user = Users.findOneByEmailAddress(email.headers.from, { - fields: { - username: 1, - name: 1, - }, - }); - if (!user) { - // user not found - return false; - } - - const prevMessage = Messages.findOneById(email.headers.mid, { - rid: 1, - u: 1, - }); - if (!prevMessage) { - // message doesn't exist anymore - return false; - } - message.rid = prevMessage.rid; - - const room = Rooms.findOneById(message.rid); - - if (!canAccessRoom(room, user)) { - return false; - } - - // check mention - if (message.msg.indexOf(`@${prevMessage.u.username}`) === -1 && room.t !== 'd') { - message.msg = `@${prevMessage.u.username} ${message.msg}`; - } - - // reply message link - let prevMessageLink = `[ ](${Meteor.absoluteUrl().replace(/\/$/, '')}`; - if (room.t === 'c') { - prevMessageLink += `/channel/${room.name}?msg=${email.headers.mid}) `; - } else if (room.t === 'd') { - prevMessageLink += `/direct/${prevMessage.u.username}?msg=${email.headers.mid}) `; - } else if (room.t === 'p') { - prevMessageLink += `/group/${room.name}?msg=${email.headers.mid}) `; - } - // add reply message link - message.msg = prevMessageLink + message.msg; - - const subscription = Subscriptions.findOneByRoomIdAndUserId(message.rid, user._id); - if (subscription && (subscription.blocked || subscription.blocker)) { - // room is blocked - return false; - } - - if (room?.muted?.includes(user.username)) { - // user is muted - return false; - } - - // room is readonly - if (room.ro === true) { - if (!hasPermission(Meteor.userId(), 'post-readonly', room._id)) { - // Check if the user was manually unmuted - if (!(room.unmuted || []).includes(user.username)) { - return false; - } - } - } - - metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 - - return _sendMessage(user, message, room); - } - - // Extract/parse reply from email body - email.body = reply.parse_reply(email.body); - - // if 'To' email format is "Name " - if (email.headers.to.indexOf('<') >= 0 && email.headers.to.indexOf('>') >= 0) { - email.headers.to = email.headers.to.split('<')[1].split('>')[0]; - } - - // if 'From' email format is "Name " - if (email.headers.from.indexOf('<') >= 0 && email.headers.from.indexOf('>') >= 0) { - email.headers.from = email.headers.from.split('<')[1].split('>')[0]; - } - - // 'To' email format "username+messageId@domain" - if (email.headers.to.indexOf('+') >= 0) { - // Valid 'To' format - email.headers.mid = email.headers.to.split('@')[0].split('+')[1]; - sendMessage(email); - } else { - SystemLogger.error('Invalid Email....If not. Please report it.'); - } -}; diff --git a/apps/meteor/app/lib/server/lib/processDirectEmail.ts b/apps/meteor/app/lib/server/lib/processDirectEmail.ts new file mode 100644 index 000000000000..0819f4475626 --- /dev/null +++ b/apps/meteor/app/lib/server/lib/processDirectEmail.ts @@ -0,0 +1,115 @@ +import { Meteor } from 'meteor/meteor'; +import moment from 'moment'; +import { ParsedMail } from 'mailparser'; +import { IMessage, IRoom } from '@rocket.chat/core-typings'; + +import { settings } from '../../../settings/server'; +import { Rooms, Messages, Users, Subscriptions } from '../../../models/server'; +import { metrics } from '../../../metrics'; +import { canAccessRoom, hasPermission } from '../../../authorization/server'; +import { sendMessage } from '../functions/sendMessage'; + +const isParsedEmail = (email: ParsedMail): email is Required => 'date' in email && 'html' in email; + +export const processDirectEmail = Meteor.bindEnvironment(function (email: ParsedMail): void { + if (!isParsedEmail(email)) { + return; + } + + const to = Array.isArray(email.to) ? email.to.find((email) => email.text) : email.to; + const mid = to?.text.split('@')[0].split('+')[1]; + + if (!mid) { + return; + } + + const ts = new Date(email.date); + + const tsDiff = Math.abs(moment(ts).diff(new Date())); + + let msg = email.text.split('\n\n').join('\n'); + + if (msg && msg.length > (settings.get('Message_MaxAllowedSize') as number)) { + return; + } + + const user = Users.findOneByEmailAddress(email.from.value[0].address, { + fields: { + username: 1, + name: 1, + }, + }); + + if (!user) { + // user not found + return; + } + + const prevMessage = Messages.findOneById(mid, { + rid: 1, + u: 1, + }); + + if (!prevMessage) { + // message doesn't exist anymore + return; + } + + const roomInfo: IRoom = Rooms.findOneById(prevMessage.rid); + + const room = canAccessRoom(roomInfo, user); + if (!room) { + return; + } + + // check mention + if (msg.indexOf(`@${prevMessage.u.username}`) === -1 && roomInfo.t !== 'd') { + msg = `@${prevMessage.u.username} ${msg}`; + } + + // reply message link WE HA THREADS KNOW DONT NEED TO QUOTE THE previous message + // let prevMessageLink = `[ ](${ Meteor.absoluteUrl().replace(/\/$/, '') }`; + // if (roomInfo.t === 'c') { + // prevMessageLink += `/channel/${ roomInfo.name }?msg=${ mid }) `; + // } else if (roomInfo.t === 'd') { + // prevMessageLink += `/direct/${ prevMessage.u.username }?msg=${ mid }) `; + // } else if (roomInfo.t === 'p') { + // prevMessageLink += `/group/${ roomInfo.name }?msg=${ mid }) `; + // } + // // add reply message link + // msg = prevMessageLink + msg; + + const subscription = Subscriptions.findOneByRoomIdAndUserId(prevMessage.rid, user._id); + if (subscription && (subscription.blocked || subscription.blocker)) { + // room is blocked + return; + } + + if ((roomInfo.muted || []).includes(user.username)) { + // user is muted + return; + } + + // room is readonly + if (roomInfo.ro === true) { + if (!hasPermission(user._id, 'post-readonly', roomInfo._id)) { + // Check if the user was manually unmuted + if (!(roomInfo.unmuted || []).includes(user.username)) { + return; + } + } + } + + metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 + + const message: Pick = { + ts: tsDiff < 10000 ? ts : new Date(), + msg, + sentByEmail: true, + groupable: false, + tmid: mid, + rid: prevMessage.rid, + }; + + return sendMessage(user, message, roomInfo); +}); diff --git a/apps/meteor/app/lib/server/methods/addUsersToRoom.js b/apps/meteor/app/lib/server/methods/addUsersToRoom.js index bdec86a80db5..99c5db5d011b 100644 --- a/apps/meteor/app/lib/server/methods/addUsersToRoom.js +++ b/apps/meteor/app/lib/server/methods/addUsersToRoom.js @@ -2,10 +2,11 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { Rooms, Subscriptions, Users } from '../../../models'; +import { Rooms, Subscriptions, Users } from '../../../models/server'; import { hasPermission } from '../../../authorization'; import { addUserToRoom } from '../functions'; import { api } from '../../../../server/sdk/api'; +import { Federation } from '../../../federation-v2/server/infrastructure/rocket-chat/Federation'; Meteor.methods({ addUsersToRoom(data = {}) { @@ -65,14 +66,14 @@ Meteor.methods({ const user = Meteor.user(); data.users.forEach((username) => { const newUser = Users.findOneByUsernameIgnoringCase(username); - if (!newUser) { + if (!newUser && !Federation.isAFederatedUsername(username)) { throw new Meteor.Error('error-invalid-username', 'Invalid username', { method: 'addUsersToRoom', }); } - const subscription = Subscriptions.findOneByRoomIdAndUserId(data.rid, newUser._id); + const subscription = newUser && Subscriptions.findOneByRoomIdAndUserId(data.rid, newUser._id); if (!subscription) { - addUserToRoom(data.rid, newUser, user); + addUserToRoom(data.rid, newUser || username, user); } else { api.broadcast('notify.ephemeralMessage', userId, data.rid, { msg: TAPi18n.__( diff --git a/apps/meteor/app/lib/server/methods/createPrivateGroup.js b/apps/meteor/app/lib/server/methods/createPrivateGroup.js index c274cca5840d..a9b4f62406f1 100644 --- a/apps/meteor/app/lib/server/methods/createPrivateGroup.js +++ b/apps/meteor/app/lib/server/methods/createPrivateGroup.js @@ -19,22 +19,6 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createPrivateGroup' }); } - // validate extra data schema - check( - extraData, - Match.ObjectIncluding({ - tokenpass: Match.Maybe({ - require: String, - tokens: [ - { - token: String, - balance: String, - }, - ], - }), - }), - ); - return createRoom('p', name, Meteor.user() && Meteor.user().username, members, readOnly, { customFields, ...extraData, diff --git a/apps/meteor/app/lib/server/methods/filterATAllTag.js b/apps/meteor/app/lib/server/methods/filterATAllTag.js index 67ae6da78a2c..2c1d52a95093 100644 --- a/apps/meteor/app/lib/server/methods/filterATAllTag.js +++ b/apps/meteor/app/lib/server/methods/filterATAllTag.js @@ -5,7 +5,7 @@ import moment from 'moment'; import { hasPermission } from '../../../authorization'; import { callbacks } from '../../../../lib/callbacks'; -import { Users } from '../../../models'; +import { Users } from '../../../models/server'; import { api } from '../../../../server/sdk/api'; callbacks.add( diff --git a/apps/meteor/app/lib/server/methods/filterATHereTag.js b/apps/meteor/app/lib/server/methods/filterATHereTag.js index 9691c416cb82..0978520e1cdd 100644 --- a/apps/meteor/app/lib/server/methods/filterATHereTag.js +++ b/apps/meteor/app/lib/server/methods/filterATHereTag.js @@ -5,7 +5,7 @@ import moment from 'moment'; import { hasPermission } from '../../../authorization'; import { callbacks } from '../../../../lib/callbacks'; -import { Users } from '../../../models'; +import { Users } from '../../../models/server'; import { api } from '../../../../server/sdk/api'; callbacks.add( diff --git a/apps/meteor/app/lib/server/methods/getChannelHistory.ts b/apps/meteor/app/lib/server/methods/getChannelHistory.ts index 4557282191a1..948508dd5a3a 100644 --- a/apps/meteor/app/lib/server/methods/getChannelHistory.ts +++ b/apps/meteor/app/lib/server/methods/getChannelHistory.ts @@ -105,8 +105,17 @@ Meteor.methods({ showThreadMessages, ); + const totalCursor = Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes( + rid, + oldest, + firstMsg.ts, + hiddenMessageTypes, + {}, + showThreadMessages, + ); + firstUnread = unreadMessages.fetch()[0]; - unreadNotLoaded = unreadMessages.count(); + unreadNotLoaded = totalCursor.count(); } } diff --git a/apps/meteor/app/lib/server/methods/joinRoom.ts b/apps/meteor/app/lib/server/methods/joinRoom.ts index fad1bb88dea0..dcde906be1af 100644 --- a/apps/meteor/app/lib/server/methods/joinRoom.ts +++ b/apps/meteor/app/lib/server/methods/joinRoom.ts @@ -3,7 +3,6 @@ import { check } from 'meteor/check'; import { hasPermission, canAccessRoom } from '../../../authorization/server'; import { Rooms } from '../../../models/server'; -import { Tokenpass, updateUserTokenpassBalances } from '../../../tokenpass/server'; import { addUserToRoom } from '../functions'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; @@ -28,23 +27,13 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'joinRoom' }); } - // TODO we should have a 'beforeJoinRoom' call back so external services can do their own validations - - if (room.tokenpass && user && user.services && user.services.tokenpass) { - const balances = updateUserTokenpassBalances(user); - - if (!Tokenpass.validateAccess(room.tokenpass, balances)) { - throw new Meteor.Error('error-not-allowed', 'Token required', { method: 'joinRoom' }); - } - } else { - if (!canAccessRoom(room, user)) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'joinRoom' }); - } - if (room.joinCodeRequired === true && code !== room.joinCode && !hasPermission(user._id, 'join-without-join-code')) { - throw new Meteor.Error('error-code-invalid', 'Invalid Room Password', { - method: 'joinRoom', - }); - } + if (!canAccessRoom(room, user)) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'joinRoom' }); + } + if (room.joinCodeRequired === true && code !== room.joinCode && !hasPermission(user._id, 'join-without-join-code')) { + throw new Meteor.Error('error-code-invalid', 'Invalid Room Password', { + method: 'joinRoom', + }); } return addUserToRoom(rid, user); diff --git a/apps/meteor/app/lib/server/methods/leaveRoom.ts b/apps/meteor/app/lib/server/methods/leaveRoom.ts index 9db1d228e1d0..d9216f65df59 100644 --- a/apps/meteor/app/lib/server/methods/leaveRoom.ts +++ b/apps/meteor/app/lib/server/methods/leaveRoom.ts @@ -1,11 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import type { IUser } from '@rocket.chat/core-typings'; +import { Roles } from '@rocket.chat/models'; import { hasPermission, hasRole } from '../../../authorization/server'; import { Subscriptions, Rooms } from '../../../models/server'; import { removeUserFromRoom } from '../functions'; -import { Roles } from '../../../models/server/raw'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; @@ -40,7 +40,7 @@ Meteor.methods({ // If user is room owner, check if there are other owners. If there isn't anyone else, warn user to set a new owner. if (hasRole(user._id, 'owner', room._id)) { const cursor = await Roles.findUsersInRole('owner', room._id); - const numOwners = Promise.await(cursor.count()); + const numOwners = await cursor.count(); if (numOwners === 1) { throw new Meteor.Error('error-you-are-last-owner', 'You are the last owner. Please set new owner before leaving the room.', { method: 'leaveRoom', diff --git a/apps/meteor/app/lib/server/methods/refreshOAuthService.ts b/apps/meteor/app/lib/server/methods/refreshOAuthService.ts index 1373cd10d436..b77b3e213c86 100644 --- a/apps/meteor/app/lib/server/methods/refreshOAuthService.ts +++ b/apps/meteor/app/lib/server/methods/refreshOAuthService.ts @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { ServiceConfiguration } from 'meteor/service-configuration'; +import { Settings } from '@rocket.chat/models'; import { hasPermission } from '../../../authorization/server'; -import { Settings } from '../../../models/server/raw'; Meteor.methods({ async refreshOAuthService() { @@ -23,6 +23,6 @@ Meteor.methods({ ServiceConfiguration.configurations.remove({}); - await Settings.update({ _id: /^(Accounts_OAuth_|SAML_|CAS_|Blockstack_).+/ }, { $set: { _updatedAt: new Date() } }, { multi: true }); + await Settings.update({ _id: /^(Accounts_OAuth_|SAML_|CAS_).+/ }, { $set: { _updatedAt: new Date() } }, { multi: true }); }, }); diff --git a/apps/meteor/app/lib/server/methods/removeOAuthService.ts b/apps/meteor/app/lib/server/methods/removeOAuthService.ts index 419992e2b0e5..9ef34bf59088 100644 --- a/apps/meteor/app/lib/server/methods/removeOAuthService.ts +++ b/apps/meteor/app/lib/server/methods/removeOAuthService.ts @@ -1,9 +1,9 @@ import { capitalize } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; +import { Settings } from '@rocket.chat/models'; import { hasPermission } from '../../../authorization/server'; -import { Settings } from '../../../models/server/raw'; Meteor.methods({ async removeOAuthService(name) { diff --git a/apps/meteor/app/lib/server/methods/saveSetting.js b/apps/meteor/app/lib/server/methods/saveSetting.js index 5c24bd377bdc..4e8829fb07bc 100644 --- a/apps/meteor/app/lib/server/methods/saveSetting.js +++ b/apps/meteor/app/lib/server/methods/saveSetting.js @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; +import { Settings } from '@rocket.chat/models'; import { hasPermission, hasAllPermission } from '../../../authorization/server'; import { getSettingPermissionId } from '../../../authorization/lib'; import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; -import { Settings } from '../../../models/server/raw'; Meteor.methods({ saveSetting: twoFactorRequired(async function (_id, value, editor) { diff --git a/apps/meteor/app/lib/server/methods/saveSettings.js b/apps/meteor/app/lib/server/methods/saveSettings.js index 137ca376a53e..5bbfd4d2cfea 100644 --- a/apps/meteor/app/lib/server/methods/saveSettings.js +++ b/apps/meteor/app/lib/server/methods/saveSettings.js @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; +import { Settings } from '@rocket.chat/models'; import { hasPermission } from '../../../authorization/server'; import { getSettingPermissionId } from '../../../authorization/lib'; import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; -import { Settings } from '../../../models/server/raw'; Meteor.methods({ saveSettings: twoFactorRequired(async function (params = []) { diff --git a/apps/meteor/app/lib/server/methods/sendInvitationEmail.js b/apps/meteor/app/lib/server/methods/sendInvitationEmail.js index 89cbdce9eb97..fe33f73fe323 100644 --- a/apps/meteor/app/lib/server/methods/sendInvitationEmail.js +++ b/apps/meteor/app/lib/server/methods/sendInvitationEmail.js @@ -3,7 +3,7 @@ import { check } from 'meteor/check'; import * as Mailer from '../../../mailer'; import { hasPermission } from '../../../authorization'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { Settings as SettingsRaw } from '../../../models/server'; let html = ''; diff --git a/apps/meteor/app/lib/server/methods/sendMessage.js b/apps/meteor/app/lib/server/methods/sendMessage.js index 91e223322b36..e5c7f4e99f13 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.js +++ b/apps/meteor/app/lib/server/methods/sendMessage.js @@ -5,15 +5,16 @@ import moment from 'moment'; import { hasPermission } from '../../../authorization'; import { metrics } from '../../../metrics'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; 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/lib/server/methods/sendSMTPTestEmail.js b/apps/meteor/app/lib/server/methods/sendSMTPTestEmail.js index 56432497522d..da8648a2bce5 100644 --- a/apps/meteor/app/lib/server/methods/sendSMTPTestEmail.js +++ b/apps/meteor/app/lib/server/methods/sendSMTPTestEmail.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import * as Mailer from '../../../mailer'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; Meteor.methods({ sendSMTPTestEmail() { diff --git a/apps/meteor/app/lib/server/methods/updateMessage.js b/apps/meteor/app/lib/server/methods/updateMessage.js index 9a30a1a96d53..a615d679c52b 100644 --- a/apps/meteor/app/lib/server/methods/updateMessage.js +++ b/apps/meteor/app/lib/server/methods/updateMessage.js @@ -2,8 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import moment from 'moment'; -import { Messages } from '../../../models'; -import { settings } from '../../../settings'; +import { Messages } from '../../../models/server'; +import { settings } from '../../../settings/server'; import { hasPermission, canSendMessage } from '../../../authorization/server'; import { updateMessage } from '../functions'; diff --git a/apps/meteor/app/lib/server/oauth/proxy.js b/apps/meteor/app/lib/server/oauth/proxy.js index ef0b12c39301..1ea4db77f257 100644 --- a/apps/meteor/app/lib/server/oauth/proxy.js +++ b/apps/meteor/app/lib/server/oauth/proxy.js @@ -1,7 +1,7 @@ import _ from 'underscore'; import { OAuth } from 'meteor/oauth'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; OAuth._redirectUri = _.wrap(OAuth._redirectUri, function (func, serviceName, ...args) { const proxy = settings.get('Accounts_OAuth_Proxy_services').replace(/\s/g, '').split(','); diff --git a/apps/meteor/app/lib/server/startup/email.ts b/apps/meteor/app/lib/server/startup/email.ts index 470599a41d52..bac864aa52b3 100644 --- a/apps/meteor/app/lib/server/startup/email.ts +++ b/apps/meteor/app/lib/server/startup/email.ts @@ -102,6 +102,9 @@ settingsRegistry.addGroup('Email', function () { } .social { font-size: 12px + } + .rc-color { + color: #F5455C; } `, { diff --git a/apps/meteor/app/lib/server/startup/robots.js b/apps/meteor/app/lib/server/startup/robots.js index 2ec4e3c19b4e..c7dbfb442462 100644 --- a/apps/meteor/app/lib/server/startup/robots.js +++ b/apps/meteor/app/lib/server/startup/robots.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; Meteor.startup(function () { return WebApp.connectHandlers.use( diff --git a/apps/meteor/app/lib/server/startup/settings.ts b/apps/meteor/app/lib/server/startup/settings.ts index 112241a71f69..9500c7138b08 100644 --- a/apps/meteor/app/lib/server/startup/settings.ts +++ b/apps/meteor/app/lib/server/startup/settings.ts @@ -426,10 +426,23 @@ settingsRegistry.addGroup('Accounts', function () { i18nLabel: 'Sort_By', }); - this.add('Accounts_Default_User_Preferences_showMessageInMainThread', false, { - type: 'boolean', + this.add('Accounts_Default_User_Preferences_alsoSendThreadToChannel', 'default', { + type: 'select', + values: [ + { + key: 'default', + i18nLabel: 'Default', + }, + { + key: 'always', + i18nLabel: 'Always', + }, + { + key: 'never', + i18nLabel: 'Never', + }, + ], public: true, - i18nLabel: 'Show_Message_In_Main_Thread', }); this.add('Accounts_Default_User_Preferences_sidebarShowFavorites', true, { @@ -457,6 +470,7 @@ settingsRegistry.addGroup('Accounts', function () { public: true, i18nLabel: 'Enter_Behaviour', }); + this.add('Accounts_Default_User_Preferences_messageViewMode', 0, { type: 'select', values: [ @@ -534,11 +548,11 @@ settingsRegistry.addGroup('Accounts', function () { i18nLabel: 'Notifications_Sound_Volume', }); - this.add('Accounts_Default_User_Preferences_enableNewMessageTemplate', false, { + this.add('Accounts_Default_User_Preferences_useLegacyMessageTemplate', false, { type: 'boolean', public: true, - i18nLabel: 'Enable_New_Message_Template', - alert: 'Enable_New_Message_Template_alert', + i18nLabel: 'Use_Legacy_Message_Template', + alert: 'Use_Legacy_Message_Template_alert', }); }); @@ -1702,6 +1716,11 @@ settingsRegistry.addGroup('Logs', function () { }, }); + this.add('Uncaught_Exceptions_Count', 0, { + hidden: true, + type: 'int', + }); + this.section('Prometheus', function () { this.add('Prometheus_Enabled', false, { type: 'boolean', @@ -2911,6 +2930,10 @@ settingsRegistry.addGroup('Setup_Wizard', function () { this.add('Organization_Email', '', { type: 'string', }); + this.add('Triggered_Emails_Count', 0, { + type: 'int', + hidden: true, + }); }); this.section('Cloud_Info', function () { @@ -3187,7 +3210,6 @@ settingsRegistry.addGroup('Call_Center', function () { this.add('VoIP_Enabled', false, { type: 'boolean', public: true, - alert: 'Experimental_Feature_Alert', enableQuery: { _id: 'Livechat_enabled', value: true, @@ -3202,22 +3224,6 @@ settingsRegistry.addGroup('Call_Center', function () { }, }); this.section('Server_Configuration', function () { - this.add('VoIP_Server_Host', '', { - type: 'string', - public: true, - enableQuery: { - _id: 'VoIP_Enabled', - value: true, - }, - }); - this.add('VoIP_Server_Websocket_Port', 0, { - type: 'int', - public: true, - enableQuery: { - _id: 'VoIP_Enabled', - value: true, - }, - }); this.add('VoIP_Server_Name', '', { type: 'string', public: true, diff --git a/apps/meteor/app/lib/server/startup/settingsOnLoadDirectReply.js b/apps/meteor/app/lib/server/startup/settingsOnLoadDirectReply.js deleted file mode 100644 index a58aacd884a9..000000000000 --- a/apps/meteor/app/lib/server/startup/settingsOnLoadDirectReply.js +++ /dev/null @@ -1,99 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Logger } from '../../../logger/server'; -import { settings } from '../../../settings/server'; -import { IMAPIntercepter, POP3Helper, POP3 } from '../lib/interceptDirectReplyEmails.js'; - -const logger = new Logger('Email Intercepter'); - -let IMAP; -let _POP3Helper; - -settings.watchMultiple( - [ - 'Direct_Reply_Enable', - 'Direct_Reply_Protocol', - 'Direct_Reply_Host', - 'Direct_Reply_Port', - 'Direct_Reply_Username', - 'Direct_Reply_Password', - 'Direct_Reply_Protocol', - 'Direct_Reply_Protocol', - ], - function () { - logger.debug('Starting Email Intercepter...'); - - if ( - settings.get('Direct_Reply_Enable') && - settings.get('Direct_Reply_Protocol') && - settings.get('Direct_Reply_Host') && - settings.get('Direct_Reply_Port') && - settings.get('Direct_Reply_Username') && - settings.get('Direct_Reply_Password') - ) { - if (settings.get('Direct_Reply_Protocol') === 'IMAP') { - // stop already running IMAP instance - if (IMAP && IMAP.isActive()) { - logger.debug('Disconnecting already running IMAP instance...'); - IMAP.stop( - Meteor.bindEnvironment(function () { - logger.debug('Starting new IMAP instance......'); - IMAP = new IMAPIntercepter(); - IMAP.start(); - return true; - }), - ); - } else if (POP3 && _POP3Helper && _POP3Helper.isActive()) { - logger.debug('Disconnecting already running POP instance...'); - _POP3Helper.stop( - Meteor.bindEnvironment(function () { - logger.debug('Starting new IMAP instance......'); - IMAP = new IMAPIntercepter(); - IMAP.start(); - return true; - }), - ); - } else { - logger.debug('Starting new IMAP instance......'); - IMAP = new IMAPIntercepter(); - IMAP.start(); - return true; - } - } else if (settings.get('Direct_Reply_Protocol') === 'POP') { - // stop already running POP instance - if (POP3 && _POP3Helper && _POP3Helper.isActive()) { - logger.debug('Disconnecting already running POP instance...'); - _POP3Helper.stop( - Meteor.bindEnvironment(function () { - logger.debug('Starting new POP instance......'); - _POP3Helper = new POP3Helper(); - _POP3Helper.start(); - return true; - }), - ); - } else if (IMAP && IMAP.isActive()) { - logger.debug('Disconnecting already running IMAP instance...'); - IMAP.stop( - Meteor.bindEnvironment(function () { - logger.debug('Starting new POP instance......'); - _POP3Helper = new POP3Helper(); - _POP3Helper.start(); - return true; - }), - ); - } else { - logger.debug('Starting new POP instance......'); - _POP3Helper = new POP3Helper(); - _POP3Helper.start(); - return true; - } - } - } else if (IMAP && IMAP.isActive()) { - // stop IMAP instance - IMAP.stop(); - } else if (POP3 && _POP3Helper.isActive()) { - // stop POP3 instance - _POP3Helper.stop(); - } - }, -); diff --git a/apps/meteor/app/lib/server/startup/settingsOnLoadDirectReply.ts b/apps/meteor/app/lib/server/startup/settingsOnLoadDirectReply.ts new file mode 100644 index 000000000000..0b4f55d9d831 --- /dev/null +++ b/apps/meteor/app/lib/server/startup/settingsOnLoadDirectReply.ts @@ -0,0 +1,43 @@ +import _ from 'underscore'; + +import { settings } from '../../../settings/server'; +import { DirectReplyIMAPInterceptor, POP3Helper } from '../lib/interceptDirectReplyEmails.js'; +import { logger } from '../../../../server/features/EmailInbox/logger'; + +let client: DirectReplyIMAPInterceptor | POP3Helper | undefined; +const startEmailInterceptor = _.debounce(async function () { + logger.info('Email Interceptor...'); + const protocol = settings.get('Direct_Reply_Protocol'); + + const isEnabled = + settings.get('Direct_Reply_Enable') && + protocol && + settings.get('Direct_Reply_Host') && + settings.get('Direct_Reply_Port') && + settings.get('Direct_Reply_Username') && + settings.get('Direct_Reply_Password'); + + if (client) { + await client.stop(); + } + + if (!isEnabled) { + logger.info('Email Interceptor Stopped...'); + return; + } + logger.info('Starting Email Interceptor...'); + + if (protocol === 'IMAP') { + client = new DirectReplyIMAPInterceptor(); + client.start(); + } + + if (protocol === 'POP') { + client = new POP3Helper(settings.get('Direct_Reply_Frequency')); + client.start(); + } +}, 1000); + +settings.watchByRegex(/^Direct_Reply_.+/, startEmailInterceptor); + +startEmailInterceptor(); diff --git a/apps/meteor/app/livechat/client/index.js b/apps/meteor/app/livechat/client/index.js index 645d89170532..23da09e08a29 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'; @@ -7,4 +6,3 @@ import './startup/notifyUnreadRooms'; import './views/app/dialog/closeRoom'; import './stylesheets/livechat.css'; import './externalFrame'; -import './lib/messageTypes'; diff --git a/apps/meteor/app/livechat/client/lib/chartHandler.js b/apps/meteor/app/livechat/client/lib/chartHandler.js deleted file mode 100644 index b49d09002fa9..000000000000 --- a/apps/meteor/app/livechat/client/lib/chartHandler.js +++ /dev/null @@ -1,221 +0,0 @@ -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; - -const lineChartConfiguration = ({ legends = false, anim = false, smallTicks = false, displayColors = false, tooltipCallbacks = {} }) => { - const config = { - layout: { - padding: { - top: 10, - bottom: 0, - }, - }, - legend: { - display: false, - }, - title: { - display: false, - }, - tooltips: { - enabled: true, - mode: 'point', - displayColors, - ...tooltipCallbacks, - }, - scales: { - xAxes: [ - { - scaleLabel: { - display: false, - }, - gridLines: { - display: true, - color: 'rgba(0, 0, 0, 0.03)', - }, - }, - ], - yAxes: [ - { - scaleLabel: { - display: false, - }, - gridLines: { - display: true, - color: 'rgba(0, 0, 0, 0.03)', - }, - ticks: { - beginAtZero: true, - }, - }, - ], - }, - hover: { - animationDuration: 0, // duration of animations when hovering an item - }, - responsive: true, - maintainAspectRatio: false, - responsiveAnimationDuration: 0, // animation duration after a resize - }; - - if (!anim) { - config.animation = { - duration: 0, // general animation time - }; - } - - if (legends) { - config.legend = { - display: true, - labels: { - boxWidth: 20, - fontSize: 8, - }, - }; - } - - if (smallTicks) { - config.scales.xAxes[0].ticks = { - fontSize: 8, - }; - - config.scales.yAxes[0].ticks = { - beginAtZero: true, - fontSize: 8, - }; - } - - return config; -}; - -const doughnutChartConfiguration = (title, tooltipCallbacks = {}) => ({ - layout: { - padding: { - top: 0, - bottom: 0, - }, - }, - legend: { - display: true, - position: 'right', - labels: { - boxWidth: 20, - fontSize: 8, - }, - }, - title: { - display: 'true', - text: title, - }, - tooltips: { - enabled: true, - mode: 'point', - displayColors: false, // hide color box - ...tooltipCallbacks, - }, - // animation: { - // duration: 0 // general animation time - // }, - hover: { - animationDuration: 0, // duration of animations when hovering an item - }, - responsive: true, - maintainAspectRatio: false, - responsiveAnimationDuration: 0, // animation duration after a resize -}); - -/** - * - * @param {Object} chart - chart element - * @param {Object} chartContext - Context of chart - * @param {Array(String)} chartLabel - * @param {Array(String)} dataLabels - * @param {Array(Array(Double))} dataPoints - */ -export const drawLineChart = async (chart, chartContext, chartLabels, dataLabels, dataSets, options = {}) => { - if (!chart) { - console.log('No chart element'); - return; - } - if (chartContext) { - chartContext.destroy(); - } - const colors = ['#2de0a5', '#ffd21f', '#f5455c', '#cbced1']; - - const datasets = []; - - chartLabels.forEach(function (chartLabel, index) { - datasets.push({ - label: TAPi18n.__(chartLabel), // chart label - data: dataSets[index], // data points corresponding to data labels, x-axis points - backgroundColor: [colors[index]], - borderColor: [colors[index]], - borderWidth: 3, - fill: false, - }); - }); - const chartImport = await import('chart.js'); - const Chart = chartImport.default; - return new Chart(chart, { - type: 'line', - data: { - labels: dataLabels, // data labels, y-axis points - datasets, - }, - options: lineChartConfiguration(options), - }); -}; - -/** - * - * @param {Object} chart - chart element - * @param {Object} chartContext - Context of chart - * @param {Array(String)} dataLabels - * @param {Array(Double)} dataPoints - */ -export const drawDoughnutChart = async (chart, title, chartContext, dataLabels, dataPoints) => { - if (!chart) { - return; - } - if (chartContext) { - chartContext.destroy(); - } - const chartImport = await import('chart.js'); - const Chart = chartImport.default; - return new Chart(chart, { - type: 'doughnut', - data: { - labels: dataLabels, // data labels, y-axis points - datasets: [ - { - data: dataPoints, // data points corresponding to data labels, x-axis points - backgroundColor: ['#2de0a5', '#cbced1', '#f5455c', '#ffd21f'], - borderWidth: 0, - }, - ], - }, - options: doughnutChartConfiguration(title), - }); -}; - -/** - * Update chart - * @param {Object} chart [Chart context] - * @param {String} label [chart label] - * @param {Array(Double)} data [updated data] - */ -export const updateChart = async (c, label, data) => { - const chart = await c; - if (chart.data.labels.indexOf(label) === -1) { - // insert data - chart.data.labels.push(label); - chart.data.datasets.forEach((dataset, idx) => { - dataset.data.push(data[idx]); - }); - } else { - // update data - const index = chart.data.labels.indexOf(label); - chart.data.datasets.forEach((dataset, idx) => { - dataset.data[index] = data[idx]; - }); - } - - chart.update(); -}; diff --git a/apps/meteor/app/livechat/client/lib/chartHandler.ts b/apps/meteor/app/livechat/client/lib/chartHandler.ts new file mode 100644 index 000000000000..7f9b6f1a6f4f --- /dev/null +++ b/apps/meteor/app/livechat/client/lib/chartHandler.ts @@ -0,0 +1,232 @@ +import type { ChartItem, Chart as ChartType, ChartConfiguration } from 'chart.js'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; + +type LineChartConfigOptions = Partial<{ + legends: boolean; + anim: boolean; + displayColors: boolean; + tooltipCallbacks: any; +}>; + +const lineChartConfiguration = ({ + legends = false, + anim = false, + tooltipCallbacks = {}, +}: LineChartConfigOptions): Partial['options']> => { + const config: ChartConfiguration<'line', number, string>['options'] = { + layout: { + padding: { + top: 10, + bottom: 0, + }, + }, + legend: { + display: false, + }, + plugins: { + tooltip: { + usePointStyle: true, + enabled: true, + mode: 'point', + yAlign: 'bottom', + displayColors: true, + ...tooltipCallbacks, + }, + }, + scales: { + xAxis: { + title: { + display: false, + }, + grid: { + display: true, + color: 'rgba(0, 0, 0, 0.03)', + }, + }, + yAxis: { + title: { + display: false, + }, + grid: { + display: true, + color: 'rgba(0, 0, 0, 0.03)', + }, + }, + }, + hover: { + intersect: false, // duration of animations when hovering an item + mode: 'index', + }, + responsive: true, + maintainAspectRatio: false, + ...(!anim ? { animation: { duration: 0 } } : {}), + ...(legends ? { legend: { display: true, labels: { boxWidth: 20, fontSize: 8 } } } : {}), + }; + + return config; +}; + +const doughnutChartConfiguration = ( + title: string, + tooltipCallbacks = {}, +): Partial['options']> => ({ + layout: { + padding: { + top: 0, + bottom: 0, + }, + }, + plugins: { + legend: { + display: true, + position: 'right', + labels: { + boxWidth: 20, + }, + }, + title: { + display: true, + text: title, + }, + tooltip: { + enabled: true, + mode: 'point', + displayColors: true, // hide color box + ...tooltipCallbacks, + }, + }, + // animation: { + // duration: 0 // general animation time + // }, + hover: { + intersect: true, // duration of animations when hovering an item + }, + responsive: true, + maintainAspectRatio: false, +}); + +type ChartDataSet = { + label: string; + data: number; + backgroundColor: string; + borderColor: string; + borderWidth: number; + fill: boolean; +}; + +/** + * + * @param {Object} chart - chart element + * @param {Object} chartContext - Context of chart + * @param {Array(String)} chartLabel + * @param {Array(String)} dataLabels + * @param {Array(Array(Double))} dataPoints + */ +export const drawLineChart = async ( + chart: HTMLCanvasElement, + chartContext: { destroy: () => void } | undefined, + chartLabels: string[], + dataLabels: string[], + dataSets: number[], + options: LineChartConfigOptions = {}, +): Promise | void> => { + if (!chart) { + console.error('No chart element'); + return; + } + if (chartContext) { + chartContext.destroy(); + } + const colors = ['#2de0a5', '#ffd21f', '#f5455c', '#cbced1']; + + const datasets: ChartDataSet[] = []; + + chartLabels.forEach(function (chartLabel: string, index: number) { + datasets.push({ + label: TAPi18n.__(chartLabel), // chart label + data: dataSets[index], // data points corresponding to data labels, x-axis points + backgroundColor: colors[index], + borderColor: colors[index], + borderWidth: 3, + fill: false, + }); + }); + const chartjs = await import('chart.js/auto'); + const Chart = chartjs.default; + return new Chart(chart, { + type: 'line', + data: { + labels: dataLabels, // data labels, y-axis points + datasets, + }, + options: lineChartConfiguration(options), + }); +}; + +/** + * + * @param {Object} chart - chart element + * @param {Object} chartContext - Context of chart + * @param {Array(String)} dataLabels + * @param {Array(Double)} dataPoints + */ +export const drawDoughnutChart = async ( + chart: ChartItem, + title: string, + chartContext: { destroy: () => void } | undefined, + dataLabels: string[], + dataPoints: number[], +): Promise | void> => { + if (!chart) { + console.error('No chart element'); + return; + } + if (chartContext) { + chartContext.destroy(); + } + const chartjs = await import('chart.js/auto'); + const Chart = chartjs.default; + return new Chart(chart, { + type: 'doughnut', + data: { + labels: dataLabels, // data labels, y-axis points + datasets: [ + { + data: dataPoints, // data points corresponding to data labels, x-axis points + backgroundColor: ['#2de0a5', '#cbced1', '#f5455c', '#ffd21f'], + borderWidth: 0, + }, + ], + }, + options: doughnutChartConfiguration(title), + }); +}; + +/** + * Update chart + * @param {Object} chart [Chart context] + * @param {String} label [chart label] + * @param {Array(Double)} data [updated data] + */ +export const updateChart = async (c: ChartType, label: string, data: { [x: string]: number }): Promise => { + const chart = await c; + if (chart.data?.labels?.indexOf(label) === -1) { + // insert data + chart.data.labels.push(label); + chart.data.datasets.forEach((dataset: { data: any[] }, idx: string | number) => { + dataset.data.push(data[idx]); + }); + } else { + // update data + const index = chart.data?.labels?.indexOf(label); + if (typeof index === 'undefined') { + return; + } + + chart.data.datasets.forEach((dataset: { data: { [x: string]: any } }, idx: string | number) => { + dataset.data[index] = data[idx]; + }); + } + + chart.update(); +}; diff --git a/apps/meteor/app/livechat/client/lib/messageTypes.js b/apps/meteor/app/livechat/client/lib/messageTypes.js deleted file mode 100644 index 9cf21362d9e1..000000000000 --- a/apps/meteor/app/livechat/client/lib/messageTypes.js +++ /dev/null @@ -1,5 +0,0 @@ -import { actionLinks } from '../../../action-links/client'; - -actionLinks.register('createLivechatCall', function (message, params, instance) { - instance.tabBar.open('video'); -}); diff --git a/apps/meteor/app/livechat/client/lib/stream/queueManager.js b/apps/meteor/app/livechat/client/lib/stream/queueManager.js index f76e5679d279..10bdfe1d5820 100644 --- a/apps/meteor/app/livechat/client/lib/stream/queueManager.js +++ b/apps/meteor/app/livechat/client/lib/stream/queueManager.js @@ -45,7 +45,7 @@ const updateCollection = (inquiry) => { }; const getInquiriesFromAPI = async () => { - const { inquiries } = await APIClient.v1.get('livechat/inquiries.queuedForUser?sort={"ts": 1}'); + const { inquiries } = await APIClient.get('/v1/livechat/inquiries.queuedForUser?sort={"ts": 1}'); return inquiries; }; @@ -68,7 +68,7 @@ const updateInquiries = async (inquiries = []) => inquiries.forEach((inquiry) => LivechatInquiry.upsert({ _id: inquiry._id }, { ...inquiry, _updatedAt: new Date(inquiry._updatedAt) })); const getAgentsDepartments = async (userId) => { - const { departments } = await APIClient.v1.get(`livechat/agents/${userId}/departments?enabledDepartmentsOnly=true`); + const { departments } = await APIClient.get(`/v1/livechat/agents/${userId}/departments`, { enabledDepartmentsOnly: true }); return departments; }; 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/client/startup/notifyUnreadRooms.js b/apps/meteor/app/livechat/client/startup/notifyUnreadRooms.js index 0449754bbd18..8fe8c82ee95c 100644 --- a/apps/meteor/app/livechat/client/startup/notifyUnreadRooms.js +++ b/apps/meteor/app/livechat/client/startup/notifyUnreadRooms.js @@ -3,7 +3,7 @@ import { Tracker } from 'meteor/tracker'; import { settings } from '../../../settings'; import { getUserPreference } from '../../../utils'; -import { Subscriptions, Users } from '../../../models'; +import { Subscriptions, Users } from '../../../models/client'; import { CustomSounds } from '../../../custom-sounds/client'; let audio = null; diff --git a/apps/meteor/app/livechat/client/views/app/dialog/closeRoom.js b/apps/meteor/app/livechat/client/views/app/dialog/closeRoom.js index 3db0d930cfc9..767bd0168d75 100644 --- a/apps/meteor/app/livechat/client/views/app/dialog/closeRoom.js +++ b/apps/meteor/app/livechat/client/views/app/dialog/closeRoom.js @@ -167,16 +167,16 @@ Template.closeRoom.onCreated(async function () { this.onEnterTag = () => this.invalidTags.set(!validateRoomTags(this.tagsRequired.get(), this.tags.get())); const { rid } = Template.currentData(); - const { room } = await APIClient.v1.get(`rooms.info?roomId=${rid}`); + const { room } = await APIClient.get(`/v1/rooms.info`, { roomId: rid }); this.tags.set(room?.tags || []); if (room?.departmentId) { - const { department } = await APIClient.v1.get(`livechat/department/${room.departmentId}?includeAgents=false`); + const { department } = await APIClient.get(`/v1/livechat/department/${room.departmentId}`, { includeAgents: false }); this.tagsRequired.set(department?.requestTagBeforeClosingChat); } const uid = Meteor.userId(); - const { departments } = await APIClient.v1.get(`livechat/agents/${uid}/departments`); + const { departments } = await APIClient.get(`/v1/livechat/agents/${uid}/departments`); const agentDepartments = departments.map((dept) => dept.departmentId); this.agentDepartments.set(agentDepartments); diff --git a/apps/meteor/app/livechat/client/views/app/livechatReadOnly.js b/apps/meteor/app/livechat/client/views/app/livechatReadOnly.js index fa38742ca152..aee6b90f7ef4 100644 --- a/apps/meteor/app/livechat/client/views/app/livechatReadOnly.js +++ b/apps/meteor/app/livechat/client/views/app/livechatReadOnly.js @@ -3,7 +3,7 @@ import { Template } from 'meteor/templating'; import { ReactiveVar } from 'meteor/reactive-var'; import { FlowRouter } from 'meteor/kadira:flow-router'; -import { ChatRoom, CachedChatRoom } from '../../../../models'; +import { ChatRoom, CachedChatRoom } from '../../../../models/client'; import { callWithErrorHandling } from '../../../../../client/lib/utils/callWithErrorHandling'; import './livechatReadOnly.html'; import { APIClient } from '../../../../utils/client'; @@ -61,7 +61,7 @@ Template.livechatReadOnly.events({ event.stopPropagation(); try { - const { success } = (await APIClient.v1.get(`livechat/room.join?roomId=${this.rid}`)) || {}; + const { success } = (await APIClient.get(`/v1/livechat/room.join`, { roomId: this.rid })) || {}; if (!success) { throw new Meteor.Error('error-join-room', 'Error joining room'); } @@ -99,13 +99,13 @@ Template.livechatReadOnly.onCreated(async function () { this.loadRoomAndInquiry = async (roomId) => { this.preparing.set(true); - const { inquiry } = await APIClient.v1.get(`livechat/inquiries.getOne?roomId=${roomId}`); + const { inquiry } = await APIClient.get(`/v1/livechat/inquiries.getOne`, { roomId }); this.inquiry.set(inquiry); if (inquiry && inquiry._id) { inquiryDataStream.on(inquiry._id, this.updateInquiry); } - const { room } = await APIClient.v1.get(`rooms.info?roomId=${roomId}`); + const { room } = await APIClient.get(`/v1/rooms.info`, { roomId }); this.room.set(room); if (room && room._id) { RoomManager.roomStream.on(roomId, (room) => this.room.set(room)); diff --git a/apps/meteor/app/livechat/client/views/app/tabbar/agentEdit.js b/apps/meteor/app/livechat/client/views/app/tabbar/agentEdit.js index ff14df8f7832..c1fef6d7fd76 100644 --- a/apps/meteor/app/livechat/client/views/app/tabbar/agentEdit.js +++ b/apps/meteor/app/livechat/client/views/app/tabbar/agentEdit.js @@ -133,7 +133,7 @@ Template.agentEdit.onCreated(async function () { this.availableDepartments = new ReactiveVar([]); this.back = Template.currentData().back; - const { departments } = await APIClient.v1.get('livechat/department?sort={"name": 1}'); + const { departments } = await APIClient.get('/v1/livechat/department?sort={"name": 1}'); this.departments.set(departments); this.availableDepartments.set(departments.filter(({ enabled }) => enabled)); @@ -146,8 +146,8 @@ Template.agentEdit.onCreated(async function () { return; } - const { user } = await APIClient.v1.get(`livechat/users/agent/${agentId}`); - const { departments } = await APIClient.v1.get(`livechat/agents/${agentId}/departments`); + const { user } = await APIClient.get(`/v1/livechat/users/agent/${agentId}`); + const { departments } = await APIClient.get(`/v1/livechat/agents/${agentId}/departments`); this.agent.set(user); this.agentDepartments.set((departments || []).map((department) => department.departmentId)); this.ready.set(true); diff --git a/apps/meteor/app/livechat/client/views/app/tabbar/agentInfo.js b/apps/meteor/app/livechat/client/views/app/tabbar/agentInfo.js index af15c9ba3f7e..725a2ec0dfea 100644 --- a/apps/meteor/app/livechat/client/views/app/tabbar/agentInfo.js +++ b/apps/meteor/app/livechat/client/views/app/tabbar/agentInfo.js @@ -153,14 +153,14 @@ Template.agentInfo.onCreated(async function () { this.tabBar = Template.currentData().tabBar; this.onRemoveAgent = Template.currentData().onRemoveAgent; - const { departments } = await APIClient.v1.get('livechat/department?sort={"name": 1}'); + const { departments } = await APIClient.get('/v1/livechat/department?sort={"name": 1}'); this.departments.set(departments); this.availableDepartments.set(departments.filter(({ enabled }) => enabled)); const loadAgentData = async (agentId) => { this.ready.set(false); - const { user } = await APIClient.v1.get(`livechat/users/agent/${agentId}`); - const { departments } = await APIClient.v1.get(`livechat/agents/${agentId}/departments`); + const { user } = await APIClient.get(`/v1/livechat/users/agent/${agentId}`); + const { departments } = await APIClient.get(`/v1/livechat/agents/${agentId}/departments`); this.agent.set(user); this.agentDepartments.set((departments || []).map((department) => department.departmentId)); this.ready.set(true); diff --git a/apps/meteor/app/livechat/client/views/app/tabbar/contactChatHistory.js b/apps/meteor/app/livechat/client/views/app/tabbar/contactChatHistory.js index 9dcd6d690ba1..690402564a14 100644 --- a/apps/meteor/app/livechat/client/views/app/tabbar/contactChatHistory.js +++ b/apps/meteor/app/livechat/client/views/app/tabbar/contactChatHistory.js @@ -70,22 +70,24 @@ Template.contactChatHistory.onCreated(async function () { const offset = this.offset.get(); const searchTerm = this.searchTerm.get(); - let baseUrl = `livechat/visitors.searchChats/room/${ - currentData.rid - }/visitor/${this.visitorId.get()}?count=${limit}&offset=${offset}&closedChatsOnly=true&servedChatsOnly=true`; - if (searchTerm) { - baseUrl += `&searchText=${searchTerm}`; - } + const baseUrl = `/v1/livechat/visitors.searchChats/room/${currentData.rid}/visitor/${this.visitorId.get()}`; + const params = { + count: limit, + offset, + closedChatsOnly: true, + servedChatsOnly: true, + ...(searchTerm && { searchText: searchTerm }), + }; this.isLoading.set(true); - const { history, total } = await APIClient.v1.get(baseUrl); + const { history, total } = await APIClient.get(baseUrl, params); this.history.set(offset === 0 ? history : this.history.get().concat(history)); this.hasMore.set(total > this.history.get().length); this.isLoading.set(false); }); this.autorun(async () => { - const { room } = await APIClient.v1.get(`rooms.info?roomId=${currentData.rid}`); + const { room } = await APIClient.get(`/v1/rooms.info`, { roomId: currentData.rid }); if (room?.v) { this.visitorId.set(room.v._id); } diff --git a/apps/meteor/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.js b/apps/meteor/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.js index 2b668732fc5c..f00adaf28354 100644 --- a/apps/meteor/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.js +++ b/apps/meteor/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.js @@ -81,12 +81,12 @@ Template.contactChatHistoryMessages.onCreated(function () { this.hasError = new ReactiveVar(false); this.error = new ReactiveVar(null); - this.loadMessages = async (url) => { + this.loadMessages = async (url, params) => { this.isLoading.set(true); const offset = this.offset.get(); try { - const { messages, total } = await APIClient.v1.get(url); + const { messages, total } = await APIClient.get(url, params); this.messages.set(offset === 0 ? messages : this.messages.get().concat(messages)); this.hasMore.set(total > this.messages.get().length); } catch (e) { @@ -103,10 +103,20 @@ Template.contactChatHistoryMessages.onCreated(function () { const searchTerm = this.searchTerm.get(); if (searchTerm !== '') { - return this.loadMessages(`chat.search/?roomId=${this.rid}&searchText=${searchTerm}&count=${limit}&offset=${offset}&sort={"ts": 1}`); + return this.loadMessages('/v1/chat.search', { + roomId: this.rid, + searchText: searchTerm, + count: limit, + offset, + sort: '{"ts": 1}', + }); } - this.loadMessages(`livechat/${this.rid}/messages?count=${limit}&offset=${offset}&sort={"ts": 1}`); + this.loadMessages(`/v1/livechat/${this.rid}/messages`, { + count: limit, + offset, + sort: '{"ts": 1}', + }); }); this.autorun(() => { diff --git a/apps/meteor/app/livechat/client/views/app/tabbar/visitorEdit.js b/apps/meteor/app/livechat/client/views/app/tabbar/visitorEdit.js index ce70d5e979b8..46f4f0cc2af4 100644 --- a/apps/meteor/app/livechat/client/views/app/tabbar/visitorEdit.js +++ b/apps/meteor/app/livechat/client/views/app/tabbar/visitorEdit.js @@ -114,7 +114,7 @@ Template.visitorEdit.onCreated(async function () { this.autorun(async () => { const { visitorId } = Template.currentData(); if (visitorId) { - const { visitor } = await APIClient.v1.get(`livechat/visitors.info?visitorId=${visitorId}`); + const { visitor } = await APIClient.get('/v1/livechat/visitors.info', { visitorId }); this.visitor.set(visitor); } }); @@ -122,15 +122,15 @@ Template.visitorEdit.onCreated(async function () { const rid = Template.currentData().roomId; this.autorun(async () => { - const { room } = await APIClient.v1.get(`rooms.info?roomId=${rid}`); - const { customFields } = await APIClient.v1.get(`livechat/custom-fields?count=${CUSTOM_FIELDS_COUNT}`); + const { room } = await APIClient.get('/v1/rooms.info', { roomId: rid }); + const { customFields } = await APIClient.get('/v1/livechat/custom-fields', { count: CUSTOM_FIELDS_COUNT }); this.room.set(room); this.tags.set((room && room.tags) || []); this.customFields.set(customFields || []); }); const uid = Meteor.userId(); - const { departments } = await APIClient.v1.get(`livechat/agents/${uid}/departments`); + const { departments } = await APIClient.get(`/v1/livechat/agents/${uid}/departments`); const agentDepartments = departments.map((dept) => dept.departmentId); this.agentDepartments.set(agentDepartments); Meteor.call('livechat:getTagsList', (err, tagsList) => { diff --git a/apps/meteor/app/livechat/client/views/app/tabbar/visitorForward.html b/apps/meteor/app/livechat/client/views/app/tabbar/visitorForward.html index 45c909a10ded..82ce65ae4706 100644 --- a/apps/meteor/app/livechat/client/views/app/tabbar/visitorForward.html +++ b/apps/meteor/app/livechat/client/views/app/tabbar/visitorForward.html @@ -3,34 +3,20 @@

{{_ "Forward_chat"}}

{{#with visitor}} -
- - {{username}} -
+
+ + {{username}} +
{{/with}}
- {{> livechatAutocompleteUser - onClickTag=onClickTagDepartment - list=selectedDepartments - onSelect=onSelectDepartments - collection='CachedDepartmentList' - endpoint='livechat/department.autocomplete' - field='name' - sort='name' - icon="queue" - label="Enter_a_department_name" - placeholder="Enter_a_department_name" - name="department" - noMatchTemplate="userSearchEmpty" - templateItem="popupList_item_channel" - template="roomSearch" - noMatchTemplate="roomSearchEmpty" - modifier=departmentModifier - conditions=departmentConditions - }} + {{> livechatAutocompleteUser onClickTag=onClickTagDepartment list=selectedDepartments onSelect=onSelectDepartments + collection='CachedDepartmentList' endpoint='livechat/department.autocomplete' field='name' sort='name' icon="queue" + label="Enter_a_department_name" placeholder="Enter_a_department_name" name="department" noMatchTemplate="userSearchEmpty" + templateItem="popupList_item_channel" template="roomSearch" noMatchTemplate="roomSearchEmpty" modifier=departmentModifier + conditions=departmentConditions }}
@@ -40,23 +26,10 @@

{{_ "Forward_chat"}}

- {{> livechatAutocompleteUser - onClickTag=onClickTagAgent - list=selectedAgents - onSelect=onSelectAgents - collection='UserAndRoom' - endpoint='users.autocomplete' - field='username' - sort='username' - label="Select_a_user" - placeholder="Select_a_user" - name="agent" - icon="at" - noMatchTemplate="userSearchEmpty" - templateItem="popupList_item_default" - modifier=agentModifier - conditions=agentConditions - }} + {{> livechatAutocompleteUser onClickTag=onClickTagAgent list=selectedAgents onSelect=onSelectAgents collection='UserAndRoom' + endpoint='/v1/users.autocomplete' field='username' sort='username' label="Select_a_user" placeholder="Select_a_user" + name="agent" icon="at" noMatchTemplate="userSearchEmpty" templateItem="popupList_item_default" modifier=agentModifier + conditions=agentConditions }}
@@ -66,8 +39,8 @@

{{_ "Forward_chat"}}

diff --git a/apps/meteor/app/livechat/client/views/app/tabbar/visitorForward.js b/apps/meteor/app/livechat/client/views/app/tabbar/visitorForward.js index b591ed84e52d..2c58cc34bf04 100644 --- a/apps/meteor/app/livechat/client/views/app/tabbar/visitorForward.js +++ b/apps/meteor/app/livechat/client/views/app/tabbar/visitorForward.js @@ -3,7 +3,7 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { Template } from 'meteor/templating'; -import { ChatRoom } from '../../../../../models'; +import { ChatRoom } from '../../../../../models/client'; import { t } from '../../../../../utils'; import './visitorForward.html'; import { APIClient } from '../../../../../utils/client'; @@ -99,7 +99,7 @@ Template.visitorForward.onCreated(async function () { } }); - const { departments } = await APIClient.v1.get('livechat/department?enabled=true'); + const { departments } = await APIClient.get('/v1/livechat/department', { enabled: true }); this.departments.set(departments); }); diff --git a/apps/meteor/app/livechat/client/views/app/tabbar/visitorInfo.js b/apps/meteor/app/livechat/client/views/app/tabbar/visitorInfo.js index 4a5d71e94c14..c755006acff8 100644 --- a/apps/meteor/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/apps/meteor/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -9,7 +9,7 @@ import moment from 'moment'; import UAParser from 'ua-parser-js'; import { modal } from '../../../../../ui-utils'; -import { Subscriptions } from '../../../../../models'; +import { Subscriptions } from '../../../../../models/client'; import { settings } from '../../../../../settings'; import { t } from '../../../../../utils'; import { hasRole, hasPermission, hasAtLeastOnePermission } from '../../../../../authorization'; @@ -355,7 +355,7 @@ Template.visitorInfo.events({ confirmButtonText: t('Yes'), }, async () => { - const { success } = await APIClient.v1.post('livechat/room.onHold', { roomId: this.rid }); + const { success } = await APIClient.post('/v1/livechat/room.onHold', { roomId: this.rid }); if (success) { modal.open({ title: t('Chat_On_Hold'), @@ -382,7 +382,7 @@ Template.visitorInfo.onCreated(function () { this.room = new ReactiveVar({}); this.updateVisitor = async (visitorId) => { - const { visitor } = await APIClient.v1.get(`livechat/visitors.info?visitorId=${visitorId}`); + const { visitor } = await APIClient.get('/v1/livechat/visitors.info', { visitorId }); this.user.set(visitor); }; @@ -409,7 +409,7 @@ Template.visitorInfo.onCreated(function () { }); const loadRoomData = async (rid) => { - const { room } = await APIClient.v1.get(`rooms.info?roomId=${rid}`); + const { room } = await APIClient.get('/v1/rooms.info', { roomId: rid }); this.updateRoom(room); }; @@ -420,7 +420,7 @@ Template.visitorInfo.onCreated(function () { this.autorun(async () => { if (this.departmentId.get()) { - const { department } = await APIClient.v1.get(`livechat/department/${this.departmentId.get()}?includeAgents=false`); + const { department } = await APIClient.get(`/v1/livechat/department/${this.departmentId.get()}`, { includeAgents: false }); this.department.set(department); } }); diff --git a/apps/meteor/app/livechat/client/views/app/tabbar/visitorNavigation.js b/apps/meteor/app/livechat/client/views/app/tabbar/visitorNavigation.js index 3142aa92a423..2c290f267aa1 100644 --- a/apps/meteor/app/livechat/client/views/app/tabbar/visitorNavigation.js +++ b/apps/meteor/app/livechat/client/views/app/tabbar/visitorNavigation.js @@ -3,7 +3,7 @@ import { Template } from 'meteor/templating'; import moment from 'moment'; import _ from 'underscore'; -import { ChatRoom } from '../../../../../models'; +import { ChatRoom } from '../../../../../models/client'; import { t } from '../../../../../utils'; import './visitorNavigation.html'; import { APIClient } from '../../../../../utils/client'; @@ -65,9 +65,10 @@ Template.visitorNavigation.onCreated(async function () { this.isLoading.set(true); const offset = this.offset.get(); if (currentData && currentData.rid) { - const { pages, total } = await APIClient.v1.get( - `livechat/visitors.pagesVisited/${currentData.rid}?count=${ITEMS_COUNT}&offset=${offset}`, - ); + const { pages, total } = await APIClient.get(`/v1/livechat/visitors.pagesVisited/${currentData.rid}`, { + count: ITEMS_COUNT, + offset, + }); this.isLoading.set(false); this.total.set(total); this.pages.set(this.pages.get().concat(pages)); diff --git a/apps/meteor/app/livechat/client/views/app/tabbar/visitorTranscript.js b/apps/meteor/app/livechat/client/views/app/tabbar/visitorTranscript.js index de70f61347f2..94f107319e9c 100644 --- a/apps/meteor/app/livechat/client/views/app/tabbar/visitorTranscript.js +++ b/apps/meteor/app/livechat/client/views/app/tabbar/visitorTranscript.js @@ -169,12 +169,12 @@ Template.visitorTranscript.onCreated(async function () { this.infoMessage = new ReactiveVar(''); this.autorun(async () => { - const { visitor } = await APIClient.v1.get(`livechat/visitors.info?visitorId=${Template.currentData().visitorId}`); + const { visitor } = await APIClient.get('/v1/livechat/visitors.info', { visitorId: Template.currentData().visitorId }); this.visitor.set(visitor); }); this.autorun(async () => { - const { room } = await APIClient.v1.get(`rooms.info?roomId=${Template.currentData().roomId}`); + const { room } = await APIClient.get('/v1/rooms.info', { roomId: Template.currentData().roomId }); this.room.set(room); if (room?.transcriptRequest) { diff --git a/apps/meteor/app/livechat/imports/server/rest/dashboards.js b/apps/meteor/app/livechat/imports/server/rest/dashboards.js index f60ef581d42a..84ebf4f9dd04 100644 --- a/apps/meteor/app/livechat/imports/server/rest/dashboards.js +++ b/apps/meteor/app/livechat/imports/server/rest/dashboards.js @@ -13,7 +13,7 @@ import { getAgentsProductivityMetrics, getChatsMetrics, } from '../../../server/lib/analytics/dashboards'; -import { Users } from '../../../../models'; +import { Users } from '../../../../models/server'; API.v1.addRoute( 'livechat/analytics/dashboards/conversation-totalizers', diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index ab8ee8a66e42..ce0d9e83143e 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -1,7 +1,8 @@ +import { isLivechatDepartmentProps } from '@rocket.chat/rest-typings'; import { Match, check } from 'meteor/check'; import { API } from '../../../../api/server'; -import { hasPermission } from '../../../../authorization/server'; +import { hasPermission, hasAtLeastOnePermission } from '../../../../authorization/server'; import { LivechatDepartment, LivechatDepartmentAgents } from '../../../../models/server'; import { Livechat } from '../../../server/lib/Livechat'; import { @@ -14,9 +15,13 @@ import { API.v1.addRoute( 'livechat/department', - { authRequired: true }, + { authRequired: true, validateParams: isLivechatDepartmentProps }, { - get() { + async get() { + if (!hasAtLeastOnePermission(this.userId, ['view-livechat-departments', 'view-l-room'])) { + return API.v1.unauthorized(); + } + const { offset, count } = this.getPaginationItems(); const { sort } = this.parseJsonQuery(); @@ -26,7 +31,7 @@ API.v1.addRoute( findDepartments({ userId: this.userId, text, - enabled, + enabled: enabled === 'true', onlyMyDepartments: onlyMyDepartments === 'true', excludeDepartmentId, pagination: { @@ -75,6 +80,10 @@ API.v1.addRoute( { authRequired: true }, { async get() { + if (!hasAtLeastOnePermission(this.userId, ['view-livechat-departments', 'view-l-room'])) { + return API.v1.unauthorized(); + } + check(this.urlParams, { _id: String, }); diff --git a/apps/meteor/app/livechat/imports/server/rest/facebook.js b/apps/meteor/app/livechat/imports/server/rest/facebook.js index 342d99d96bf4..db8f74ea264c 100644 --- a/apps/meteor/app/livechat/imports/server/rest/facebook.js +++ b/apps/meteor/app/livechat/imports/server/rest/facebook.js @@ -1,10 +1,11 @@ import crypto from 'crypto'; import { Random } from 'meteor/random'; +import { LivechatVisitors } from '@rocket.chat/models'; import { API } from '../../../../api/server'; -import { LivechatRooms, LivechatVisitors } from '../../../../models'; -import { settings } from '../../../../settings'; +import { LivechatRooms } from '../../../../models/server'; +import { settings } from '../../../../settings/server'; import { Livechat } from '../../../server/lib/Livechat'; /** @@ -21,7 +22,7 @@ import { Livechat } from '../../../server/lib/Livechat'; * @apiParam {String} [attachments] Facebook message attachments */ API.v1.addRoute('livechat/facebook', { - post() { + async post() { if (!this.bodyParams.text && !this.bodyParams.attachments) { return { success: false, @@ -63,7 +64,7 @@ API.v1.addRoute('livechat/facebook', { }, }, }; - let visitor = LivechatVisitors.getVisitorByToken(this.bodyParams.token); + let visitor = await LivechatVisitors.getVisitorByToken(this.bodyParams.token); if (visitor) { const rooms = LivechatRooms.findOpenByVisitorToken(visitor.token).fetch(); if (rooms && rooms.length > 0) { @@ -76,12 +77,12 @@ API.v1.addRoute('livechat/facebook', { sendMessage.message.rid = Random.id(); sendMessage.message.token = this.bodyParams.token; - const userId = Livechat.registerGuest({ + const userId = await Livechat.registerGuest({ token: sendMessage.message.token, name: `${this.bodyParams.first_name} ${this.bodyParams.last_name}`, }); - visitor = LivechatVisitors.findOneById(userId); + visitor = await LivechatVisitors.findOneById(userId); } sendMessage.message.msg = this.bodyParams.text; diff --git a/apps/meteor/app/livechat/imports/server/rest/inquiries.js b/apps/meteor/app/livechat/imports/server/rest/inquiries.js index 72b077f556a3..7615bec8098b 100644 --- a/apps/meteor/app/livechat/imports/server/rest/inquiries.js +++ b/apps/meteor/app/livechat/imports/server/rest/inquiries.js @@ -1,17 +1,18 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { LivechatInquiryStatus } from '@rocket.chat/core-typings'; +import { LivechatInquiry } from '@rocket.chat/models'; import { API } from '../../../../api/server'; import { hasPermission } from '../../../../authorization'; -import { Users, LivechatDepartment, LivechatInquiry } from '../../../../models'; +import { Users, LivechatDepartment } from '../../../../models/server'; import { findInquiries, findOneInquiryByRoomId } from '../../../server/api/lib/inquiries'; API.v1.addRoute( 'livechat/inquiries.list', { authRequired: true }, { - get() { + async get() { if (!hasPermission(this.userId, 'view-livechat-manager')) { return API.v1.unauthorized(); } @@ -25,11 +26,11 @@ API.v1.addRoute( ourQuery.department = departmentFromDB._id; } } - const cursor = LivechatInquiry.find(ourQuery, { + const { cursor, totalCount } = LivechatInquiry.findPaginated(ourQuery, { sort: sort || { ts: -1 }, skip: offset, limit: count, - fields: { + projection: { rid: 1, name: 1, ts: 1, @@ -37,14 +38,14 @@ API.v1.addRoute( department: 1, }, }); - const totalCount = cursor.count(); - const inquiries = cursor.fetch(); + + const [inquiries, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ inquiries, offset, count: inquiries.length, - total: totalCount, + total, }); }, }, diff --git a/apps/meteor/app/livechat/imports/server/rest/officeHour.js b/apps/meteor/app/livechat/imports/server/rest/officeHour.js deleted file mode 100644 index 1dcb2827f63b..000000000000 --- a/apps/meteor/app/livechat/imports/server/rest/officeHour.js +++ /dev/null @@ -1,21 +0,0 @@ -import { API } from '../../../../api/server'; -import { findLivechatOfficeHours } from '../../../server/api/lib/officeHour'; - -API.v1.addRoute( - 'livechat/office-hours', - { authRequired: true }, - { - get() { - const { officeHours } = Promise.await(findLivechatOfficeHours({ userId: this.userId })); - return API.v1.success( - this.deprecationWarning({ - endpoint: 'livechat/office-hours', - versionWillBeRemoved: '4.0.0', - response: { - officeHours, - }, - }), - ); - }, - }, -); diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.js b/apps/meteor/app/livechat/imports/server/rest/sms.js index 3447870082b8..1756325757df 100644 --- a/apps/meteor/app/livechat/imports/server/rest/sms.js +++ b/apps/meteor/app/livechat/imports/server/rest/sms.js @@ -1,9 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { OmnichannelSourceType } from '@rocket.chat/core-typings'; +import { LivechatVisitors } from '@rocket.chat/models'; import { FileUpload } from '../../../../file-upload/server'; -import { LivechatRooms, LivechatVisitors, LivechatDepartment } from '../../../../models/server'; +import { LivechatRooms, LivechatDepartment } from '../../../../models/server'; import { API } from '../../../../api/server'; import { fetch } from '../../../../../server/lib/http/fetch'; import { SMS } from '../../../../sms'; @@ -34,7 +35,7 @@ const defineDepartment = (idOrName) => { return department && department._id; }; -const defineVisitor = (smsNumber, targetDepartment) => { +const defineVisitor = async (smsNumber, targetDepartment) => { const visitor = LivechatVisitors.findOneVisitorByPhone(smsNumber); let data = { token: (visitor && visitor.token) || Random.id(), @@ -53,7 +54,7 @@ const defineVisitor = (smsNumber, targetDepartment) => { data.department = targetDepartment; } - const id = Livechat.registerGuest(data); + const id = await Livechat.registerGuest(data); return LivechatVisitors.findOneById(id); }; @@ -79,7 +80,7 @@ API.v1.addRoute('livechat/sms-incoming/:service', { targetDepartment = defineDepartment(SMS.department); } - const visitor = defineVisitor(sms.from, targetDepartment); + const visitor = await defineVisitor(sms.from, targetDepartment); const { token } = visitor; const room = LivechatRooms.findOneOpenByVisitorTokenAndDepartmentId(token, targetDepartment); const roomExists = !!room; diff --git a/apps/meteor/app/livechat/imports/server/rest/upload.js b/apps/meteor/app/livechat/imports/server/rest/upload.js index 8547b94645f1..5057c5e7e006 100644 --- a/apps/meteor/app/livechat/imports/server/rest/upload.js +++ b/apps/meteor/app/livechat/imports/server/rest/upload.js @@ -1,8 +1,9 @@ import { Meteor } from 'meteor/meteor'; import filesize from 'filesize'; +import { LivechatVisitors } from '@rocket.chat/models'; import { settings } from '../../../../settings/server'; -import { Settings, LivechatRooms, LivechatVisitors } from '../../../../models'; +import { Settings, LivechatRooms } from '../../../../models/server'; import { fileUploadIsValidContentType } from '../../../../utils/server'; import { FileUpload } from '../../../../file-upload'; import { API } from '../../../../api/server'; @@ -19,13 +20,13 @@ settings.watch('FileUpload_MaxFileSize', function (value) { }); API.v1.addRoute('livechat/upload/:rid', { - post() { + async post() { if (!this.request.headers['x-visitor-token']) { return API.v1.unauthorized(); } const visitorToken = this.request.headers['x-visitor-token']; - const visitor = LivechatVisitors.getVisitorByToken(visitorToken); + const visitor = await LivechatVisitors.getVisitorByToken(visitorToken); if (!visitor) { return API.v1.unauthorized(); @@ -36,10 +37,11 @@ API.v1.addRoute('livechat/upload/:rid', { return API.v1.unauthorized(); } - const { file, ...fields } = Promise.await( - getUploadFormData({ + const [file, fields] = await getUploadFormData( + { request: this.request, - }), + }, + { field: 'file' }, ); if (!fileUploadIsValidContentType(file.mimetype)) { diff --git a/apps/meteor/app/livechat/imports/server/rest/users.js b/apps/meteor/app/livechat/imports/server/rest/users.js index 0118d8fc6551..11e3b15ac6f8 100644 --- a/apps/meteor/app/livechat/imports/server/rest/users.js +++ b/apps/meteor/app/livechat/imports/server/rest/users.js @@ -3,7 +3,7 @@ import _ from 'underscore'; import { hasPermission } from '../../../../authorization'; import { API } from '../../../../api/server'; -import { Users } from '../../../../models'; +import { Users } from '../../../../models/server'; import { Livechat } from '../../../server/lib/Livechat'; import { findAgents, findManagers } from '../../../server/api/lib/users'; diff --git a/apps/meteor/app/livechat/imports/server/rest/visitors.js b/apps/meteor/app/livechat/imports/server/rest/visitors.js index ff1490cd5847..e9e6ad3eea45 100644 --- a/apps/meteor/app/livechat/imports/server/rest/visitors.js +++ b/apps/meteor/app/livechat/imports/server/rest/visitors.js @@ -1,3 +1,4 @@ +import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Match, check } from 'meteor/check'; import { API } from '../../../../api/server'; @@ -142,7 +143,7 @@ API.v1.addRoute( 'livechat/visitors.search', { authRequired: true }, { - get() { + async get() { const { term } = this.requestParams(); check(term, Match.Maybe(String)); @@ -150,18 +151,19 @@ API.v1.addRoute( const { offset, count } = this.getPaginationItems(); const { sort } = this.parseJsonQuery(); + const nameOrUsername = new RegExp(escapeRegExp(term), 'i'); + return API.v1.success( - Promise.await( - findVisitorsByEmailOrPhoneOrNameOrUsername({ - userId: this.userId, - term, - pagination: { - offset, - count, - sort, - }, - }), - ), + await findVisitorsByEmailOrPhoneOrNameOrUsername({ + userId: this.userId, + emailOrPhone: term, + nameOrUsername, + pagination: { + offset, + count, + sort, + }, + }), ); }, }, diff --git a/apps/meteor/app/livechat/imports/server/rest/visitors.ts b/apps/meteor/app/livechat/imports/server/rest/visitors.ts index 1c76ff876ac7..134286d700c4 100644 --- a/apps/meteor/app/livechat/imports/server/rest/visitors.ts +++ b/apps/meteor/app/livechat/imports/server/rest/visitors.ts @@ -1,9 +1,8 @@ import { check } from 'meteor/check'; -import type { IMessage } from '@rocket.chat/core-typings'; +import { Messages } from '@rocket.chat/models'; import { API } from '../../../../api/server'; import { LivechatRooms } from '../../../../models/server'; -import { Messages } from '../../../../models/server/raw'; import { normalizeMessagesForUser } from '../../../../utils/server/lib/normalizeMessagesForUser'; import { canAccessRoom } from '../../../../authorization/server'; @@ -29,15 +28,13 @@ API.v1.addRoute( throw new Error('not-allowed'); } - const cursor = Messages.findLivechatClosedMessages(this.urlParams.rid, { + const { cursor, totalCount } = Messages.findLivechatClosedMessages(this.urlParams.rid, { sort: sort || { ts: -1 }, skip: offset, limit: count, }); - const total = await cursor.count(); - - const messages = (await cursor.toArray()) as IMessage[]; + const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ messages: normalizeMessagesForUser(messages, this.userId), diff --git a/apps/meteor/app/livechat/lib/Assets.js b/apps/meteor/app/livechat/lib/Assets.js deleted file mode 100644 index 32546c6003be..000000000000 --- a/apps/meteor/app/livechat/lib/Assets.js +++ /dev/null @@ -1,2 +0,0 @@ -export const addServerUrlToIndex = (file) => - file.replace('', ``); diff --git a/apps/meteor/app/livechat/lib/Assets.ts b/apps/meteor/app/livechat/lib/Assets.ts new file mode 100644 index 000000000000..c812cd22a9b3 --- /dev/null +++ b/apps/meteor/app/livechat/lib/Assets.ts @@ -0,0 +1,4 @@ +export const addServerUrlToIndex = (file: string): string => { + const rootUrl = (global as any).__meteor_runtime_config__.ROOT_URL.replace(/\/$/, ''); + return file.replace('', ``); +}; diff --git a/apps/meteor/app/livechat/lib/messageTypes.js b/apps/meteor/app/livechat/lib/messageTypes.js deleted file mode 100644 index 62bf6c8aeaf2..000000000000 --- a/apps/meteor/app/livechat/lib/messageTypes.js +++ /dev/null @@ -1,130 +0,0 @@ -import formatDistance from 'date-fns/formatDistance'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import moment from 'moment'; -import { escapeHTML } from '@rocket.chat/string-helpers'; - -import { MessageTypes } from '../../ui-utils'; - -MessageTypes.registerType({ - id: 'livechat_navigation_history', - system: true, - message: 'New_visitor_navigation', - data(message) { - if (!message.navigation || !message.navigation.page) { - return; - } - return { - history: `${(message.navigation.page.title ? `${message.navigation.page.title} - ` : '') + message.navigation.page.location.href}`, - }; - }, -}); - -MessageTypes.registerType({ - id: 'livechat_transfer_history', - system: true, - message: 'New_chat_transfer', - data(message) { - if (!message.transferData) { - return; - } - - const { comment } = message.transferData; - const commentLabel = comment && comment !== '' ? '_with_a_comment' : ''; - const from = - message.transferData.transferredBy && (message.transferData.transferredBy.name || message.transferData.transferredBy.username); - const transferTypes = { - agent: () => - TAPi18n.__(`Livechat_transfer_to_agent${commentLabel}`, { - from, - to: - message.transferData.transferredTo && (message.transferData.transferredTo.name || message.transferData.transferredTo.username), - ...(comment && { comment }), - }), - department: () => - TAPi18n.__(`Livechat_transfer_to_department${commentLabel}`, { - from, - to: message.transferData.nextDepartment && message.transferData.nextDepartment.name, - ...(comment && { comment }), - }), - queue: () => - TAPi18n.__('Livechat_transfer_return_to_the_queue', { - from, - }), - }; - return { - transfer: transferTypes[message.transferData.scope](), - }; - }, -}); - -MessageTypes.registerType({ - id: 'livechat_transcript_history', - system: true, - message: 'Livechat_chat_transcript_sent', - data(message) { - if (!message.requestData) { - return; - } - - const { requestData: { type, visitor = {}, user = {} } = {} } = message; - const requestTypes = { - visitor: () => - TAPi18n.__('Livechat_visitor_transcript_request', { - guest: visitor.name || visitor.username, - }), - user: () => - TAPi18n.__('Livechat_user_sent_chat_transcript_to_visitor', { - agent: user.name || user.username, - guest: visitor.name || visitor.username, - }), - }; - - return { - transcript: requestTypes[type](), - }; - }, -}); - -MessageTypes.registerType({ - id: 'livechat_video_call', - system: true, - message: 'New_videocall_request', -}); - -MessageTypes.registerType({ - id: 'livechat_webrtc_video_call', - render(message) { - if (message.msg === 'ended' && message.webRtcCallEndTs && message.ts) { - return TAPi18n.__('WebRTC_call_ended_message', { - callDuration: formatDistance(new Date(message.webRtcCallEndTs), new Date(message.ts)), - endTime: moment(message.webRtcCallEndTs).format('h:mm A'), - }); - } - if (message.msg === 'declined' && message.webRtcCallEndTs) { - return TAPi18n.__('WebRTC_call_declined_message'); - } - return escapeHTML(message.msg); - }, -}); - -MessageTypes.registerType({ - id: 'omnichannel_placed_chat_on_hold', - system: true, - message: 'Omnichannel_placed_chat_on_hold', - data(message) { - return { - comment: message.comment, - }; - }, -}); - -MessageTypes.registerType({ - id: 'omnichannel_on_hold_chat_resumed', - system: true, - message: 'Omnichannel_on_hold_chat_resumed', - data(message) { - return { - comment: message.comment, - }; - }, -}); diff --git a/apps/meteor/app/livechat/lib/messageTypes.ts b/apps/meteor/app/livechat/lib/messageTypes.ts new file mode 100644 index 000000000000..096534d83b9d --- /dev/null +++ b/apps/meteor/app/livechat/lib/messageTypes.ts @@ -0,0 +1,134 @@ +import formatDistance from 'date-fns/formatDistance'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import moment from 'moment'; +import { escapeHTML } from '@rocket.chat/string-helpers'; +import { IOmnichannelSystemMessage } from '@rocket.chat/core-typings'; + +import { MessageTypes } from '../../ui-utils/lib/MessageTypes'; + +MessageTypes.registerType({ + id: 'livechat_navigation_history', + system: true, + message: 'New_visitor_navigation', + data(message: IOmnichannelSystemMessage) { + return { + history: message.navigation + ? `${(message.navigation.page.title ? `${message.navigation.page.title} - ` : '') + message.navigation.page.location.href}` + : '', + }; + }, +}); + +MessageTypes.registerType({ + id: 'livechat_transfer_history', + system: true, + message: 'New_chat_transfer', + data(message: IOmnichannelSystemMessage) { + if (!message.transferData) { + return { + transfer: '', + }; + } + + const { comment } = message.transferData; + const commentLabel = comment && comment !== '' ? '_with_a_comment' : ''; + const from = + message.transferData.transferredBy && (message.transferData.transferredBy.name || message.transferData.transferredBy.username); + const transferTypes = { + agent: (): string => + TAPi18n.__(`Livechat_transfer_to_agent${commentLabel}`, { + from, + to: message?.transferData?.transferredTo?.name || message?.transferData?.transferredTo?.username || '', + ...(comment && { comment }), + }), + department: (): string => + TAPi18n.__(`Livechat_transfer_to_department${commentLabel}`, { + from, + to: message?.transferData?.nextDepartment?.name || '', + ...(comment && { comment }), + }), + queue: (): string => + TAPi18n.__('Livechat_transfer_return_to_the_queue', { + from, + }), + }; + return { + transfer: transferTypes[message.transferData.scope](), + }; + }, +}); + +MessageTypes.registerType({ + id: 'livechat_transcript_history', + system: true, + message: 'Livechat_chat_transcript_sent', + data(message: IOmnichannelSystemMessage) { + if (!message.requestData) { + return { + transcript: '', + }; + } + + const { requestData: { type, visitor, user } = { type: 'user' } } = message; + const requestTypes = { + visitor: (): string => + TAPi18n.__('Livechat_visitor_transcript_request', { + guest: visitor?.name || visitor?.username || '', + }), + user: (): string => + TAPi18n.__('Livechat_user_sent_chat_transcript_to_visitor', { + agent: user?.name || user?.username || '', + guest: visitor?.name || visitor?.username || '', + }), + }; + + return { + transcript: requestTypes[type](), + }; + }, +}); + +MessageTypes.registerType({ + id: 'livechat_video_call', + system: true, + message: 'New_videocall_request', +}); + +MessageTypes.registerType({ + id: 'livechat_webrtc_video_call', + render(message) { + if (message.msg === 'ended' && message.webRtcCallEndTs && message.ts) { + return TAPi18n.__('WebRTC_call_ended_message', { + callDuration: formatDistance(new Date(message.webRtcCallEndTs), new Date(message.ts)), + endTime: moment(message.webRtcCallEndTs).format('h:mm A'), + }); + } + if (message.msg === 'declined' && message.webRtcCallEndTs) { + return TAPi18n.__('WebRTC_call_declined_message'); + } + return escapeHTML(message.msg); + }, + message: 'room_changed_privacy', +}); + +MessageTypes.registerType({ + id: 'omnichannel_placed_chat_on_hold', + system: true, + message: 'Omnichannel_placed_chat_on_hold', + data(message: IOmnichannelSystemMessage) { + return { + comment: message.comment ? message.comment : 'No comment provided', + }; + }, +}); + +MessageTypes.registerType({ + id: 'omnichannel_on_hold_chat_resumed', + system: true, + message: 'Omnichannel_on_hold_chat_resumed', + data(message: IOmnichannelSystemMessage) { + return { + comment: message.comment ? message.comment : 'No comment provided', + }; + }, +}); diff --git a/apps/meteor/app/livechat/lib/stream/constants.js b/apps/meteor/app/livechat/lib/stream/constants.ts similarity index 100% rename from apps/meteor/app/livechat/lib/stream/constants.js rename to apps/meteor/app/livechat/lib/stream/constants.ts diff --git a/apps/meteor/app/livechat/server/api.js b/apps/meteor/app/livechat/server/api.js index 9e2902ad30b3..55f2768a0941 100644 --- a/apps/meteor/app/livechat/server/api.js +++ b/apps/meteor/app/livechat/server/api.js @@ -13,5 +13,4 @@ import '../imports/server/rest/visitors.js'; import '../imports/server/rest/visitors.ts'; import '../imports/server/rest/dashboards.js'; import '../imports/server/rest/queue.js'; -import '../imports/server/rest/officeHour.js'; import '../imports/server/rest/businessHours.js'; diff --git a/apps/meteor/app/livechat/server/api/lib/agents.js b/apps/meteor/app/livechat/server/api/lib/agents.js index ded51c4f8eab..15d331552d16 100644 --- a/apps/meteor/app/livechat/server/api/lib/agents.js +++ b/apps/meteor/app/livechat/server/api/lib/agents.js @@ -1,5 +1,6 @@ +import { LivechatDepartmentAgents } from '@rocket.chat/models'; + import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { LivechatDepartmentAgents } from '../../../../models/server/raw'; export async function findAgentDepartments({ userId, enabledDepartmentsOnly, agentId }) { if (!(await hasPermissionAsync(userId, 'view-l-room'))) { diff --git a/apps/meteor/app/livechat/server/api/lib/appearance.js b/apps/meteor/app/livechat/server/api/lib/appearance.js index 6af8612b32c1..94e6d36cef5d 100644 --- a/apps/meteor/app/livechat/server/api/lib/appearance.js +++ b/apps/meteor/app/livechat/server/api/lib/appearance.js @@ -1,5 +1,6 @@ +import { Settings } from '@rocket.chat/models'; + import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { Settings } from '../../../../models/server/raw'; export async function findAppearance({ userId }) { if (!(await hasPermissionAsync(userId, 'view-livechat-manager'))) { diff --git a/apps/meteor/app/livechat/server/api/lib/customFields.js b/apps/meteor/app/livechat/server/api/lib/customFields.js index ee76b6c1a91d..20b91b5c5d0f 100644 --- a/apps/meteor/app/livechat/server/api/lib/customFields.js +++ b/apps/meteor/app/livechat/server/api/lib/customFields.js @@ -1,7 +1,7 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { LivechatCustomField } from '@rocket.chat/models'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { LivechatCustomField } from '../../../../models/server/raw'; export async function findLivechatCustomFields({ userId, text, pagination: { offset, count, sort } }) { if (!(await hasPermissionAsync(userId, 'view-l-room'))) { @@ -14,15 +14,13 @@ export async function findLivechatCustomFields({ userId, text, pagination: { off }), }; - const cursor = await LivechatCustomField.find(query, { + const { cursor, totalCount } = LivechatCustomField.findPaginated(query, { sort: sort || { label: 1 }, skip: offset, limit: count, }); - const total = await cursor.count(); - - const customFields = await cursor.toArray(); + const [customFields, total] = await Promise.all([cursor.toArray(), totalCount]); return { customFields, diff --git a/apps/meteor/app/livechat/server/api/lib/departments.ts b/apps/meteor/app/livechat/server/api/lib/departments.ts index ffa00fe64d3b..423e4f4318dc 100644 --- a/apps/meteor/app/livechat/server/api/lib/departments.ts +++ b/apps/meteor/app/livechat/server/api/lib/departments.ts @@ -1,13 +1,13 @@ -import { FilterQuery, SortOptionObject } from 'mongodb'; +import type { Filter, FindOptions } from 'mongodb'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import type { PaginatedResult } from '@rocket.chat/rest-typings'; import type { ILivechatDepartmentRecord, ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; +import { LivechatDepartment, LivechatDepartmentAgents } from '@rocket.chat/models'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { LivechatDepartment, LivechatDepartmentAgents } from '../../../../models/server/raw'; import { callbacks } from '../../../../../lib/callbacks'; -type Pagination = { pagination: { offset: number; count: number; sort: SortOptionObject } }; +type Pagination = { pagination: { offset: number; count: number; sort: FindOptions['sort'] } }; type FindDepartmentParams = { userId: string; onlyMyDepartments?: boolean; @@ -25,7 +25,7 @@ type FindDepartmentToAutocompleteParams = { uid: string; selector: { exceptions: string[]; - conditions: FilterQuery; + conditions: Filter; term: string; }; onlyMyDepartments?: boolean; @@ -58,15 +58,13 @@ export async function findDepartments({ query = callbacks.run('livechat.applyDepartmentRestrictions', query, { userId }); } - const cursor = LivechatDepartment.find(query, { + const { cursor, totalCount } = LivechatDepartment.findPaginated(query, { sort: sort || { name: 1 }, skip: offset, limit: count, }); - const total = await cursor.count(); - - const departments = await cursor.toArray(); + const [departments, total] = await Promise.all([cursor.toArray(), totalCount]); return { departments, @@ -118,7 +116,11 @@ export async function findDepartmentsToAutocomplete({ const { exceptions = [] } = selector; let { conditions = {} } = selector; - const options = { + if (onlyMyDepartments) { + conditions = callbacks.run('livechat.applyDepartmentRestrictions', conditions, { userId: uid }); + } + + const items = await LivechatDepartment.findByNameRegexWithExceptionsAndConditions(selector.term, exceptions, conditions, { projection: { _id: 1, name: 1, @@ -126,18 +128,7 @@ export async function findDepartmentsToAutocomplete({ sort: { name: 1, }, - }; - - if (onlyMyDepartments) { - conditions = callbacks.run('livechat.applyDepartmentRestrictions', conditions, { userId: uid }); - } - - const items = await LivechatDepartment.findByNameRegexWithExceptionsAndConditions( - selector.term, - exceptions, - conditions, - options, - ).toArray(); + }).toArray(); return { items, }; @@ -152,15 +143,13 @@ export async function findDepartmentAgents({ throw new Error('error-not-authorized'); } - const cursor = LivechatDepartmentAgents.findAgentsByDepartmentId(departmentId, { + const { cursor, totalCount } = LivechatDepartmentAgents.findAgentsByDepartmentId(departmentId, { sort: sort || { username: 1 }, skip: offset, limit: count, }); - const total = await cursor.count(); - - const agents = await cursor.toArray(); + const [agents, total] = await Promise.all([cursor.toArray(), totalCount]); return { agents, diff --git a/apps/meteor/app/livechat/server/api/lib/inquiries.js b/apps/meteor/app/livechat/server/api/lib/inquiries.js index 2b113433fe0c..88c3492f9c61 100644 --- a/apps/meteor/app/livechat/server/api/lib/inquiries.js +++ b/apps/meteor/app/livechat/server/api/lib/inquiries.js @@ -1,5 +1,6 @@ +import { LivechatDepartmentAgents, LivechatDepartment, LivechatInquiry } from '@rocket.chat/models'; + import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { LivechatDepartmentAgents, LivechatDepartment, LivechatInquiry } from '../../../../models/server/raw'; const agentDepartments = async (userId) => { const agentDepartments = (await LivechatDepartmentAgents.findByAgentId(userId).toArray()).map(({ departmentId }) => departmentId); @@ -48,9 +49,9 @@ export async function findInquiries({ userId, department: filterDepartment, stat ], }; - const cursor = LivechatInquiry.find(filter, options); - const total = await cursor.count(); - const inquiries = await cursor.toArray(); + const { cursor, totalCount } = LivechatInquiry.findPaginated(filter, options); + + const [inquiries, total] = await Promise.all([cursor.toArray(), totalCount]); return { inquiries, diff --git a/apps/meteor/app/livechat/server/api/lib/integrations.js b/apps/meteor/app/livechat/server/api/lib/integrations.js index 46bb09c3571c..8b90dad373e9 100644 --- a/apps/meteor/app/livechat/server/api/lib/integrations.js +++ b/apps/meteor/app/livechat/server/api/lib/integrations.js @@ -1,5 +1,6 @@ +import { Settings } from '@rocket.chat/models'; + import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { Settings } from '../../../../models/server/raw'; export async function findIntegrationSettings({ userId }) { if (!(await hasPermissionAsync(userId, 'view-livechat-manager'))) { diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.js b/apps/meteor/app/livechat/server/api/lib/livechat.js index d324da67fe37..507f06d687ed 100644 --- a/apps/meteor/app/livechat/server/api/lib/livechat.js +++ b/apps/meteor/app/livechat/server/api/lib/livechat.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { EmojiCustom, LivechatTrigger, LivechatVisitors } from '@rocket.chat/models'; -import { LivechatRooms, LivechatVisitors, LivechatDepartment } from '../../../../models/server'; -import { EmojiCustom, LivechatTrigger } from '../../../../models/server/raw'; +import { LivechatRooms, LivechatDepartment } from '../../../../models/server'; import { Livechat } from '../../lib/Livechat'; import { callbacks } from '../../../../../lib/callbacks'; import { normalizeAgent } from '../../lib/Helper'; @@ -40,7 +40,7 @@ export function findDepartments(businessUnit) { export function findGuest(token) { return LivechatVisitors.getVisitorByToken(token, { - fields: { + projection: { name: 1, username: 1, token: 1, @@ -121,7 +121,7 @@ export async function settings({ businessUnit = '' } = {}) { nameFieldRegistrationForm: initSettings.Livechat_name_field_registration_form, emailFieldRegistrationForm: initSettings.Livechat_email_field_registration_form, displayOfflineForm: initSettings.Livechat_display_offline_form, - videoCall: initSettings.Omnichannel_call_provider === 'Jitsi' && initSettings.Jitsi_Enabled === true, + videoCall: initSettings.Omnichannel_call_provider === 'Jitsi', fileUpload: initSettings.Livechat_fileupload_enabled && initSettings.FileUpload_Enabled, language: initSettings.Language, transcript: initSettings.Livechat_enable_transcript, @@ -155,8 +155,8 @@ export async function settings({ businessUnit = '' } = {}) { }, ], jitsi: [ - { icon: 'icon-videocam', i18nLabel: 'Accept', method_id: 'createLivechatCall' }, - { icon: 'icon-cancel', i18nLabel: 'Decline', method_id: 'denyLivechatCall' }, + { icon: 'icon-videocam', i18nLabel: 'Accept' }, + { icon: 'icon-cancel', i18nLabel: 'Decline' }, ], }, }, diff --git a/apps/meteor/app/livechat/server/api/lib/officeHour.js b/apps/meteor/app/livechat/server/api/lib/officeHour.js deleted file mode 100644 index dbdadd48fe22..000000000000 --- a/apps/meteor/app/livechat/server/api/lib/officeHour.js +++ /dev/null @@ -1,12 +0,0 @@ -import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { LivechatBusinessHours } from '../../../../models/server/raw'; - -export async function findLivechatOfficeHours({ userId }) { - if (!(await hasPermissionAsync(userId, 'view-livechat-business-hours'))) { - throw new Error('error-not-authorized'); - } - - return { - officeHours: (await LivechatBusinessHours.findOneDefaultBusinessHour()).workHours, - }; -} diff --git a/apps/meteor/app/livechat/server/api/lib/queue.js b/apps/meteor/app/livechat/server/api/lib/queue.js index 730a8e99cc8c..8098c61ed723 100644 --- a/apps/meteor/app/livechat/server/api/lib/queue.js +++ b/apps/meteor/app/livechat/server/api/lib/queue.js @@ -1,5 +1,6 @@ +import { LivechatRooms } from '@rocket.chat/models'; + import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { LivechatRooms } from '../../../../models/server/raw'; export async function findQueueMetrics({ userId, agentId, includeOfflineAgents, departmentId, pagination: { offset, count, sort } }) { if (!(await hasPermissionAsync(userId, 'view-l-room'))) { diff --git a/apps/meteor/app/livechat/server/api/lib/rooms.js b/apps/meteor/app/livechat/server/api/lib/rooms.js index 3ff91ba8b257..32862d65b4dd 100644 --- a/apps/meteor/app/livechat/server/api/lib/rooms.js +++ b/apps/meteor/app/livechat/server/api/lib/rooms.js @@ -1,4 +1,4 @@ -import { LivechatRooms, LivechatDepartment } from '../../../../models/server/raw'; +import { LivechatRooms, LivechatDepartment } from '@rocket.chat/models'; export async function findRooms({ agents, @@ -12,7 +12,7 @@ export async function findRooms({ onhold, options: { offset, count, fields, sort }, }) { - const cursor = LivechatRooms.findRoomsWithCriteria({ + const { cursor, totalCount } = LivechatRooms.findRoomsWithCriteria({ agents, roomName, departmentId, @@ -30,9 +30,7 @@ export async function findRooms({ }, }); - const total = await cursor.count(); - - const rooms = await cursor.toArray(); + const [rooms, total] = await Promise.all([cursor.toArray(), totalCount]); const departmentsIds = [...new Set(rooms.map((room) => room.departmentId).filter(Boolean))]; if (departmentsIds.length) { diff --git a/apps/meteor/app/livechat/server/api/lib/transfer.js b/apps/meteor/app/livechat/server/api/lib/transfer.js index f9f41a001b5d..68424216c16a 100644 --- a/apps/meteor/app/livechat/server/api/lib/transfer.js +++ b/apps/meteor/app/livechat/server/api/lib/transfer.js @@ -1,5 +1,6 @@ +import { Messages } from '@rocket.chat/models'; + import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { Messages } from '../../../../models/server/raw'; const normalizeTransferHistory = ({ transferData }) => transferData; export async function findLivechatTransferHistory({ userId, rid, pagination: { offset, count, sort } }) { @@ -7,19 +8,17 @@ export async function findLivechatTransferHistory({ userId, rid, pagination: { o throw new Error('error-not-authorized'); } - const cursor = await Messages.find( + const { cursor, totalCount } = Messages.findPaginated( { rid, t: 'livechat_transfer_history' }, { - fields: { transferData: 1 }, + projection: { transferData: 1 }, sort: sort || { ts: 1 }, skip: offset, limit: count, }, ); - const total = await cursor.count(); - const messages = await cursor.toArray(); - const history = messages.map(normalizeTransferHistory); + const [history, total] = await Promise.all([cursor.map(normalizeTransferHistory).toArray(), totalCount]); return { history, diff --git a/apps/meteor/app/livechat/server/api/lib/triggers.js b/apps/meteor/app/livechat/server/api/lib/triggers.js index 06d46d578cd8..1cf65b944374 100644 --- a/apps/meteor/app/livechat/server/api/lib/triggers.js +++ b/apps/meteor/app/livechat/server/api/lib/triggers.js @@ -1,12 +1,13 @@ +import { LivechatTrigger } from '@rocket.chat/models'; + import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { LivechatTrigger } from '../../../../models/server/raw'; export async function findTriggers({ userId, pagination: { offset, count, sort } }) { if (!(await hasPermissionAsync(userId, 'view-livechat-manager'))) { throw new Error('error-not-authorized'); } - const cursor = await LivechatTrigger.find( + const { cursor, totalCount } = LivechatTrigger.findPaginated( {}, { sort: sort || { name: 1 }, @@ -15,9 +16,7 @@ export async function findTriggers({ userId, pagination: { offset, count, sort } }, ); - const total = await cursor.count(); - - const triggers = await cursor.toArray(); + const [triggers, total] = await Promise.all([cursor.toArray(), totalCount]); return { triggers, diff --git a/apps/meteor/app/livechat/server/api/lib/users.js b/apps/meteor/app/livechat/server/api/lib/users.js index b7c5e1e11bd7..98f77cc144b0 100644 --- a/apps/meteor/app/livechat/server/api/lib/users.js +++ b/apps/meteor/app/livechat/server/api/lib/users.js @@ -1,7 +1,7 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { Users } from '@rocket.chat/models'; -import { hasAllPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { Users } from '../../../../models/server/raw'; +import { hasAllPermissionAsync, hasAtLeastOnePermissionAsync } from '../../../../authorization/server/functions/hasPermission'; /** * @param {IRole['_id']} role the role id @@ -17,7 +17,7 @@ async function findUsers({ role, text, pagination: { offset, count, sort } }) { }); } - const cursor = await Users.findUsersInRolesWithQuery(role, query, { + const { cursor, totalCount } = Users.findPaginatedUsersInRolesWithQuery(role, query, { sort: sort || { name: 1 }, skip: offset, limit: count, @@ -31,9 +31,7 @@ async function findUsers({ role, text, pagination: { offset, count, sort } }) { }, }); - const total = await cursor.count(); - - const users = await cursor.toArray(); + const [users, total] = await Promise.all([cursor.toArray(), totalCount]); return { users, @@ -43,7 +41,7 @@ async function findUsers({ role, text, pagination: { offset, count, sort } }) { }; } export async function findAgents({ userId, text, pagination: { offset, count, sort } }) { - if (!(await hasAllPermissionAsync(userId, ['view-l-room', 'transfer-livechat-guest']))) { + if (!(await hasAtLeastOnePermissionAsync(userId, ['manage-livechat-agents', 'transfer-livechat-guest', 'edit-omnichannel-contact']))) { throw new Error('error-not-authorized'); } diff --git a/apps/meteor/app/livechat/server/api/lib/visitors.js b/apps/meteor/app/livechat/server/api/lib/visitors.js index 3fbda7c7705b..49486693f9b6 100644 --- a/apps/meteor/app/livechat/server/api/lib/visitors.js +++ b/apps/meteor/app/livechat/server/api/lib/visitors.js @@ -1,5 +1,6 @@ +import { LivechatVisitors, Messages, LivechatRooms } from '@rocket.chat/models'; + import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { LivechatVisitors, Messages, LivechatRooms } from '../../../../models/server/raw'; import { canAccessRoomAsync } from '../../../../authorization/server/functions/canAccessRoom'; export async function findVisitorInfo({ userId, visitorId }) { @@ -25,15 +26,13 @@ export async function findVisitedPages({ userId, roomId, pagination: { offset, c if (!room) { throw new Error('invalid-room'); } - const cursor = Messages.findByRoomIdAndType(room._id, 'livechat_navigation_history', { + const { cursor, totalCount } = Messages.findPaginatedByRoomIdAndType(room._id, 'livechat_navigation_history', { sort: sort || { ts: -1 }, skip: offset, limit: count, }); - const total = await cursor.count(); - - const pages = await cursor.toArray(); + const [pages, total] = await Promise.all([cursor.toArray(), totalCount]); return { pages, @@ -55,15 +54,13 @@ export async function findChatHistory({ userId, roomId, visitorId, pagination: { throw new Error('error-not-allowed'); } - const cursor = LivechatRooms.findByVisitorId(visitorId, { + const { cursor, totalCount } = LivechatRooms.findPaginatedByVisitorId(visitorId, { sort: sort || { ts: -1 }, skip: offset, limit: count, }); - const total = await cursor.count(); - - const history = await cursor.toArray(); + const [history, total] = await Promise.all([cursor.toArray(), totalCount]); return { history, @@ -149,17 +146,21 @@ export async function findVisitorsToAutocomplete({ userId, selector }) { }; } -export async function findVisitorsByEmailOrPhoneOrNameOrUsername({ userId, term, pagination: { offset, count, sort } }) { +export async function findVisitorsByEmailOrPhoneOrNameOrUsername({ + userId, + emailOrPhone, + nameOrUsername, + pagination: { offset, count, sort }, +}) { if (!(await hasPermissionAsync(userId, 'view-l-room'))) { throw new Error('error-not-authorized'); } - const cursor = LivechatVisitors.findVisitorsByEmailOrPhoneOrNameOrUsername(term, { + const { cursor, totalCount } = LivechatVisitors.findPaginatedVisitorsByEmailOrPhoneOrNameOrUsername(emailOrPhone, nameOrUsername, { sort: sort || { ts: -1 }, skip: offset, limit: count, - fields: { - _id: 1, + projection: { username: 1, name: 1, phone: 1, @@ -169,9 +170,7 @@ export async function findVisitorsByEmailOrPhoneOrNameOrUsername({ userId, term, }, }); - const total = await cursor.count(); - - const visitors = await cursor.toArray(); + const [visitors, total] = await Promise.all([cursor.toArray(), totalCount]); return { visitors, diff --git a/apps/meteor/app/livechat/server/api/rest.js b/apps/meteor/app/livechat/server/api/rest.js index 7273c565aac9..bfc6fe85465b 100644 --- a/apps/meteor/app/livechat/server/api/rest.js +++ b/apps/meteor/app/livechat/server/api/rest.js @@ -9,4 +9,4 @@ import './v1/customField.js'; import './v1/room.js'; import './v1/videoCall.js'; import './v1/transfer.js'; -import './v1/contact.js'; +import './v1/contact'; diff --git a/apps/meteor/app/livechat/server/api/v1/agent.js b/apps/meteor/app/livechat/server/api/v1/agent.js index 7e2329058542..104f0bc012e3 100644 --- a/apps/meteor/app/livechat/server/api/v1/agent.js +++ b/apps/meteor/app/livechat/server/api/v1/agent.js @@ -6,14 +6,14 @@ import { findRoom, findGuest, findAgent, findOpenRoom } from '../lib/livechat'; import { Livechat } from '../../lib/Livechat'; API.v1.addRoute('livechat/agent.info/:rid/:token', { - get() { + async get() { try { check(this.urlParams, { rid: String, token: String, }); - const visitor = findGuest(this.urlParams.token); + const visitor = await findGuest(this.urlParams.token); if (!visitor) { throw new Meteor.Error('invalid-token'); } diff --git a/apps/meteor/app/livechat/server/api/v1/config.js b/apps/meteor/app/livechat/server/api/v1/config.js index c97a5b2a3b56..1d7bf36a08f9 100644 --- a/apps/meteor/app/livechat/server/api/v1/config.js +++ b/apps/meteor/app/livechat/server/api/v1/config.js @@ -26,7 +26,7 @@ API.v1.addRoute('livechat/config', { const config = await cachedSettings({ businessUnit }); const status = Livechat.online(department); - const guest = token && Livechat.findGuest(token); + const guest = token && (await Livechat.findGuest(token)); const room = guest && findOpenRoom(token); const agent = guest && room && room.servedBy && findAgent(room.servedBy._id); diff --git a/apps/meteor/app/livechat/server/api/v1/contact.js b/apps/meteor/app/livechat/server/api/v1/contact.js deleted file mode 100644 index 21cc25519faa..000000000000 --- a/apps/meteor/app/livechat/server/api/v1/contact.js +++ /dev/null @@ -1,75 +0,0 @@ -import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { API } from '../../../../api/server'; -import { Contacts } from '../../lib/Contacts'; -import { LivechatVisitors } from '../../../../models'; - -API.v1.addRoute( - 'omnichannel/contact', - { authRequired: true }, - { - post() { - try { - check(this.bodyParams, { - _id: Match.Maybe(String), - token: String, - name: String, - email: Match.Maybe(String), - phone: Match.Maybe(String), - customFields: Match.Maybe(Object), - contactManager: Match.Maybe(Object), - }); - - const contact = Contacts.registerContact(this.bodyParams); - - return API.v1.success({ contact }); - } catch (e) { - return API.v1.failure(e); - } - }, - get() { - check(this.queryParams, { - contactId: String, - }); - - const contact = Promise.await(LivechatVisitors.findOneById(this.queryParams.contactId)); - - return API.v1.success({ contact }); - }, - }, -); - -API.v1.addRoute( - 'omnichannel/contact.search', - { authRequired: true }, - { - get() { - try { - check(this.queryParams, { - email: Match.Maybe(String), - phone: Match.Maybe(String), - }); - - const { email, phone } = this.queryParams; - - if (!email && !phone) { - throw new Meteor.Error('error-invalid-params'); - } - - const query = Object.assign( - {}, - { - ...(email && { visitorEmails: { address: email } }), - ...(phone && { phone: { phoneNumber: phone } }), - }, - ); - - const contact = Promise.await(LivechatVisitors.findOne(query)); - return API.v1.success({ contact }); - } catch (e) { - return API.v1.failure(e); - } - }, - }, -); diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts new file mode 100644 index 000000000000..baf9eb6410e7 --- /dev/null +++ b/apps/meteor/app/livechat/server/api/v1/contact.ts @@ -0,0 +1,70 @@ +import { Match, check } from 'meteor/check'; +import { Meteor } from 'meteor/meteor'; +import { LivechatVisitors } from '@rocket.chat/models'; + +import { API } from '../../../../api/server'; +import { Contacts } from '../../lib/Contacts'; + +API.v1.addRoute( + 'omnichannel/contact', + { authRequired: true }, + { + async post() { + check(this.bodyParams, { + _id: Match.Maybe(String), + token: String, + name: String, + email: Match.Maybe(String), + phone: Match.Maybe(String), + username: Match.Maybe(String), + customFields: Match.Maybe(Object), + contactManager: Match.Maybe({ + username: String, + }), + }); + + const contact = await Contacts.registerContact(this.bodyParams); + + return API.v1.success({ contact }); + }, + async get() { + check(this.queryParams, { + contactId: String, + }); + + const contact = await LivechatVisitors.findOneById(this.queryParams.contactId); + + return API.v1.success({ contact }); + }, + }, +); + +API.v1.addRoute( + 'omnichannel/contact.search', + { authRequired: true }, + { + async get() { + check(this.queryParams, { + email: Match.Maybe(String), + phone: Match.Maybe(String), + }); + + const { email, phone } = this.queryParams; + + if (!email && !phone) { + throw new Meteor.Error('error-invalid-params'); + } + + const query = Object.assign( + {}, + { + ...(email && { visitorEmails: { address: email } }), + ...(phone && { phone: { phoneNumber: phone } }), + }, + ); + + const contact = await LivechatVisitors.findOne(query); + return API.v1.success({ contact }); + }, + }, +); diff --git a/apps/meteor/app/livechat/server/api/v1/customField.js b/apps/meteor/app/livechat/server/api/v1/customField.js index b15ded04a5d0..5d242f3d00ad 100644 --- a/apps/meteor/app/livechat/server/api/v1/customField.js +++ b/apps/meteor/app/livechat/server/api/v1/customField.js @@ -7,7 +7,7 @@ import { Livechat } from '../../lib/Livechat'; import { findLivechatCustomFields, findCustomFieldById } from '../lib/customFields'; API.v1.addRoute('livechat/custom.field', { - post() { + async post() { try { check(this.bodyParams, { token: String, @@ -18,12 +18,12 @@ API.v1.addRoute('livechat/custom.field', { const { token, key, value, overwrite } = this.bodyParams; - const guest = findGuest(token); + const guest = await findGuest(token); if (!guest) { throw new Meteor.Error('invalid-token'); } - if (!Livechat.setCustomFields({ token, key, value, overwrite })) { + if (!(await Livechat.setCustomFields({ token, key, value, overwrite }))) { return API.v1.failure(); } @@ -35,7 +35,7 @@ API.v1.addRoute('livechat/custom.field', { }); API.v1.addRoute('livechat/custom.fields', { - post() { + async post() { check(this.bodyParams, { token: String, customFields: [ @@ -48,19 +48,21 @@ API.v1.addRoute('livechat/custom.fields', { }); const { token } = this.bodyParams; - const guest = findGuest(token); + const guest = await findGuest(token); if (!guest) { throw new Meteor.Error('invalid-token'); } - const fields = this.bodyParams.customFields.map((customField) => { - const data = Object.assign({ token }, customField); - if (!Livechat.setCustomFields(data)) { - return API.v1.failure(); - } + const fields = await Promise.all( + this.bodyParams.customFields.map(async (customField) => { + const data = Object.assign({ token }, customField); + if (!(await Livechat.setCustomFields(data))) { + return API.v1.failure(); + } - return { Key: customField.key, value: customField.value, overwrite: customField.overwrite }; - }); + return { Key: customField.key, value: customField.value, overwrite: customField.overwrite }; + }), + ); return API.v1.success({ fields }); }, diff --git a/apps/meteor/app/livechat/server/api/v1/message.js b/apps/meteor/app/livechat/server/api/v1/message.js index b0827d4748d7..2d57ca7f5724 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.js +++ b/apps/meteor/app/livechat/server/api/v1/message.js @@ -2,8 +2,9 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { Random } from 'meteor/random'; import { OmnichannelSourceType } from '@rocket.chat/core-typings'; +import { LivechatVisitors } from '@rocket.chat/models'; -import { Messages, LivechatRooms, LivechatVisitors } from '../../../../models'; +import { Messages, LivechatRooms } from '../../../../models/server'; import { hasPermission } from '../../../../authorization'; import { API } from '../../../../api/server'; import { loadMessageHistory } from '../../../../lib'; @@ -13,7 +14,7 @@ import { normalizeMessageFileUpload } from '../../../../utils/server/functions/n import { settings } from '../../../../settings/server'; API.v1.addRoute('livechat/message', { - post() { + async post() { try { check(this.bodyParams, { _id: Match.Maybe(String), @@ -28,7 +29,7 @@ API.v1.addRoute('livechat/message', { const { token, rid, agent, msg } = this.bodyParams; - const guest = findGuest(token); + const guest = await findGuest(token); if (!guest) { throw new Meteor.Error('invalid-token'); } @@ -67,7 +68,7 @@ API.v1.addRoute('livechat/message', { }, }; - const result = Promise.await(Livechat.sendMessage(sendMessage)); + const result = await Livechat.sendMessage(sendMessage); if (result) { const message = Messages.findOneById(_id); return API.v1.success({ message }); @@ -81,7 +82,7 @@ API.v1.addRoute('livechat/message', { }); API.v1.addRoute('livechat/message/:_id', { - get() { + async get() { try { check(this.urlParams, { _id: String, @@ -95,7 +96,7 @@ API.v1.addRoute('livechat/message/:_id', { const { token, rid } = this.queryParams; const { _id } = this.urlParams; - const guest = findGuest(token); + const guest = await findGuest(token); if (!guest) { throw new Meteor.Error('invalid-token'); } @@ -111,7 +112,7 @@ API.v1.addRoute('livechat/message/:_id', { } if (message.file) { - message = Promise.await(normalizeMessageFileUpload(message)); + message = await normalizeMessageFileUpload(message); } return API.v1.success({ message }); @@ -120,7 +121,7 @@ API.v1.addRoute('livechat/message/:_id', { } }, - put() { + async put() { try { check(this.urlParams, { _id: String, @@ -135,7 +136,7 @@ API.v1.addRoute('livechat/message/:_id', { const { token, rid } = this.bodyParams; const { _id } = this.urlParams; - const guest = findGuest(token); + const guest = await findGuest(token); if (!guest) { throw new Meteor.Error('invalid-token'); } @@ -157,7 +158,7 @@ API.v1.addRoute('livechat/message/:_id', { if (result) { let message = Messages.findOneById(_id); if (message.file) { - message = Promise.await(normalizeMessageFileUpload(message)); + message = await normalizeMessageFileUpload(message); } return API.v1.success({ message }); @@ -168,7 +169,7 @@ API.v1.addRoute('livechat/message/:_id', { return API.v1.failure(e); } }, - delete() { + async delete() { try { check(this.urlParams, { _id: String, @@ -182,7 +183,7 @@ API.v1.addRoute('livechat/message/:_id', { const { token, rid } = this.bodyParams; const { _id } = this.urlParams; - const guest = findGuest(token); + const guest = await findGuest(token); if (!guest) { throw new Meteor.Error('invalid-token'); } @@ -197,7 +198,7 @@ API.v1.addRoute('livechat/message/:_id', { throw new Meteor.Error('invalid-message'); } - const result = Promise.await(Livechat.deleteMessage({ guest, message })); + const result = await Livechat.deleteMessage({ guest, message }); if (result) { return API.v1.success({ message: { @@ -215,7 +216,7 @@ API.v1.addRoute('livechat/message/:_id', { }); API.v1.addRoute('livechat/messages.history/:rid', { - get() { + async get() { try { check(this.urlParams, { rid: String, @@ -230,7 +231,7 @@ API.v1.addRoute('livechat/messages.history/:rid', { throw new Meteor.Error('error-token-param-not-provided', 'The required "token" query param is missing.'); } - const guest = findGuest(token); + const guest = await findGuest(token); if (!guest) { throw new Meteor.Error('invalid-token'); } @@ -276,7 +277,7 @@ API.v1.addRoute( 'livechat/messages', { authRequired: true }, { - post() { + async post() { if (!hasPermission(this.userId, 'view-livechat-manager')) { return API.v1.unauthorized(); } @@ -299,7 +300,7 @@ API.v1.addRoute( const visitorToken = this.bodyParams.visitor.token; - let visitor = LivechatVisitors.getVisitorByToken(visitorToken); + let visitor = await LivechatVisitors.getVisitorByToken(visitorToken); let rid; if (visitor) { const rooms = LivechatRooms.findOpenByVisitorToken(visitorToken).fetch(); @@ -314,8 +315,8 @@ API.v1.addRoute( const guest = this.bodyParams.visitor; guest.connectionData = normalizeHttpHeaderData(this.request.headers); - const visitorId = Livechat.registerGuest(guest); - visitor = LivechatVisitors.findOneById(visitorId); + const visitorId = await Livechat.registerGuest(guest); + visitor = await LivechatVisitors.findOneById(visitorId); } const sentMessages = this.bodyParams.messages.map((message) => { diff --git a/apps/meteor/app/livechat/server/api/v1/room.js b/apps/meteor/app/livechat/server/api/v1/room.js index 5e3bf4636a28..6a0f991795ae 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.js +++ b/apps/meteor/app/livechat/server/api/v1/room.js @@ -4,8 +4,8 @@ import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { OmnichannelSourceType } from '@rocket.chat/core-typings'; -import { settings as rcSettings } from '../../../../settings'; -import { Messages, LivechatRooms } from '../../../../models'; +import { settings as rcSettings } from '../../../../settings/server'; +import { Messages, LivechatRooms } from '../../../../models/server'; import { API } from '../../../../api/server'; import { findGuest, findRoom, getRoom, settings, findAgent, onCheckRoomParams } from '../lib/livechat'; import { Livechat } from '../../lib/Livechat'; @@ -15,7 +15,7 @@ import { canAccessRoom } from '../../../../authorization/server'; import { addUserToRoom } from '../../../../lib/server/functions'; API.v1.addRoute('livechat/room', { - get() { + async get() { const defaultCheckParams = { token: String, rid: Match.Maybe(String), @@ -28,7 +28,7 @@ API.v1.addRoute('livechat/room', { const { token, rid: roomId, agentId, ...extraParams } = this.queryParams; - const guest = findGuest(token); + const guest = await findGuest(token); if (!guest) { throw new Meteor.Error('invalid-token'); } @@ -54,7 +54,7 @@ API.v1.addRoute('livechat/room', { }, }; - room = Promise.await(getRoom({ guest, rid, agent, roomInfo, extraParams })); + room = await getRoom({ guest, rid, agent, roomInfo, extraParams }); return API.v1.success(room); } @@ -68,7 +68,7 @@ API.v1.addRoute('livechat/room', { }); API.v1.addRoute('livechat/room.close', { - post() { + async post() { try { check(this.bodyParams, { rid: String, @@ -77,7 +77,7 @@ API.v1.addRoute('livechat/room.close', { const { rid, token } = this.bodyParams; - const visitor = findGuest(token); + const visitor = await findGuest(token); if (!visitor) { throw new Meteor.Error('invalid-token'); } @@ -106,7 +106,7 @@ API.v1.addRoute('livechat/room.close', { }); API.v1.addRoute('livechat/room.transfer', { - post() { + async post() { try { check(this.bodyParams, { rid: String, @@ -116,7 +116,7 @@ API.v1.addRoute('livechat/room.transfer', { const { rid, token, department } = this.bodyParams; - const guest = findGuest(token); + const guest = await findGuest(token); if (!guest) { throw new Meteor.Error('invalid-token'); } @@ -132,7 +132,7 @@ API.v1.addRoute('livechat/room.transfer', { const { _id, username, name } = guest; const transferredBy = normalizeTransferredByData({ _id, username, name, userType: 'visitor' }, room); - if (!Promise.await(Livechat.transfer(room, guest, { roomId: rid, departmentId: department, transferredBy }))) { + if (!(await Livechat.transfer(room, guest, { roomId: rid, departmentId: department, transferredBy }))) { return API.v1.failure(); } @@ -145,7 +145,7 @@ API.v1.addRoute('livechat/room.transfer', { }); API.v1.addRoute('livechat/room.survey', { - post() { + async post() { try { check(this.bodyParams, { rid: String, @@ -160,7 +160,7 @@ API.v1.addRoute('livechat/room.survey', { const { rid, token, data } = this.bodyParams; - const visitor = findGuest(token); + const visitor = await findGuest(token); if (!visitor) { throw new Meteor.Error('invalid-token'); } @@ -170,7 +170,7 @@ API.v1.addRoute('livechat/room.survey', { throw new Meteor.Error('invalid-room'); } - const config = Promise.await(settings()); + const config = await settings(); if (!config.survey || !config.survey.items || !config.survey.values) { throw new Meteor.Error('invalid-livechat-config'); } diff --git a/apps/meteor/app/livechat/server/api/v1/transcript.js b/apps/meteor/app/livechat/server/api/v1/transcript.js index f8f3c923d25e..040bb51a0f65 100644 --- a/apps/meteor/app/livechat/server/api/v1/transcript.js +++ b/apps/meteor/app/livechat/server/api/v1/transcript.js @@ -5,7 +5,7 @@ import { API } from '../../../../api/server'; import { Livechat } from '../../lib/Livechat'; API.v1.addRoute('livechat/transcript', { - post() { + async post() { try { check(this.bodyParams, { token: String, @@ -14,7 +14,7 @@ API.v1.addRoute('livechat/transcript', { }); const { token, rid, email } = this.bodyParams; - if (!Livechat.sendTranscript({ token, rid, email })) { + if (!(await Livechat.sendTranscript({ token, rid, email }))) { return API.v1.failure({ message: TAPi18n.__('Error_sending_livechat_transcript') }); } diff --git a/apps/meteor/app/livechat/server/api/v1/transfer.js b/apps/meteor/app/livechat/server/api/v1/transfer.js index 536884726a26..b1b65e0202e4 100644 --- a/apps/meteor/app/livechat/server/api/v1/transfer.js +++ b/apps/meteor/app/livechat/server/api/v1/transfer.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { LivechatRooms } from '../../../../models'; +import { LivechatRooms } from '../../../../models/server'; import { API } from '../../../../api/server'; import { findLivechatTransferHistory } from '../lib/transfer'; diff --git a/apps/meteor/app/livechat/server/api/v1/videoCall.js b/apps/meteor/app/livechat/server/api/v1/videoCall.js index b7f3e299a09d..ca13d3278bcd 100644 --- a/apps/meteor/app/livechat/server/api/v1/videoCall.js +++ b/apps/meteor/app/livechat/server/api/v1/videoCall.js @@ -1,76 +1,17 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { OmnichannelSourceType } from '@rocket.chat/core-typings'; -import { Messages, Rooms } from '../../../../models'; +import { Messages, Rooms, Settings } from '../../../../models'; import { settings as rcSettings } from '../../../../settings/server'; import { API } from '../../../../api/server'; -import { findGuest, getRoom, settings } from '../lib/livechat'; +import { settings } from '../lib/livechat'; import { hasPermission, canSendMessage } from '../../../../authorization'; import { Livechat } from '../../lib/Livechat'; import { Logger } from '../../../../logger'; const logger = new Logger('LivechatVideoCallApi'); -API.v1.addRoute('livechat/video.call/:token', { - get() { - try { - check(this.urlParams, { - token: String, - }); - - check(this.queryParams, { - rid: Match.Maybe(String), - }); - - const { token } = this.urlParams; - - const guest = findGuest(token); - if (!guest) { - throw new Meteor.Error('invalid-token'); - } - - const rid = this.queryParams.rid || Random.id(); - const roomInfo = { - jitsiTimeout: new Date(Date.now() + 3600 * 1000), - source: { - type: OmnichannelSourceType.API, - alias: 'video-call', - }, - }; - const { room } = getRoom({ guest, rid, roomInfo }); - const config = Promise.await(settings()); - if (!config.theme || !config.theme.actionLinks || !config.theme.actionLinks.jitsi) { - throw new Meteor.Error('invalid-livechat-config'); - } - - Messages.createWithTypeRoomIdMessageAndUser('livechat_video_call', room._id, '', guest, { - actionLinks: config.theme.actionLinks.jitsi, - }); - let rname; - if (rcSettings.get('Jitsi_URL_Room_Hash')) { - rname = rcSettings.get('uniqueID') + rid; - } else { - rname = encodeURIComponent(room.t === 'd' ? room.usernames.join(' x ') : room.name); - } - const videoCall = { - rid, - domain: rcSettings.get('Jitsi_Domain'), - provider: 'jitsi', - room: rcSettings.get('Jitsi_URL_Room_Prefix') + rname + rcSettings.get('Jitsi_URL_Room_Suffix'), - timeout: new Date(Date.now() + 3600 * 1000), - }; - - return API.v1.success(this.deprecationWarning({ videoCall })); - } catch (e) { - logger.error(e); - return API.v1.failure(e); - } - }, -}); - API.v1.addRoute( 'livechat/webrtc.call', { authRequired: true }, @@ -107,6 +48,7 @@ API.v1.addRoute( let { callStatus } = room; if (!callStatus || callStatus === 'ended' || callStatus === 'declined') { + Settings.incrementValueById('WebRTC_Calls_Count'); callStatus = 'ringing'; Promise.await(Rooms.setCallStatusAndCallStartTime(room._id, callStatus)); Promise.await( diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index 0c2a23cc2e21..9226926f94a9 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import type { ILivechatVisitorDTO, IRoom } from '@rocket.chat/core-typings'; +import { LivechatVisitors as VisitorsRaw } from '@rocket.chat/models'; -import { LivechatRooms, LivechatVisitors, LivechatCustomField } from '../../../../models/server'; -import { LivechatVisitors as VisitorsRaw } from '../../../../models/server/raw'; +import { LivechatRooms, LivechatCustomField } from '../../../../models/server'; import { API } from '../../../../api/server'; import { findGuest, normalizeHttpHeaderData } from '../lib/livechat'; import { Livechat } from '../../lib/Livechat'; @@ -37,7 +37,7 @@ API.v1.addRoute('livechat/visitor', { } guest.connectionData = normalizeHttpHeaderData(this.request.headers); - const visitorId = Livechat.registerGuest(guest as any); // TODO: Rewrite Livechat to TS + const visitorId = await Livechat.registerGuest(guest as any); // TODO: Rewrite Livechat to TS let visitor = await VisitorsRaw.findOneById(visitorId, {}); // If it's updating an existing visitor, it must also update the roomInfo @@ -55,7 +55,8 @@ API.v1.addRoute('livechat/visitor', { return; } const { key, value, overwrite } = field; - if (customField.scope === 'visitor' && !LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite)) { + // TODO: refactor this to use normal await + if (customField.scope === 'visitor' && !Promise.await(VisitorsRaw.updateLivechatDataByToken(token, key, value, overwrite))) { return API.v1.failure(); } }); @@ -112,7 +113,7 @@ API.v1.addRoute('livechat/visitor/:token', { } const { _id } = visitor; - const result = Livechat.removeGuest(_id); + const result = await Livechat.removeGuest(_id); if (!result) { throw new Meteor.Error('error-removing-visitor', 'An error ocurred while deleting visitor'); } @@ -156,7 +157,7 @@ API.v1.addRoute('livechat/visitor.callStatus', { }); const { token, callStatus, rid, callId } = this.bodyParams; - const guest = findGuest(token); + const guest = await findGuest(token); if (!guest) { throw new Meteor.Error('invalid-token'); } @@ -174,7 +175,7 @@ API.v1.addRoute('livechat/visitor.status', { const { token, status } = this.bodyParams; - const guest = findGuest(token); + const guest = await findGuest(token); if (!guest) { throw new Meteor.Error('invalid-token'); } diff --git a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts index 66024e005ae2..205cd9e741fa 100644 --- a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts +++ b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts @@ -1,9 +1,9 @@ -import moment from 'moment'; +import moment from 'moment-timezone'; import type { ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings'; +import type { ILivechatBusinessHoursModel, IUsersModel } from '@rocket.chat/model-typings'; +import { LivechatBusinessHours, Users } from '@rocket.chat/models'; -import { IWorkHoursCronJobsWrapper, LivechatBusinessHoursRaw } from '../../../models/server/raw/LivechatBusinessHours'; -import { UsersRaw } from '../../../models/server/raw/Users'; -import { LivechatBusinessHours, Users } from '../../../models/server/raw'; +import { IWorkHoursCronJobsWrapper } from '../../../../server/models/raw/LivechatBusinessHours'; export interface IBusinessHourBehavior { findHoursToCreateJobs(): Promise; @@ -27,9 +27,9 @@ export interface IBusinessHourType { } export abstract class AbstractBusinessHourBehavior { - protected BusinessHourRepository: LivechatBusinessHoursRaw = LivechatBusinessHours; + protected BusinessHourRepository: ILivechatBusinessHoursModel = LivechatBusinessHours; - protected UsersRepository: UsersRaw = Users; + protected UsersRepository: IUsersModel = Users; async findHoursToCreateJobs(): Promise { return this.BusinessHourRepository.findHoursToScheduleJobs(); @@ -54,9 +54,9 @@ export abstract class AbstractBusinessHourBehavior { } export abstract class AbstractBusinessHourType { - protected BusinessHourRepository: LivechatBusinessHoursRaw = LivechatBusinessHours; + protected BusinessHourRepository: ILivechatBusinessHoursModel = LivechatBusinessHours; - protected UsersRepository: UsersRaw = Users; + protected UsersRepository: IUsersModel = Users; protected async baseSaveBusinessHour(businessHourData: ILivechatBusinessHour): Promise { businessHourData.active = Boolean(businessHourData.active); diff --git a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts index 6ea2c8be5487..cf673dc0979e 100644 --- a/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts +++ b/apps/meteor/app/livechat/server/business-hour/BusinessHourManager.ts @@ -1,11 +1,11 @@ import moment from 'moment'; import { ILivechatBusinessHour, LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; import type { ICronJobs } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; import { IBusinessHourBehavior, IBusinessHourType } from './AbstractBusinessHour'; import { settings } from '../../../settings/server'; import { callbacks } from '../../../../lib/callbacks'; -import { Users } from '../../../models/server/raw'; const cronJobDayDict: Record = { Sunday: 0, diff --git a/apps/meteor/app/livechat/server/business-hour/Default.ts b/apps/meteor/app/livechat/server/business-hour/Default.ts index 010950800946..a94f12baecfe 100644 --- a/apps/meteor/app/livechat/server/business-hour/Default.ts +++ b/apps/meteor/app/livechat/server/business-hour/Default.ts @@ -1,4 +1,4 @@ -import moment from 'moment'; +import moment from 'moment-timezone'; import { ILivechatBusinessHour, LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; import { AbstractBusinessHourType, IBusinessHourType } from './AbstractBusinessHour'; diff --git a/apps/meteor/app/livechat/server/business-hour/Helper.ts b/apps/meteor/app/livechat/server/business-hour/Helper.ts index 98369bf58d33..89c439606b0a 100644 --- a/apps/meteor/app/livechat/server/business-hour/Helper.ts +++ b/apps/meteor/app/livechat/server/business-hour/Helper.ts @@ -1,7 +1,7 @@ import moment from 'moment'; import { ILivechatBusinessHour, LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; +import { LivechatBusinessHours, Users } from '@rocket.chat/models'; -import { LivechatBusinessHours, Users } from '../../../models/server/raw'; import { createDefaultBusinessHourRow } from '../../../models/server/models/LivechatBusinessHours'; export const filterBusinessHoursThatMustBeOpened = async ( diff --git a/apps/meteor/app/livechat/server/config.ts b/apps/meteor/app/livechat/server/config.ts index 0e4042ce70d4..89339e29e2a2 100644 --- a/apps/meteor/app/livechat/server/config.ts +++ b/apps/meteor/app/livechat/server/config.ts @@ -471,15 +471,6 @@ Meteor.startup(function () { enableQuery: omnichannelEnabledQuery, }); - this.add('Livechat_RDStation_Token', '', { - type: 'string', - group: 'Omnichannel', - public: false, - section: 'RD Station', - i18nLabel: 'RDStation_Token', - enableQuery: omnichannelEnabledQuery, - }); - this.add('Livechat_Routing_Method', 'Auto_Selection', { type: 'select', group: 'Omnichannel', diff --git a/apps/meteor/app/livechat/server/hooks/RDStation.js b/apps/meteor/app/livechat/server/hooks/RDStation.js deleted file mode 100644 index b4f5398018c1..000000000000 --- a/apps/meteor/app/livechat/server/hooks/RDStation.js +++ /dev/null @@ -1,62 +0,0 @@ -import { HTTP } from 'meteor/http'; - -import { settings } from '../../../settings'; -import { callbacks } from '../../../../lib/callbacks'; -import { Livechat } from '../lib/Livechat'; -import { SystemLogger } from '../../../../server/lib/logger/system'; - -function sendToRDStation(room) { - if (!settings.get('Livechat_RDStation_Token')) { - return room; - } - - const livechatData = Livechat.getLivechatRoomGuestInfo(room); - - if (!livechatData.visitor.email) { - return room; - } - - const email = Array.isArray(livechatData.visitor.email) ? livechatData.visitor.email[0].address : livechatData.visitor.email; - - const options = { - headers: { - 'Content-Type': 'application/json', - }, - data: { - token_rdstation: settings.get('Livechat_RDStation_Token'), - identificador: 'rocketchat-livechat', - client_id: livechatData.visitor._id, - email, - }, - }; - - options.data.nome = livechatData.visitor.name || livechatData.visitor.username; - - if (livechatData.visitor.phone) { - options.data.telefone = livechatData.visitor.phone; - } - - if (livechatData.tags) { - options.data.tags = livechatData.tags; - } - - Object.keys(livechatData.customFields || {}).forEach((field) => { - options.data[field] = livechatData.customFields[field]; - }); - - Object.keys(livechatData.visitor.customFields || {}).forEach((field) => { - options.data[field] = livechatData.visitor.customFields[field]; - }); - - try { - HTTP.call('POST', 'https://www.rdstation.com.br/api/1.3/conversions', options); - } catch (e) { - SystemLogger.error('Error sending lead to RD Station ->', e); - } - - return room; -} - -callbacks.add('livechat.closeRoom', sendToRDStation, callbacks.priority.MEDIUM, 'livechat-rd-station-close-room'); - -callbacks.add('livechat.saveInfo', sendToRDStation, callbacks.priority.MEDIUM, 'livechat-rd-station-save-info'); diff --git a/apps/meteor/app/livechat/server/hooks/beforeCloseRoom.js b/apps/meteor/app/livechat/server/hooks/beforeCloseRoom.js index 4604c7c0fca3..641b2bbb7b96 100644 --- a/apps/meteor/app/livechat/server/hooks/beforeCloseRoom.js +++ b/apps/meteor/app/livechat/server/hooks/beforeCloseRoom.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; -import { LivechatDepartment } from '../../../models'; +import { LivechatDepartment } from '../../../models/server'; const concatUnique = (...arrays) => [...new Set([].concat(...arrays.filter(Array.isArray)))]; diff --git a/apps/meteor/app/livechat/server/hooks/beforeDelegateAgent.js b/apps/meteor/app/livechat/server/hooks/beforeDelegateAgent.js index e93c682ee175..9b13160afebe 100644 --- a/apps/meteor/app/livechat/server/hooks/beforeDelegateAgent.js +++ b/apps/meteor/app/livechat/server/hooks/beforeDelegateAgent.js @@ -1,6 +1,6 @@ import { callbacks } from '../../../../lib/callbacks'; -import { settings } from '../../../settings'; -import { Users, LivechatDepartmentAgents } from '../../../models'; +import { settings } from '../../../settings/server'; +import { Users, LivechatDepartmentAgents } from '../../../models/server'; callbacks.add( 'livechat.beforeDelegateAgent', @@ -14,10 +14,10 @@ callbacks.add( } if (department) { - return LivechatDepartmentAgents.getNextBotForDepartment(department); + return Promise.await(LivechatDepartmentAgents.getNextBotForDepartment(department)); } - return Users.getNextBotAgent(); + return Promise.await(Users.getNextBotAgent()); }, callbacks.priority.HIGH, 'livechat-before-delegate-agent', diff --git a/apps/meteor/app/livechat/server/hooks/leadCapture.js b/apps/meteor/app/livechat/server/hooks/leadCapture.js index 892bcf8aade0..6a1d3b5b67b9 100644 --- a/apps/meteor/app/livechat/server/hooks/leadCapture.js +++ b/apps/meteor/app/livechat/server/hooks/leadCapture.js @@ -1,6 +1,7 @@ +import { LivechatVisitors } from '@rocket.chat/models'; + import { callbacks } from '../../../../lib/callbacks'; -import { settings } from '../../../settings'; -import { LivechatVisitors } from '../../../models'; +import { settings } from '../../../settings/server'; function validateMessage(message, room) { // skips this callback if the message was edited @@ -40,7 +41,7 @@ callbacks.add( const msgEmails = message.msg.match(emailRegexp); if (msgEmails || msgPhones) { - LivechatVisitors.saveGuestEmailPhoneById(room.v._id, msgEmails, msgPhones); + Promise.await(LivechatVisitors.saveGuestEmailPhoneById(room.v._id, msgEmails, msgPhones)); callbacks.run('livechat.leadCapture', room); } diff --git a/apps/meteor/app/livechat/server/hooks/markRoomNotResponded.js b/apps/meteor/app/livechat/server/hooks/markRoomNotResponded.js index 2503b894280c..78bf971d2889 100644 --- a/apps/meteor/app/livechat/server/hooks/markRoomNotResponded.js +++ b/apps/meteor/app/livechat/server/hooks/markRoomNotResponded.js @@ -1,5 +1,5 @@ import { callbacks } from '../../../../lib/callbacks'; -import { LivechatRooms } from '../../../models'; +import { LivechatRooms } from '../../../models/server'; callbacks.add( 'afterSaveMessage', diff --git a/apps/meteor/app/livechat/server/hooks/markRoomResponded.js b/apps/meteor/app/livechat/server/hooks/markRoomResponded.js index 5b905cfb4fe7..78049d6d3ccd 100644 --- a/apps/meteor/app/livechat/server/hooks/markRoomResponded.js +++ b/apps/meteor/app/livechat/server/hooks/markRoomResponded.js @@ -1,5 +1,5 @@ import { callbacks } from '../../../../lib/callbacks'; -import { LivechatRooms } from '../../../models'; +import { LivechatRooms } from '../../../models/server'; callbacks.add( 'afterSaveMessage', diff --git a/apps/meteor/app/livechat/server/hooks/offlineMessage.js b/apps/meteor/app/livechat/server/hooks/offlineMessage.js index 51898a917d45..4b12e9edf854 100644 --- a/apps/meteor/app/livechat/server/hooks/offlineMessage.js +++ b/apps/meteor/app/livechat/server/hooks/offlineMessage.js @@ -1,5 +1,5 @@ import { callbacks } from '../../../../lib/callbacks'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { Livechat } from '../lib/Livechat'; callbacks.add( diff --git a/apps/meteor/app/livechat/server/hooks/offlineMessageToChannel.js b/apps/meteor/app/livechat/server/hooks/offlineMessageToChannel.js index 5ea708c470d5..83a79c6b97af 100644 --- a/apps/meteor/app/livechat/server/hooks/offlineMessageToChannel.js +++ b/apps/meteor/app/livechat/server/hooks/offlineMessageToChannel.js @@ -1,9 +1,9 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { callbacks } from '../../../../lib/callbacks'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { sendMessage } from '../../../lib'; -import { LivechatDepartment, Rooms, Users } from '../../../models'; +import { LivechatDepartment, Rooms, Users } from '../../../models/server'; callbacks.add( 'livechat.offlineMessage', diff --git a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.js b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.js index ebc4a018144c..e3453f6e1451 100644 --- a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.js +++ b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.js @@ -1,10 +1,10 @@ import moment from 'moment'; +import { LivechatBusinessHours, LivechatDepartment } from '@rocket.chat/models'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { callbacks } from '../../../../lib/callbacks'; import { LivechatRooms, Messages } from '../../../models/server'; import { businessHourManager } from '../business-hour'; -import { LivechatBusinessHours, LivechatDepartment } from '../../../models/server/raw'; const getSecondsWhenOfficeHoursIsDisabled = (room, agentLastMessage) => moment(new Date(room.closedAt)).diff(moment(new Date(agentLastMessage.ts)), 'seconds'); diff --git a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.js b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.js index f4f641a474ff..fbedddc12672 100644 --- a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.js +++ b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.js @@ -1,5 +1,5 @@ import { callbacks } from '../../../../lib/callbacks'; -import { LivechatRooms } from '../../../models'; +import { LivechatRooms } from '../../../models/server'; import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload'; callbacks.add( diff --git a/apps/meteor/app/livechat/server/hooks/saveContactLastChat.js b/apps/meteor/app/livechat/server/hooks/saveContactLastChat.js index 0d87723b7079..9745b09930e3 100644 --- a/apps/meteor/app/livechat/server/hooks/saveContactLastChat.js +++ b/apps/meteor/app/livechat/server/hooks/saveContactLastChat.js @@ -13,7 +13,7 @@ callbacks.add( _id, ts: new Date(), }; - Livechat.updateLastChat(guestId, lastChat); + Promise.await(Livechat.updateLastChat(guestId, lastChat)); }, callbacks.priority.MEDIUM, 'livechat-save-last-chat', diff --git a/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts b/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts index a5c43e7397cb..fe726a73a410 100644 --- a/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts +++ b/apps/meteor/app/livechat/server/hooks/saveLastMessageToInquiry.ts @@ -1,7 +1,7 @@ import { isOmnichannelRoom, isEditedMessage } from '@rocket.chat/core-typings'; +import { LivechatInquiry } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; -import { LivechatInquiry } from '../../../models/server/raw'; import { settings } from '../../../settings/server'; import { RoutingManager } from '../lib/RoutingManager'; diff --git a/apps/meteor/app/livechat/server/hooks/saveLastVisitorMessageTs.js b/apps/meteor/app/livechat/server/hooks/saveLastVisitorMessageTs.js index 0748ae13e47e..40d6bd4c68d8 100644 --- a/apps/meteor/app/livechat/server/hooks/saveLastVisitorMessageTs.js +++ b/apps/meteor/app/livechat/server/hooks/saveLastVisitorMessageTs.js @@ -1,5 +1,5 @@ import { callbacks } from '../../../../lib/callbacks'; -import { LivechatRooms } from '../../../models'; +import { LivechatRooms } from '../../../models/server'; callbacks.add( 'afterSaveMessage', diff --git a/apps/meteor/app/livechat/server/hooks/sendToCRM.js b/apps/meteor/app/livechat/server/hooks/sendToCRM.js index 0e63bb6c6393..c54b761717c9 100644 --- a/apps/meteor/app/livechat/server/hooks/sendToCRM.js +++ b/apps/meteor/app/livechat/server/hooks/sendToCRM.js @@ -1,6 +1,6 @@ import { settings } from '../../../settings/server'; import { callbacks } from '../../../../lib/callbacks'; -import { Messages, LivechatRooms } from '../../../models'; +import { Messages, LivechatRooms } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload'; @@ -41,7 +41,7 @@ function sendToCRM(type, room, includeMessages = true) { return room; } - const postData = Livechat.getLivechatRoomGuestInfo(room); + const postData = Promise.await(Livechat.getLivechatRoomGuestInfo(room)); postData.type = type; diff --git a/apps/meteor/app/livechat/server/hooks/sendToFacebook.js b/apps/meteor/app/livechat/server/hooks/sendToFacebook.js index 334a68c8a22f..d45f56c2b366 100644 --- a/apps/meteor/app/livechat/server/hooks/sendToFacebook.js +++ b/apps/meteor/app/livechat/server/hooks/sendToFacebook.js @@ -1,5 +1,5 @@ import { callbacks } from '../../../../lib/callbacks'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import OmniChannel from '../lib/OmniChannel'; import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload'; diff --git a/apps/meteor/app/livechat/server/hooks/sendTranscriptOnClose.js b/apps/meteor/app/livechat/server/hooks/sendTranscriptOnClose.js index 6744d4a71bb2..597d656b2f0e 100644 --- a/apps/meteor/app/livechat/server/hooks/sendTranscriptOnClose.js +++ b/apps/meteor/app/livechat/server/hooks/sendTranscriptOnClose.js @@ -1,6 +1,6 @@ import { callbacks } from '../../../../lib/callbacks'; import { Livechat } from '../lib/Livechat'; -import { LivechatRooms } from '../../../models'; +import { LivechatRooms } from '../../../models/server'; const sendTranscriptOnClose = (room) => { const { _id: rid, transcriptRequest, v: { token } = {} } = room; @@ -9,7 +9,8 @@ const sendTranscriptOnClose = (room) => { } const { email, subject, requestedBy: user } = transcriptRequest; - Livechat.sendTranscript({ token, rid, email, subject, user }); + // TODO: refactor this to use normal await + Promise.await(Livechat.sendTranscript({ token, rid, email, subject, user })); LivechatRooms.removeTranscriptRequestByRoomId(rid); diff --git a/apps/meteor/app/livechat/server/index.js b/apps/meteor/app/livechat/server/index.js index a6f03ba9c4dd..c44390cd6de6 100644 --- a/apps/meteor/app/livechat/server/index.js +++ b/apps/meteor/app/livechat/server/index.js @@ -10,7 +10,6 @@ import './hooks/leadCapture'; import './hooks/markRoomResponded'; import './hooks/offlineMessage'; import './hooks/offlineMessageToChannel'; -import './hooks/RDStation'; import './hooks/saveAnalyticsData'; import './hooks/sendToCRM'; import './hooks/sendToFacebook'; @@ -32,7 +31,6 @@ import './methods/getAgentData'; import './methods/getAgentOverviewData'; import './methods/getAnalyticsChartData'; import './methods/getAnalyticsOverviewData'; -import './methods/getInitialData'; import './methods/getNextAgent'; import './methods/getRoutingConfig'; import './methods/loadHistory'; @@ -61,8 +59,6 @@ import './methods/sendFileLivechatMessage'; import './methods/sendOfflineMessage'; import './methods/setCustomField'; import './methods/setDepartmentForVisitor'; -import './methods/startVideoCall'; -import './methods/startFileUploadRoom'; import './methods/transfer'; import './methods/webhookTest'; import './methods/setUpConnection'; @@ -84,7 +80,6 @@ import './sendMessageBySMS'; import './api'; import './api/rest'; import './externalFrame'; -import './lib/messageTypes'; import './methods/saveBusinessHour'; export { Livechat } from './lib/Livechat'; diff --git a/apps/meteor/app/livechat/server/lib/Analytics.js b/apps/meteor/app/livechat/server/lib/Analytics.js index 1974a5be1985..7a18f19d9d94 100644 --- a/apps/meteor/app/livechat/server/lib/Analytics.js +++ b/apps/meteor/app/livechat/server/lib/Analytics.js @@ -1,8 +1,8 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import moment from 'moment'; +import moment from 'moment-timezone'; +import { LivechatRooms as LivechatRoomsRaw } from '@rocket.chat/models'; -import { LivechatRooms } from '../../../models'; -import { LivechatRooms as LivechatRoomsRaw } from '../../../models/server/raw'; +import { LivechatRooms } from '../../../models/server'; import { secondsToHHMMSS } from '../../../utils/server'; import { getTimezone } from '../../../utils/server/lib/getTimezone'; import { Logger } from '../../../logger'; diff --git a/apps/meteor/app/livechat/server/lib/Contacts.js b/apps/meteor/app/livechat/server/lib/Contacts.js deleted file mode 100644 index 980828f0a360..000000000000 --- a/apps/meteor/app/livechat/server/lib/Contacts.js +++ /dev/null @@ -1,70 +0,0 @@ -import { check } from 'meteor/check'; -import s from 'underscore.string'; - -import { LivechatVisitors, LivechatCustomField, LivechatRooms, Rooms, LivechatInquiry, Subscriptions } from '../../../models'; - -export const Contacts = { - registerContact({ token, name, email, phone, username, customFields = {}, contactManager = {} } = {}) { - check(token, String); - - const visitorEmail = s.trim(email).toLowerCase(); - - let contactId; - const updateUser = { - $set: { - token, - }, - }; - - const user = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } }); - - if (user) { - contactId = user._id; - } else { - if (!username) { - username = LivechatVisitors.getNextVisitorUsername(); - } - - let existingUser = null; - - if (visitorEmail !== '' && (existingUser = LivechatVisitors.findOneGuestByEmailAddress(visitorEmail))) { - contactId = existingUser._id; - } else { - const userData = { - username, - ts: new Date(), - }; - - contactId = LivechatVisitors.insert(userData); - } - } - - updateUser.$set.name = name; - updateUser.$set.phone = (phone && [{ phoneNumber: phone }]) || null; - updateUser.$set.visitorEmails = (visitorEmail && [{ address: visitorEmail }]) || null; - - const allowedCF = LivechatCustomField.find({ scope: 'visitor' }, { fields: { _id: 1 } }).map(({ _id }) => _id); - - const livechatData = Object.keys(customFields) - .filter((key) => allowedCF.includes(key) && customFields[key] !== '' && customFields[key] !== undefined) - .reduce((obj, key) => { - obj[key] = customFields[key]; - return obj; - }, {}); - - updateUser.$set.livechatData = livechatData; - updateUser.$set.contactManager = (contactManager?.username && { username: contactManager.username }) || null; - - LivechatVisitors.updateById(contactId, updateUser); - - const rooms = LivechatRooms.findByVisitorId(contactId).fetch(); - - rooms?.length && - rooms.forEach((room) => { - const { _id: rid } = room; - Rooms.setFnameById(rid, name) && LivechatInquiry.setNameByRoomId(rid, name) && Subscriptions.updateDisplayNameByRoomId(rid, name); - }); - - return contactId; - }, -}; diff --git a/apps/meteor/app/livechat/server/lib/Contacts.ts b/apps/meteor/app/livechat/server/lib/Contacts.ts new file mode 100644 index 000000000000..45fbf828fdb2 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/Contacts.ts @@ -0,0 +1,109 @@ +import { check } from 'meteor/check'; +import { Meteor } from 'meteor/meteor'; +import s from 'underscore.string'; +import { MatchKeysAndValues, OnlyFieldsOfType } from 'mongodb'; +import { LivechatVisitors, Users, LivechatRooms } from '@rocket.chat/models'; +import { ILivechatCustomField, ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings'; + +import { LivechatCustomField, Rooms, LivechatInquiry, Subscriptions } from '../../../models/server'; + +type RegisterContactProps = { + _id?: string; + token: string; + name: string; + username?: string; + email?: string; + phone?: string; + customFields?: Record; + contactManager?: { + username: string; + }; +}; + +export const Contacts = { + async registerContact({ + token, + name, + email = '', + phone, + username, + customFields = {}, + contactManager, + }: RegisterContactProps): Promise { + check(token, String); + + const visitorEmail = s.trim(email).toLowerCase(); + + if (contactManager?.username) { + // verify if the user exists with this username and has a livechat-agent role + const user = await Users.findOneByUsername(contactManager.username, { projection: { roles: 1 } }); + if (!user) { + throw new Meteor.Error('error-contact-manager-not-found', `No user found with username ${contactManager.username}`); + } + if (!user.roles || !Array.isArray(user.roles) || !user.roles.includes('livechat-agent')) { + throw new Meteor.Error('error-invalid-contact-manager', 'The contact manager must have the role "livechat-agent"'); + } + } + + let contactId; + + const user = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); + + if (user) { + contactId = user._id; + } else { + if (!username) { + username = await LivechatVisitors.getNextVisitorUsername(); + } + + let existingUser = null; + + if (visitorEmail !== '' && (existingUser = await LivechatVisitors.findOneGuestByEmailAddress(visitorEmail))) { + contactId = existingUser._id; + } else { + const userData = { + username, + ts: new Date(), + token, + }; + + contactId = (await LivechatVisitors.insertOne(userData)).insertedId; + } + } + + const allowedCF: ILivechatCustomField['_id'][] = LivechatCustomField.find({ scope: 'visitor' }, { fields: { _id: 1 } }).map( + ({ _id }: ILivechatCustomField) => _id, + ); + + const livechatData = Object.keys(customFields) + .filter((key) => allowedCF.includes(key) && customFields[key] !== '' && customFields[key] !== undefined) + .reduce((obj: Record, key) => { + obj[key] = customFields[key]; + return obj; + }, {}); + + const updateUser: { $set: MatchKeysAndValues; $unset?: OnlyFieldsOfType } = { + $set: { + token, + name, + livechatData, + ...(phone && { phone: [{ phoneNumber: phone }] }), + ...(visitorEmail && { visitorEmails: [{ address: visitorEmail }] }), + ...(contactManager?.username && { contactManager: { username: contactManager.username } }), + }, + ...(!contactManager?.username && { $unset: { contactManager: 1 } }), + }; + + await LivechatVisitors.updateOne({ _id: contactId }, updateUser); + + const rooms: IOmnichannelRoom[] = await LivechatRooms.findByVisitorId(contactId, {}).toArray(); + + rooms?.length && + rooms.forEach((room) => { + const { _id: rid } = room; + Rooms.setFnameById(rid, name) && LivechatInquiry.setNameByRoomId(rid, name) && Subscriptions.updateDisplayNameByRoomId(rid, name); + }); + + return contactId; + }, +}; diff --git a/apps/meteor/app/livechat/server/lib/Helper.js b/apps/meteor/app/livechat/server/lib/Helper.js index cf43982f9242..3a51f2343d1e 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.js +++ b/apps/meteor/app/livechat/server/lib/Helper.js @@ -19,7 +19,7 @@ import { Livechat } from './Livechat'; import { RoutingManager } from './RoutingManager'; import { callbacks } from '../../../../lib/callbacks'; import { Logger } from '../../../logger'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { Apps, AppEvents } from '../../../apps/server'; import notifications from '../../../notifications/server/lib/Notifications'; import { sendNotification } from '../../../lib/server'; @@ -531,7 +531,7 @@ export const forwardRoomToDepartment = async (room, guest, transferData) => { } const { token } = guest; - Livechat.setDepartmentForGuest({ token, department: departmentId }); + await Livechat.setDepartmentForGuest({ token, department: departmentId }); logger.debug(`Department for visitor with token ${token} was updated to ${departmentId}`); return true; diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 3c387b73f532..d365e7c95fd0 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -9,6 +9,7 @@ import _ from 'underscore'; import s from 'underscore.string'; import moment from 'moment-timezone'; import UAParser from 'ua-parser-js'; +import { Users as UsersRaw, LivechatVisitors } from '@rocket.chat/models'; import { QueueManager } from './QueueManager'; import { RoutingManager } from './RoutingManager'; @@ -26,7 +27,6 @@ import { LivechatDepartmentAgents, LivechatDepartment, LivechatCustomField, - LivechatVisitors, LivechatInquiry, } from '../../../models/server'; import { Logger } from '../../../logger/server'; @@ -40,9 +40,9 @@ import { normalizeTransferredByData, parseAgentCustomFields, updateDepartmentAge import { Apps, AppEvents } from '../../../apps/server'; import { businessHourManager } from '../business-hour'; import notifications from '../../../notifications/server/lib/Notifications'; -import { Users as UsersRaw } from '../../../models/server/raw'; import { addUserRoles } from '../../../../server/lib/roles/addUserRoles'; import { removeUserFromRoles } from '../../../../server/lib/roles/removeUserFromRoles'; +import { VideoConf } from '../../../../server/sdk'; const logger = new Logger('Livechat'); @@ -172,8 +172,8 @@ export const Livechat = { } if (guest.department && !LivechatDepartment.findOneById(guest.department)) { - LivechatVisitors.removeDepartmentById(guest._id); - guest = LivechatVisitors.findOneById(guest._id); + await LivechatVisitors.removeDepartmentById(guest._id); + guest = await LivechatVisitors.findOneById(guest._id); } if (room == null) { @@ -272,7 +272,7 @@ export const Livechat = { return true; }, - registerGuest({ id, token, name, email, department, phone, username, connectionData, status = 'online' } = {}) { + async registerGuest({ id, token, name, email, department, phone, username, connectionData, status = 'online' } = {}) { check(token, String); check(id, Match.Maybe(String)); @@ -307,24 +307,24 @@ export const Livechat = { updateUser.$set.department = dep._id; } - const user = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } }); + const user = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); let existingUser = null; if (user) { Livechat.logger.debug('Found matching user by token'); userId = user._id; - } else if (phone?.number && (existingUser = LivechatVisitors.findOneVisitorByPhone(phone.number))) { + } else if (phone?.number && (existingUser = await LivechatVisitors.findOneVisitorByPhone(phone.number))) { Livechat.logger.debug('Found matching user by phone number'); userId = existingUser._id; // Don't change token when matching by phone number, use current visitor token updateUser.$set.token = existingUser.token; - } else if (email && (existingUser = LivechatVisitors.findOneGuestByEmailAddress(email))) { + } else if (email && (existingUser = await LivechatVisitors.findOneGuestByEmailAddress(email))) { Livechat.logger.debug('Found matching user by email'); userId = existingUser._id; } else { Livechat.logger.debug(`No matches found. Attempting to create new user with token ${token}`); if (!username) { - username = LivechatVisitors.getNextVisitorUsername(); + username = await LivechatVisitors.getNextVisitorUsername(); } const userData = { @@ -344,15 +344,15 @@ export const Livechat = { } } - userId = LivechatVisitors.insert(userData); + userId = (await LivechatVisitors.insertOne(userData)).insertedId; } - LivechatVisitors.updateById(userId, updateUser); + await LivechatVisitors.updateById(userId, updateUser); return userId; }, - setDepartmentForGuest({ token, department } = {}) { + async setDepartmentForGuest({ token, department } = {}) { check(token, String); check(department, String); @@ -371,14 +371,14 @@ export const Livechat = { }); } - const user = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } }); + const user = await LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } }); if (user) { return LivechatVisitors.updateById(user._id, updateUser); } return false; }, - saveGuest({ _id, name, email, phone, livechatData = {} }, userId) { + async saveGuest({ _id, name, email, phone, livechatData = {} }, userId) { Livechat.logger.debug(`Saving data for visitor ${_id}`); const updateData = {}; @@ -411,7 +411,7 @@ export const Livechat = { }); updateData.livechatData = customFields; } - const ret = LivechatVisitors.saveGuestById(_id, updateData); + const ret = await LivechatVisitors.saveGuestById(_id, updateData); Meteor.defer(() => { Apps.triggerEvent(AppEvents.IPostLivechatGuestSaved, _id); @@ -506,7 +506,7 @@ export const Livechat = { return LivechatRooms.removeById(rid); }, - setCustomFields({ token, key, value, overwrite } = {}) { + async setCustomFields({ token, key, value, overwrite } = {}) { check(token, String); check(key, String); check(value, String); @@ -554,7 +554,6 @@ export const Livechat = { 'Livechat_offline_form_unavailable', 'Livechat_display_offline_form', 'Omnichannel_call_provider', - 'Jitsi_Enabled', 'Language', 'Livechat_enable_transcript', 'Livechat_transcript_message', @@ -636,7 +635,8 @@ export const Livechat = { forwardOpenChats(userId) { Livechat.logger.debug(`Transferring open chats for user ${userId}`); LivechatRooms.findOpenByAgent(userId).forEach((room) => { - const guest = LivechatVisitors.findOneById(room.v._id); + // TODO: refactor to use normal await + const guest = Promise.await(LivechatVisitors.findOneById(room.v._id)); const user = Users.findOneById(userId); const { _id, username, name } = user; const transferredBy = normalizeTransferredByData({ _id, username, name }, room); @@ -811,8 +811,8 @@ export const Livechat = { } }, - getLivechatRoomGuestInfo(room) { - const visitor = LivechatVisitors.findOneById(room.v._id); + async getLivechatRoomGuestInfo(room) { + const visitor = await LivechatVisitors.findOneById(room.v._id); const agent = Users.findOneById(room.servedBy && room.servedBy._id); const ua = new UAParser(); @@ -925,6 +925,7 @@ export const Livechat = { Users.removeLivechatData(_id); this.setUserStatusLivechat(_id, 'not-available'); LivechatDepartmentAgents.removeByAgentId(_id); + Promise.await(LivechatVisitors.removeContactManagerByUsername(username)); return true; } @@ -945,16 +946,16 @@ export const Livechat = { return removeUserFromRoles(user._id, ['livechat-manager']); }, - removeGuest(_id) { + async removeGuest(_id) { check(_id, String); - const guest = LivechatVisitors.findOneById(_id); + const guest = await LivechatVisitors.findOneById(_id, { projection: { _id: 1 } }); if (!guest) { throw new Meteor.Error('error-invalid-guest', 'Invalid guest', { method: 'livechat:removeGuest', }); } - this.cleanGuestHistory(_id); + await this.cleanGuestHistory(_id); return LivechatVisitors.removeById(_id); }, @@ -970,8 +971,8 @@ export const Livechat = { return user; }, - cleanGuestHistory(_id) { - const guest = LivechatVisitors.findOneById(_id); + async cleanGuestHistory(_id) { + const guest = await LivechatVisitors.findOneById(_id); if (!guest) { throw new Meteor.Error('error-invalid-guest', 'Invalid guest', { method: 'livechat:cleanGuestHistory', @@ -1143,15 +1144,15 @@ export const Livechat = { }); }, - sendTranscript({ token, rid, email, subject, user }) { + async sendTranscript({ token, rid, email, subject, user }) { check(rid, String); check(email, String); Livechat.logger.debug(`Sending conversation transcript of room ${rid} to user with token ${token}`); const room = LivechatRooms.findOneById(rid); - const visitor = LivechatVisitors.getVisitorByToken(token, { - fields: { _id: 1, token: 1, language: 1, username: 1, name: 1 }, + const visitor = await LivechatVisitors.getVisitorByToken(token, { + projection: { _id: 1, token: 1, language: 1, username: 1, name: 1 }, }); const userLanguage = (visitor && visitor.language) || settings.get('Language') || 'en'; const timezone = getTimezone(user); @@ -1401,17 +1402,21 @@ export const Livechat = { return LivechatRooms.findOneById(roomId); }, - updateLastChat(contactId, lastChat) { + async updateLastChat(contactId, lastChat) { const updateUser = { $set: { lastChat, }, }; - LivechatVisitors.updateById(contactId, updateUser); + await LivechatVisitors.updateById(contactId, updateUser); }, updateCallStatus(callId, rid, status, user) { Rooms.setCallStatus(rid, status); if (status === 'ended' || status === 'declined') { + if (Promise.await(VideoConf.declineLivechatCall(callId))) { + return; + } + return updateMessage({ _id: callId, msg: status, actionLinks: [], webRtcCallEndTs: new Date() }, user); } }, diff --git a/apps/meteor/app/livechat/server/lib/OmniChannel.js b/apps/meteor/app/livechat/server/lib/OmniChannel.js index 9b320364b12c..0a4651114890 100644 --- a/apps/meteor/app/livechat/server/lib/OmniChannel.js +++ b/apps/meteor/app/livechat/server/lib/OmniChannel.js @@ -1,6 +1,6 @@ import { HTTP } from 'meteor/http'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; const gatewayURL = 'https://omni.rocket.chat'; diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.js b/apps/meteor/app/livechat/server/lib/QueueManager.js index b5faaa016903..5ffea4cb7881 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.js +++ b/apps/meteor/app/livechat/server/lib/QueueManager.js @@ -68,7 +68,7 @@ export const QueueManager = { ); logger.debug(`Generated inquiry for visitor ${guest._id} with id ${inquiry._id} [Not queued]`); - LivechatRooms.updateRoomCount(); + await LivechatRooms.updateRoomCount(); await queueInquiry(room, inquiry, agent); logger.debug(`Inquiry ${inquiry._id} queued`); diff --git a/apps/meteor/app/livechat/server/lib/analytics/agents.js b/apps/meteor/app/livechat/server/lib/analytics/agents.js index f23025cabeb6..1f1a39f85c5c 100644 --- a/apps/meteor/app/livechat/server/lib/analytics/agents.js +++ b/apps/meteor/app/livechat/server/lib/analytics/agents.js @@ -1,4 +1,4 @@ -import { LivechatRooms, LivechatAgentActivity } from '../../../../models/server/raw'; +import { LivechatRooms, LivechatAgentActivity } from '@rocket.chat/models'; const findAllAverageServiceTimeAsync = async ({ start, end, options = {} }) => { if (!start || !end) { diff --git a/apps/meteor/app/livechat/server/lib/analytics/dashboards.js b/apps/meteor/app/livechat/server/lib/analytics/dashboards.js index 7819499ea9e9..71781bfa81c4 100644 --- a/apps/meteor/app/livechat/server/lib/analytics/dashboards.js +++ b/apps/meteor/app/livechat/server/lib/analytics/dashboards.js @@ -1,7 +1,7 @@ import moment from 'moment'; +import { LivechatRooms, Users, LivechatVisitors, LivechatAgentActivity } from '@rocket.chat/models'; -import { LivechatRooms, Users, LivechatVisitors, LivechatAgentActivity } from '../../../../models/server/raw'; -import { settings } from '../../../../settings'; +import { settings } from '../../../../settings/server'; import { Livechat } from '../Livechat'; import { secondsToHHMMSS } from '../../../../utils/server'; import { diff --git a/apps/meteor/app/livechat/server/lib/analytics/departments.js b/apps/meteor/app/livechat/server/lib/analytics/departments.js index d1cd5904a869..bf7a01adf0b8 100644 --- a/apps/meteor/app/livechat/server/lib/analytics/departments.js +++ b/apps/meteor/app/livechat/server/lib/analytics/departments.js @@ -1,4 +1,4 @@ -import { LivechatRooms, Messages } from '../../../../models/server/raw'; +import { LivechatRooms, Messages } from '@rocket.chat/models'; export const findAllRoomsAsync = async ({ start, end, answered, departmentId, options = {} }) => { if (!start || !end) { diff --git a/apps/meteor/app/livechat/server/lib/messageTypes.js b/apps/meteor/app/livechat/server/lib/messageTypes.js deleted file mode 100644 index dadb7e69594c..000000000000 --- a/apps/meteor/app/livechat/server/lib/messageTypes.js +++ /dev/null @@ -1,26 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; - -import { actionLinks } from '../../../action-links/server'; -import { api } from '../../../../server/sdk/api'; -import { Messages, LivechatRooms } from '../../../models/server'; -import { settings } from '../../../settings/server'; -import { Livechat } from './Livechat'; - -actionLinks.register('denyLivechatCall', function (message /* , params*/) { - const user = Meteor.user(); - - Messages.createWithTypeRoomIdMessageAndUser('command', message.rid, 'endCall', user); - api.broadcast('notify.deleteMessage', message.rid, { _id: message._id }); - - const language = user.language || settings.get('Language') || 'en'; - - Livechat.closeRoom({ - user, - room: LivechatRooms.findOneById(message.rid), - comment: TAPi18n.__('Videocall_declined', { lng: language }), - }); - Meteor.defer(() => { - Messages.setHiddenById(message._id); - }); -}); diff --git a/apps/meteor/app/livechat/server/methods/closeByVisitor.js b/apps/meteor/app/livechat/server/methods/closeByVisitor.js index 03c415dbb448..022e614bb7f0 100644 --- a/apps/meteor/app/livechat/server/methods/closeByVisitor.js +++ b/apps/meteor/app/livechat/server/methods/closeByVisitor.js @@ -1,13 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { LivechatVisitors } from '@rocket.chat/models'; -import { settings } from '../../../settings'; -import { LivechatRooms, LivechatVisitors } from '../../../models'; +import { settings } from '../../../settings/server'; +import { LivechatRooms } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; Meteor.methods({ - 'livechat:closeByVisitor'({ roomId, token }) { - const visitor = LivechatVisitors.getVisitorByToken(token); + async 'livechat:closeByVisitor'({ roomId, token }) { + const visitor = await LivechatVisitors.getVisitorByToken(token); const language = (visitor && visitor.language) || settings.get('Language') || 'en'; diff --git a/apps/meteor/app/livechat/server/methods/closeRoom.js b/apps/meteor/app/livechat/server/methods/closeRoom.js index 773b22b5cdd4..9eeccfac638a 100644 --- a/apps/meteor/app/livechat/server/methods/closeRoom.js +++ b/apps/meteor/app/livechat/server/methods/closeRoom.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; -import { Subscriptions, LivechatRooms } from '../../../models'; +import { Subscriptions, LivechatRooms } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; Meteor.methods({ diff --git a/apps/meteor/app/livechat/server/methods/discardTranscript.js b/apps/meteor/app/livechat/server/methods/discardTranscript.js index 5943ebb56ba3..099c8ad002d8 100644 --- a/apps/meteor/app/livechat/server/methods/discardTranscript.js +++ b/apps/meteor/app/livechat/server/methods/discardTranscript.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import { hasPermission } from '../../../authorization'; -import { LivechatRooms } from '../../../models'; +import { LivechatRooms } from '../../../models/server'; Meteor.methods({ 'livechat:discardTranscript'(rid) { diff --git a/apps/meteor/app/livechat/server/methods/facebook.js b/apps/meteor/app/livechat/server/methods/facebook.js index 90d27222aa2e..86cd7132cc1a 100644 --- a/apps/meteor/app/livechat/server/methods/facebook.js +++ b/apps/meteor/app/livechat/server/methods/facebook.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; import { SystemLogger } from '../../../../server/lib/logger/system'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import OmniChannel from '../lib/OmniChannel'; import { Settings } from '../../../models/server'; diff --git a/apps/meteor/app/livechat/server/methods/getAgentData.js b/apps/meteor/app/livechat/server/methods/getAgentData.js index dd2d6a3b235d..1cc6993974c6 100644 --- a/apps/meteor/app/livechat/server/methods/getAgentData.js +++ b/apps/meteor/app/livechat/server/methods/getAgentData.js @@ -1,15 +1,16 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; +import { LivechatVisitors } from '@rocket.chat/models'; -import { Users, LivechatRooms, LivechatVisitors } from '../../../models'; +import { Users, LivechatRooms } from '../../../models/server'; Meteor.methods({ - 'livechat:getAgentData'({ roomId, token }) { + async 'livechat:getAgentData'({ roomId, token }) { check(roomId, String); check(token, String); const room = LivechatRooms.findOneById(roomId); - const visitor = LivechatVisitors.getVisitorByToken(token); + const visitor = await LivechatVisitors.getVisitorByToken(token); if (!room || room.t !== 'l' || !room.v || room.v.token !== visitor.token) { throw new Meteor.Error('error-invalid-room', 'Invalid room'); diff --git a/apps/meteor/app/livechat/server/methods/getAgentOverviewData.js b/apps/meteor/app/livechat/server/methods/getAgentOverviewData.js index d60d36957530..8df403e421e1 100644 --- a/apps/meteor/app/livechat/server/methods/getAgentOverviewData.js +++ b/apps/meteor/app/livechat/server/methods/getAgentOverviewData.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; import { Livechat } from '../lib/Livechat'; -import { Users } from '../../../models'; +import { Users } from '../../../models/server'; Meteor.methods({ 'livechat:getAgentOverviewData'(options) { diff --git a/apps/meteor/app/livechat/server/methods/getAnalyticsChartData.js b/apps/meteor/app/livechat/server/methods/getAnalyticsChartData.js index caa86c650899..8f9706e93bf7 100644 --- a/apps/meteor/app/livechat/server/methods/getAnalyticsChartData.js +++ b/apps/meteor/app/livechat/server/methods/getAnalyticsChartData.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; -import { Users } from '../../../models'; +import { Users } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; Meteor.methods({ diff --git a/apps/meteor/app/livechat/server/methods/getAnalyticsOverviewData.js b/apps/meteor/app/livechat/server/methods/getAnalyticsOverviewData.js index 1ff65fce8522..407be7c7596e 100644 --- a/apps/meteor/app/livechat/server/methods/getAnalyticsOverviewData.js +++ b/apps/meteor/app/livechat/server/methods/getAnalyticsOverviewData.js @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; -import { Users } from '../../../models'; -import { settings } from '../../../settings'; +import { Users } from '../../../models/server'; +import { settings } from '../../../settings/server'; import { Livechat } from '../lib/Livechat'; Meteor.methods({ diff --git a/apps/meteor/app/livechat/server/methods/getCustomFields.js b/apps/meteor/app/livechat/server/methods/getCustomFields.js index df39364bccee..f18f94b6df2d 100644 --- a/apps/meteor/app/livechat/server/methods/getCustomFields.js +++ b/apps/meteor/app/livechat/server/methods/getCustomFields.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { LivechatCustomField } from '../../../models'; +import { LivechatCustomField } from '../../../models/server'; Meteor.methods({ 'livechat:getCustomFields'() { diff --git a/apps/meteor/app/livechat/server/methods/getFirstRoomMessage.js b/apps/meteor/app/livechat/server/methods/getFirstRoomMessage.js index 75665226b538..3c9cc0601577 100644 --- a/apps/meteor/app/livechat/server/methods/getFirstRoomMessage.js +++ b/apps/meteor/app/livechat/server/methods/getFirstRoomMessage.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { LivechatRooms, Messages } from '../../../models'; +import { LivechatRooms, Messages } from '../../../models/server'; import { hasPermission } from '../../../authorization'; Meteor.methods({ diff --git a/apps/meteor/app/livechat/server/methods/getInitialData.js b/apps/meteor/app/livechat/server/methods/getInitialData.js deleted file mode 100644 index e46c66e042f7..000000000000 --- a/apps/meteor/app/livechat/server/methods/getInitialData.js +++ /dev/null @@ -1,112 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; - -import { LivechatRooms, Users, LivechatDepartment, LivechatVisitors } from '../../../models/server'; -import { LivechatTrigger } from '../../../models/server/raw'; -import { Livechat } from '../lib/Livechat'; -import { deprecationWarning } from '../../../api/server/helpers/deprecationWarning'; - -Meteor.methods({ - async 'livechat:getInitialData'(visitorToken, departmentId) { - const info = { - enabled: null, - title: null, - color: null, - registrationForm: null, - room: null, - visitor: null, - triggers: [], - departments: [], - allowSwitchingDepartments: null, - online: true, - offlineColor: null, - offlineMessage: null, - offlineSuccessMessage: null, - offlineUnavailableMessage: null, - displayOfflineForm: null, - videoCall: null, - fileUpload: null, - conversationFinishedMessage: null, - conversationFinishedText: null, - nameFieldRegistrationForm: null, - emailFieldRegistrationForm: null, - registrationFormMessage: null, - showConnecting: false, - }; - - const options = { - fields: { - name: 1, - t: 1, - cl: 1, - u: 1, - usernames: 1, - v: 1, - servedBy: 1, - departmentId: 1, - }, - }; - const room = departmentId - ? LivechatRooms.findOpenByVisitorTokenAndDepartmentId(visitorToken, departmentId, options).fetch() - : LivechatRooms.findOpenByVisitorToken(visitorToken, options).fetch(); - if (room && room.length > 0) { - info.room = room[0]; - } - - const visitor = LivechatVisitors.getVisitorByToken(visitorToken, { - fields: { - name: 1, - username: 1, - visitorEmails: 1, - department: 1, - }, - }); - - if (room) { - info.visitor = visitor; - } - - const initSettings = Livechat.getInitSettings(); - - info.title = initSettings.Livechat_title; - info.color = initSettings.Livechat_title_color; - info.enabled = initSettings.Livechat_enabled; - info.registrationForm = initSettings.Livechat_registration_form; - info.offlineTitle = initSettings.Livechat_offline_title; - info.offlineColor = initSettings.Livechat_offline_title_color; - info.offlineMessage = initSettings.Livechat_offline_message; - info.offlineSuccessMessage = initSettings.Livechat_offline_success_message; - info.offlineUnavailableMessage = initSettings.Livechat_offline_form_unavailable; - info.displayOfflineForm = initSettings.Livechat_display_offline_form; - info.language = initSettings.Language; - info.videoCall = initSettings.Omnichannel_call_provider === 'Jitsi' && initSettings.Jitsi_Enabled === true; - info.fileUpload = initSettings.Livechat_fileupload_enabled && initSettings.FileUpload_Enabled; - info.transcript = initSettings.Livechat_enable_transcript; - info.transcriptMessage = initSettings.Livechat_transcript_message; - info.conversationFinishedMessage = initSettings.Livechat_conversation_finished_message; - info.conversationFinishedText = initSettings.Livechat_conversation_finished_text; - info.nameFieldRegistrationForm = initSettings.Livechat_name_field_registration_form; - info.emailFieldRegistrationForm = initSettings.Livechat_email_field_registration_form; - info.registrationFormMessage = initSettings.Livechat_registration_form_message; - info.showConnecting = initSettings.Livechat_Show_Connecting; - - info.agentData = room && room[0] && room[0].servedBy && Users.getAgentInfo(room[0].servedBy._id); - - await LivechatTrigger.findEnabled().forEach((trigger) => { - info.triggers.push(_.pick(trigger, '_id', 'actions', 'conditions', 'runOnce')); - }); - - LivechatDepartment.findEnabledWithAgents().forEach((department) => { - info.departments.push(department); - }); - info.allowSwitchingDepartments = initSettings.Livechat_allow_switching_departments; - - info.online = Users.findOnlineAgents().count() > 0; - - return deprecationWarning({ - endpoint: 'livechat:getInitialData', - versionWillBeRemoved: '5.0', - response: info, - }); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/getNextAgent.js b/apps/meteor/app/livechat/server/methods/getNextAgent.js index 8cfe404c0435..d2f0074e2d1c 100644 --- a/apps/meteor/app/livechat/server/methods/getNextAgent.js +++ b/apps/meteor/app/livechat/server/methods/getNextAgent.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { LivechatRooms, Users } from '../../../models'; +import { LivechatRooms, Users } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; Meteor.methods({ diff --git a/apps/meteor/app/livechat/server/methods/loadHistory.js b/apps/meteor/app/livechat/server/methods/loadHistory.js index a9d59d42ccbd..7e2fd97d5329 100644 --- a/apps/meteor/app/livechat/server/methods/loadHistory.js +++ b/apps/meteor/app/livechat/server/methods/loadHistory.js @@ -1,15 +1,16 @@ import { Meteor } from 'meteor/meteor'; +import { LivechatVisitors } from '@rocket.chat/models'; import { loadMessageHistory } from '../../../lib'; -import { LivechatVisitors, LivechatRooms } from '../../../models'; +import { LivechatRooms } from '../../../models/server'; Meteor.methods({ - 'livechat:loadHistory'({ token, rid, end, limit = 20, ls }) { + async 'livechat:loadHistory'({ token, rid, end, limit = 20, ls }) { if (!token || typeof token !== 'string') { return; } - const visitor = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } }); + const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); if (!visitor) { throw new Meteor.Error('invalid-visitor', 'Invalid Visitor', { diff --git a/apps/meteor/app/livechat/server/methods/loginByToken.js b/apps/meteor/app/livechat/server/methods/loginByToken.js index 9320b1f424d6..4c2c7c658365 100644 --- a/apps/meteor/app/livechat/server/methods/loginByToken.js +++ b/apps/meteor/app/livechat/server/methods/loginByToken.js @@ -1,10 +1,9 @@ import { Meteor } from 'meteor/meteor'; - -import { LivechatVisitors } from '../../../models'; +import { LivechatVisitors } from '@rocket.chat/models'; Meteor.methods({ - 'livechat:loginByToken'(token) { - const visitor = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } }); + async 'livechat:loginByToken'(token) { + const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); if (!visitor) { return; diff --git a/apps/meteor/app/livechat/server/methods/registerGuest.js b/apps/meteor/app/livechat/server/methods/registerGuest.js index b2fd0c0e41d7..b6c7b7f14f32 100644 --- a/apps/meteor/app/livechat/server/methods/registerGuest.js +++ b/apps/meteor/app/livechat/server/methods/registerGuest.js @@ -1,11 +1,12 @@ import { Meteor } from 'meteor/meteor'; +import { LivechatVisitors } from '@rocket.chat/models'; -import { Messages, LivechatRooms, LivechatVisitors } from '../../../models'; +import { Messages, LivechatRooms } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; Meteor.methods({ - 'livechat:registerGuest'({ token, name, email, department, customFields } = {}) { - const userId = Livechat.registerGuest.call(this, { + async 'livechat:registerGuest'({ token, name, email, department, customFields } = {}) { + const userId = await Livechat.registerGuest.call(this, { token, name, email, @@ -15,8 +16,8 @@ Meteor.methods({ // update visited page history to not expire Messages.keepHistoryForToken(token); - const visitor = LivechatVisitors.getVisitorByToken(token, { - fields: { + const visitor = await LivechatVisitors.getVisitorByToken(token, { + projection: { token: 1, name: 1, username: 1, @@ -32,6 +33,7 @@ Meteor.methods({ }); if (customFields && customFields instanceof Array) { + // TODO: refactor to use normal await customFields.forEach((customField) => { if (typeof customField !== 'object') { return; @@ -39,7 +41,7 @@ Meteor.methods({ if (!customField.scope || customField.scope !== 'room') { const { key, value, overwrite } = customField; - LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite); + Promise.await(LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite)); } }); } diff --git a/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.js b/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.js index da93127f73aa..c70a82a853bd 100644 --- a/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.js +++ b/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; -import { LivechatRooms } from '../../../models'; +import { LivechatRooms } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; Meteor.methods({ diff --git a/apps/meteor/app/livechat/server/methods/removeCustomField.js b/apps/meteor/app/livechat/server/methods/removeCustomField.js index 5b6fd869d6bf..d0364716194e 100644 --- a/apps/meteor/app/livechat/server/methods/removeCustomField.js +++ b/apps/meteor/app/livechat/server/methods/removeCustomField.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import { hasPermission } from '../../../authorization'; -import { LivechatCustomField } from '../../../models'; +import { LivechatCustomField } from '../../../models/server'; Meteor.methods({ 'livechat:removeCustomField'(_id) { diff --git a/apps/meteor/app/livechat/server/methods/removeRoom.js b/apps/meteor/app/livechat/server/methods/removeRoom.js index f8c251e28a36..a39a0abb4f02 100644 --- a/apps/meteor/app/livechat/server/methods/removeRoom.js +++ b/apps/meteor/app/livechat/server/methods/removeRoom.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; -import { LivechatRooms } from '../../../models'; +import { LivechatRooms } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; Meteor.methods({ diff --git a/apps/meteor/app/livechat/server/methods/removeTrigger.js b/apps/meteor/app/livechat/server/methods/removeTrigger.js index d20a9816a6bb..946b5f68be6f 100644 --- a/apps/meteor/app/livechat/server/methods/removeTrigger.js +++ b/apps/meteor/app/livechat/server/methods/removeTrigger.js @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; +import { LivechatTrigger } from '@rocket.chat/models'; import { hasPermission } from '../../../authorization/server'; -import { LivechatTrigger } from '../../../models/server/raw'; Meteor.methods({ async 'livechat:removeTrigger'(triggerId) { diff --git a/apps/meteor/app/livechat/server/methods/requestTranscript.js b/apps/meteor/app/livechat/server/methods/requestTranscript.js index 98f85bd66de1..8e3424c8d00f 100644 --- a/apps/meteor/app/livechat/server/methods/requestTranscript.js +++ b/apps/meteor/app/livechat/server/methods/requestTranscript.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import { hasPermission } from '../../../authorization'; -import { Users } from '../../../models'; +import { Users } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; Meteor.methods({ diff --git a/apps/meteor/app/livechat/server/methods/returnAsInquiry.js b/apps/meteor/app/livechat/server/methods/returnAsInquiry.js index 86203317d393..7cedc7ee7499 100644 --- a/apps/meteor/app/livechat/server/methods/returnAsInquiry.js +++ b/apps/meteor/app/livechat/server/methods/returnAsInquiry.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; -import { LivechatRooms } from '../../../models'; +import { LivechatRooms } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; Meteor.methods({ diff --git a/apps/meteor/app/livechat/server/methods/saveBusinessHour.ts b/apps/meteor/app/livechat/server/methods/saveBusinessHour.ts index bb0d585456b0..dcded190b982 100644 --- a/apps/meteor/app/livechat/server/methods/saveBusinessHour.ts +++ b/apps/meteor/app/livechat/server/methods/saveBusinessHour.ts @@ -8,7 +8,7 @@ Meteor.methods({ try { Promise.await(businessHourManager.saveBusinessHour(businessHourData)); } catch (e) { - throw new Meteor.Error(e.message); + throw new Meteor.Error(e instanceof Error ? e.message : String(e)); } }, }); diff --git a/apps/meteor/app/livechat/server/methods/saveCustomField.js b/apps/meteor/app/livechat/server/methods/saveCustomField.js index 1f45ed936fff..f50ac18231cb 100644 --- a/apps/meteor/app/livechat/server/methods/saveCustomField.js +++ b/apps/meteor/app/livechat/server/methods/saveCustomField.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { hasPermission } from '../../../authorization'; -import { LivechatCustomField } from '../../../models'; +import { LivechatCustomField } from '../../../models/server'; Meteor.methods({ 'livechat:saveCustomField'(_id, customFieldData) { diff --git a/apps/meteor/app/livechat/server/methods/saveInfo.js b/apps/meteor/app/livechat/server/methods/saveInfo.js index b00ffac58834..c5b4a164d6ca 100644 --- a/apps/meteor/app/livechat/server/methods/saveInfo.js +++ b/apps/meteor/app/livechat/server/methods/saveInfo.js @@ -2,12 +2,12 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { hasPermission } from '../../../authorization'; -import { LivechatRooms } from '../../../models'; +import { LivechatRooms } from '../../../models/server'; import { callbacks } from '../../../../lib/callbacks'; import { Livechat } from '../lib/Livechat'; Meteor.methods({ - 'livechat:saveInfo'(guestData, roomData) { + async 'livechat:saveInfo'(guestData, roomData) { const userId = Meteor.userId(); if (!userId || !hasPermission(userId, 'view-l-room')) { @@ -49,7 +49,7 @@ Meteor.methods({ delete guestData.phone; } - const ret = Livechat.saveGuest(guestData, userId) && Livechat.saveRoomInfo(roomData, guestData, userId); + const ret = (await Livechat.saveGuest(guestData, userId)) && Livechat.saveRoomInfo(roomData, guestData, userId); const user = Meteor.users.findOne({ _id: userId }, { fields: { _id: 1, username: 1 } }); diff --git a/apps/meteor/app/livechat/server/methods/saveSurveyFeedback.js b/apps/meteor/app/livechat/server/methods/saveSurveyFeedback.js index 2f6b0b815167..7d908cdc7a90 100644 --- a/apps/meteor/app/livechat/server/methods/saveSurveyFeedback.js +++ b/apps/meteor/app/livechat/server/methods/saveSurveyFeedback.js @@ -1,17 +1,16 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import _ from 'underscore'; - -import { LivechatRooms, LivechatVisitors } from '../../../models'; +import { LivechatRooms, LivechatVisitors } from '@rocket.chat/models'; Meteor.methods({ - 'livechat:saveSurveyFeedback'(visitorToken, visitorRoom, formData) { + async 'livechat:saveSurveyFeedback'(visitorToken, visitorRoom, formData) { check(visitorToken, String); check(visitorRoom, String); check(formData, [Match.ObjectIncluding({ name: String, value: String })]); - const visitor = LivechatVisitors.getVisitorByToken(visitorToken); - const room = LivechatRooms.findOneById(visitorRoom); + const visitor = await LivechatVisitors.getVisitorByToken(visitorToken); + const room = await LivechatRooms.findOneById(visitorRoom); if (visitor !== undefined && room !== undefined && room.v !== undefined && room.v.token === visitor.token) { const updateData = {}; diff --git a/apps/meteor/app/livechat/server/methods/saveTrigger.js b/apps/meteor/app/livechat/server/methods/saveTrigger.js index 252d6c022397..66f1aa6be961 100644 --- a/apps/meteor/app/livechat/server/methods/saveTrigger.js +++ b/apps/meteor/app/livechat/server/methods/saveTrigger.js @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; +import { LivechatTrigger } from '@rocket.chat/models'; import { hasPermission } from '../../../authorization'; -import { LivechatTrigger } from '../../../models/server/raw'; Meteor.methods({ async 'livechat:saveTrigger'(trigger) { diff --git a/apps/meteor/app/livechat/server/methods/searchAgent.js b/apps/meteor/app/livechat/server/methods/searchAgent.js index bfd8d7b67584..600a28150bad 100644 --- a/apps/meteor/app/livechat/server/methods/searchAgent.js +++ b/apps/meteor/app/livechat/server/methods/searchAgent.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { hasPermission } from '../../../authorization'; -import { Users } from '../../../models'; +import { Users } from '../../../models/server'; Meteor.methods({ 'livechat:searchAgent'(username) { diff --git a/apps/meteor/app/livechat/server/methods/sendFileLivechatMessage.js b/apps/meteor/app/livechat/server/methods/sendFileLivechatMessage.js index 6f9a20d5d731..3f2a3e7c98bb 100644 --- a/apps/meteor/app/livechat/server/methods/sendFileLivechatMessage.js +++ b/apps/meteor/app/livechat/server/methods/sendFileLivechatMessage.js @@ -1,13 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { Random } from 'meteor/random'; +import { LivechatVisitors } from '@rocket.chat/models'; -import { LivechatRooms, LivechatVisitors } from '../../../models'; +import { LivechatRooms } from '../../../models/server'; import { FileUpload } from '../../../file-upload/server'; Meteor.methods({ async sendFileLivechatMessage(roomId, visitorToken, file, msgData = {}) { - const visitor = LivechatVisitors.getVisitorByToken(visitorToken); + const visitor = await LivechatVisitors.getVisitorByToken(visitorToken); if (!visitor) { return false; diff --git a/apps/meteor/app/livechat/server/methods/sendMessageLivechat.js b/apps/meteor/app/livechat/server/methods/sendMessageLivechat.js index df6861f7af4a..7b6d2d0608b9 100644 --- a/apps/meteor/app/livechat/server/methods/sendMessageLivechat.js +++ b/apps/meteor/app/livechat/server/methods/sendMessageLivechat.js @@ -1,13 +1,13 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { OmnichannelSourceType } from '@rocket.chat/core-typings'; +import { LivechatVisitors } from '@rocket.chat/models'; -import { LivechatVisitors } from '../../../models'; import { Livechat } from '../lib/Livechat'; import { settings } from '../../../settings/server'; Meteor.methods({ - sendMessageLivechat({ token, _id, rid, msg, file, attachments }, agent) { + async sendMessageLivechat({ token, _id, rid, msg, file, attachments }, agent) { check(token, String); check(_id, String); check(rid, String); @@ -21,8 +21,8 @@ Meteor.methods({ }), ); - const guest = LivechatVisitors.getVisitorByToken(token, { - fields: { + const guest = await LivechatVisitors.getVisitorByToken(token, { + projection: { name: 1, username: 1, department: 1, diff --git a/apps/meteor/app/livechat/server/methods/sendTranscript.js b/apps/meteor/app/livechat/server/methods/sendTranscript.js index fb8fdc48863e..a29b73236533 100644 --- a/apps/meteor/app/livechat/server/methods/sendTranscript.js +++ b/apps/meteor/app/livechat/server/methods/sendTranscript.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; -import { Users } from '../../../models'; +import { Users } from '../../../models/server'; import { hasPermission } from '../../../authorization'; import { Livechat } from '../lib/Livechat'; diff --git a/apps/meteor/app/livechat/server/methods/setCustomField.js b/apps/meteor/app/livechat/server/methods/setCustomField.js index 14601cbf8adf..cd8576bcd0aa 100644 --- a/apps/meteor/app/livechat/server/methods/setCustomField.js +++ b/apps/meteor/app/livechat/server/methods/setCustomField.js @@ -1,9 +1,10 @@ import { Meteor } from 'meteor/meteor'; +import { LivechatVisitors } from '@rocket.chat/models'; -import { LivechatRooms, LivechatVisitors, LivechatCustomField } from '../../../models'; +import { LivechatRooms, LivechatCustomField } from '../../../models/server'; Meteor.methods({ - 'livechat:setCustomField'(token, key, value, overwrite = true) { + async 'livechat:setCustomField'(token, key, value, overwrite = true) { const customField = LivechatCustomField.findOneById(key); if (customField) { if (customField.scope === 'room') { diff --git a/apps/meteor/app/livechat/server/methods/setDepartmentForVisitor.js b/apps/meteor/app/livechat/server/methods/setDepartmentForVisitor.js index 9d1f7abaccd9..ada1703b34c6 100644 --- a/apps/meteor/app/livechat/server/methods/setDepartmentForVisitor.js +++ b/apps/meteor/app/livechat/server/methods/setDepartmentForVisitor.js @@ -1,18 +1,19 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; +import { LivechatVisitors } from '@rocket.chat/models'; -import { LivechatRooms, Messages, LivechatVisitors } from '../../../models'; +import { LivechatRooms, Messages } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; import { normalizeTransferredByData } from '../lib/Helper'; Meteor.methods({ - 'livechat:setDepartmentForVisitor'({ roomId, visitorToken, departmentId } = {}) { + async 'livechat:setDepartmentForVisitor'({ roomId, visitorToken, departmentId } = {}) { check(roomId, String); check(visitorToken, String); check(departmentId, String); const room = LivechatRooms.findOneById(roomId); - const visitor = LivechatVisitors.getVisitorByToken(visitorToken); + const visitor = await LivechatVisitors.getVisitorByToken(visitorToken); if (!room || room.t !== 'l' || !room.v || room.v.token !== visitor.token) { throw new Meteor.Error('error-invalid-room', 'Invalid room'); diff --git a/apps/meteor/app/livechat/server/methods/startFileUploadRoom.js b/apps/meteor/app/livechat/server/methods/startFileUploadRoom.js deleted file mode 100644 index f6a3691faeb1..000000000000 --- a/apps/meteor/app/livechat/server/methods/startFileUploadRoom.js +++ /dev/null @@ -1,29 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Random } from 'meteor/random'; -import { OmnichannelSourceType } from '@rocket.chat/core-typings'; - -import { LivechatVisitors } from '../../../models'; -import { Livechat } from '../lib/Livechat'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; - -Meteor.methods({ - 'livechat:startFileUploadRoom'(roomId, token) { - methodDeprecationLogger.warn('livechat:startFileUploadRoom will be deprecated in future versions of Rocket.Chat'); - const guest = LivechatVisitors.getVisitorByToken(token); - - const message = { - _id: Random.id(), - rid: roomId || Random.id(), - msg: '', - ts: new Date(), - token: guest.token, - }; - - const roomInfo = { - source: OmnichannelSourceType.API, - alias: 'file-upload', - }; - - return Livechat.getRoom(guest, message, roomInfo); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/startVideoCall.js b/apps/meteor/app/livechat/server/methods/startVideoCall.js deleted file mode 100644 index 3319ca905cf3..000000000000 --- a/apps/meteor/app/livechat/server/methods/startVideoCall.js +++ /dev/null @@ -1,58 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Random } from 'meteor/random'; -import { OmnichannelSourceType } from '@rocket.chat/core-typings'; - -import { Messages } from '../../../models'; -import { settings } from '../../../settings'; -import { Livechat } from '../lib/Livechat'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; - -Meteor.methods({ - async 'livechat:startVideoCall'(roomId) { - methodDeprecationLogger.warn('livechat:startVideoCall will be deprecated in future versions of Rocket.Chat'); - if (!Meteor.userId()) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'livechat:closeByVisitor', - }); - } - - const guest = Meteor.user(); - - const message = { - _id: Random.id(), - rid: roomId || Random.id(), - msg: '', - ts: new Date(), - }; - - const roomInfo = { - jitsiTimeout: new Date(Date.now() + 3600 * 1000), - source: { - type: OmnichannelSourceType.API, - alias: 'video-call', - }, - }; - - const room = await Livechat.getRoom(guest, message, roomInfo); - message.rid = room._id; - - Messages.createWithTypeRoomIdMessageAndUser('livechat_video_call', room._id, '', guest, { - actionLinks: [ - { icon: 'icon-videocam', i18nLabel: 'Accept', method_id: 'createLivechatCall', params: '' }, - { icon: 'icon-cancel', i18nLabel: 'Decline', method_id: 'denyLivechatCall', params: '' }, - ], - }); - - let rname; - if (settings.get('Jitsi_URL_Room_Hash')) { - rname = settings.get('uniqueID') + roomId; - } else { - rname = encodeURIComponent(room.t === 'd' ? room.usernames.join(' x ') : room.name); - } - return { - roomId: room._id, - domain: settings.get('Jitsi_Domain'), - jitsiRoom: settings.get('Jitsi_URL_Room_Prefix') + rname + settings.get('Jitsi_URL_Room_Suffix'), - }; - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/transfer.js b/apps/meteor/app/livechat/server/methods/transfer.js index 0e29de863aec..52a189c97e5c 100644 --- a/apps/meteor/app/livechat/server/methods/transfer.js +++ b/apps/meteor/app/livechat/server/methods/transfer.js @@ -1,13 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; +import { LivechatVisitors } from '@rocket.chat/models'; -import { hasPermission } from '../../../authorization'; -import { LivechatRooms, Subscriptions, LivechatVisitors, Users } from '../../../models'; +import { hasPermission } from '../../../authorization/server'; +import { LivechatRooms, Subscriptions, Users } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; import { normalizeTransferredByData } from '../lib/Helper'; Meteor.methods({ - 'livechat:transfer'(transferData) { + async 'livechat:transfer'(transferData) { if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-l-room')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:transfer' }); } @@ -38,7 +39,7 @@ Meteor.methods({ }); } - const guest = LivechatVisitors.findOneById(room.v && room.v._id); + const guest = await LivechatVisitors.findOneById(room.v && room.v._id); transferData.transferredBy = normalizeTransferredByData(Meteor.user() || {}, room); if (transferData.userId) { const userToTransfer = Users.findOneById(transferData.userId); diff --git a/apps/meteor/app/livechat/server/roomAccessValidator.internalService.ts b/apps/meteor/app/livechat/server/roomAccessValidator.internalService.ts index d33c08f6e696..dc1a1a6cbc85 100644 --- a/apps/meteor/app/livechat/server/roomAccessValidator.internalService.ts +++ b/apps/meteor/app/livechat/server/roomAccessValidator.internalService.ts @@ -9,7 +9,7 @@ export class AuthorizationLivechat extends ServiceClassInternal implements IAuth protected internal = true; - async canAccessRoom(room: IOmnichannelRoom, user: Pick, extraData?: object): Promise { + async canAccessRoom(room: IOmnichannelRoom, user?: Pick, extraData?: object): Promise { for (const validator of validators) { if (validator(room, user, extraData)) { return true; diff --git a/apps/meteor/app/livechat/server/sendMessageBySMS.js b/apps/meteor/app/livechat/server/sendMessageBySMS.js index 72c36d158ce7..587dd2663a29 100644 --- a/apps/meteor/app/livechat/server/sendMessageBySMS.js +++ b/apps/meteor/app/livechat/server/sendMessageBySMS.js @@ -1,7 +1,8 @@ +import { LivechatVisitors } from '@rocket.chat/models'; + import { callbacks } from '../../../lib/callbacks'; -import { settings } from '../../settings'; +import { settings } from '../../settings/server'; import { SMS } from '../../sms'; -import { LivechatVisitors } from '../../models'; import { normalizeMessageFileUpload } from '../../utils/server/functions/normalizeMessageFileUpload'; callbacks.add( @@ -49,7 +50,7 @@ callbacks.add( return message; } - const visitor = LivechatVisitors.getVisitorByToken(room.v.token); + const visitor = Promise.await(LivechatVisitors.getVisitorByToken(room.v.token)); if (!visitor || !visitor.phone || visitor.phone.length === 0) { return message; diff --git a/apps/meteor/app/livechat/server/startup.js b/apps/meteor/app/livechat/server/startup.js index 14229e94948b..f9dcf3201f81 100644 --- a/apps/meteor/app/livechat/server/startup.js +++ b/apps/meteor/app/livechat/server/startup.js @@ -3,7 +3,7 @@ import { Accounts } from 'meteor/accounts-base'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { roomCoordinator } from '../../../server/lib/rooms/roomCoordinator'; -import { LivechatRooms } from '../../models'; +import { LivechatRooms } from '../../models/server'; import { callbacks } from '../../../lib/callbacks'; import { settings } from '../../settings/server'; import { LivechatAgentActivityMonitor } from './statistics/LivechatAgentActivityMonitor'; diff --git a/apps/meteor/app/livechat/server/statistics/LivechatAgentActivityMonitor.ts b/apps/meteor/app/livechat/server/statistics/LivechatAgentActivityMonitor.ts index 5fbb8c9edd21..7379376a7a72 100644 --- a/apps/meteor/app/livechat/server/statistics/LivechatAgentActivityMonitor.ts +++ b/apps/meteor/app/livechat/server/statistics/LivechatAgentActivityMonitor.ts @@ -2,10 +2,10 @@ import moment from 'moment'; import { ISocketConnection } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import { SyncedCron } from 'meteor/littledata:synced-cron'; +import { LivechatAgentActivity, Sessions } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { Users } from '../../../models/server'; -import { LivechatAgentActivity, Sessions } from '../../../models/server/raw'; const formatDate = (dateTime = new Date()): { date: number } => ({ date: parseInt(moment(dateTime).format('YYYYMMDD')), diff --git a/apps/meteor/app/livestream/client/tabBar.tsx b/apps/meteor/app/livestream/client/tabBar.tsx index d06e2ee90aa6..8d81cf78a915 100644 --- a/apps/meteor/app/livestream/client/tabBar.tsx +++ b/apps/meteor/app/livestream/client/tabBar.tsx @@ -1,6 +1,7 @@ import React, { ReactNode, useMemo } from 'react'; import { Option, Badge } from '@rocket.chat/fuselage'; import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import { isRoomFederated } from '@rocket.chat/core-typings'; import { addAction } from '../../../client/views/room/lib/Toolbox'; import Header from '../../../client/components/Header'; @@ -8,7 +9,7 @@ import Header from '../../../client/components/Header'; addAction('livestream', ({ room }) => { const enabled = useSetting('Livestream_enabled'); const t = useTranslation(); - + const federated = isRoomFederated(room); const isLive = room?.streamingOptions?.id && room.streamingOptions.type === 'livestream'; return useMemo( @@ -21,6 +22,10 @@ addAction('livestream', ({ room }) => { icon: 'podcast', template: 'liveStreamTab', order: isLive ? -1 : 15, + ...(federated && { + 'data-tooltip': federated ? 'Livestream_unavailable_for_federation' : '', + 'disabled': true, + }), renderAction: (props): ReactNode => ( {isLive ? ( @@ -41,6 +46,6 @@ addAction('livestream', ({ room }) => { ), } : null, - [enabled, isLive, t], + [enabled, isLive, t, federated], ); }); diff --git a/apps/meteor/app/livestream/client/views/liveStreamTab.js b/apps/meteor/app/livestream/client/views/liveStreamTab.js index 52fe8519ddc3..d819278cb121 100644 --- a/apps/meteor/app/livestream/client/views/liveStreamTab.js +++ b/apps/meteor/app/livestream/client/views/liveStreamTab.js @@ -12,7 +12,7 @@ import { t } from '../../../utils'; import { settings } from '../../../settings'; import { callbacks } from '../../../../lib/callbacks'; import { hasAllPermission } from '../../../authorization'; -import { Users, Rooms } from '../../../models'; +import { Users, Rooms } from '../../../models/client'; import { handleError } from '../../../../client/lib/utils/handleError'; import { dispatchToastMessage } from '../../../../client/lib/toast'; diff --git a/apps/meteor/app/livestream/server/functions/livestream.js b/apps/meteor/app/livestream/server/functions/livestream.js index bba3833650d3..f17a5524c93d 100644 --- a/apps/meteor/app/livestream/server/functions/livestream.js +++ b/apps/meteor/app/livestream/server/functions/livestream.js @@ -1,4 +1,4 @@ -import google from 'googleapis'; +import { google } from 'googleapis'; const { OAuth2 } = google.auth; diff --git a/apps/meteor/app/livestream/server/methods.js b/apps/meteor/app/livestream/server/methods.js index ab8a91988ab2..f9942c3e7d8a 100644 --- a/apps/meteor/app/livestream/server/methods.js +++ b/apps/meteor/app/livestream/server/methods.js @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { createLiveStream, statusLiveStream, statusStreamLiveStream, getBroadcastStatus, setBroadcastStatus } from './functions/livestream'; -import { settings } from '../../settings'; -import { Rooms } from '../../models'; +import { settings } from '../../settings/server'; +import { Rooms } from '../../models/server'; const selectLivestreamSettings = (user) => user && user.settings && user.settings.livestream; diff --git a/apps/meteor/app/livestream/server/routes.js b/apps/meteor/app/livestream/server/routes.js index 3c63bac44c00..93f340ff5eeb 100644 --- a/apps/meteor/app/livestream/server/routes.js +++ b/apps/meteor/app/livestream/server/routes.js @@ -1,7 +1,7 @@ -import google from 'googleapis'; +import { google } from 'googleapis'; -import { settings } from '../../settings'; -import { Users } from '../../models'; +import { settings } from '../../settings/server'; +import { Users } from '../../models/server'; import { API } from '../../api/server'; const { OAuth2 } = google.auth; diff --git a/apps/meteor/app/mail-messages/server/functions/sendMail.ts b/apps/meteor/app/mail-messages/server/functions/sendMail.ts index dbce8cc9caf0..3f0fad8185c8 100644 --- a/apps/meteor/app/mail-messages/server/functions/sendMail.ts +++ b/apps/meteor/app/mail-messages/server/functions/sendMail.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { EJSON } from 'meteor/ejson'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { escapeHTML } from '@rocket.chat/string-helpers'; -import { FilterQuery } from 'mongodb'; +import type { Filter } from 'mongodb'; import { IUser } from '@rocket.chat/core-typings'; import { placeholders } from '../../../utils/server'; @@ -18,7 +18,7 @@ export const sendMail = function (from: string, subject: string, body: string, d }); } - let userQuery: FilterQuery = { 'mailer.unsubscribed': { $exists: 0 } }; + let userQuery: Filter = { 'mailer.unsubscribed': { $exists: 0 } }; if (query) { userQuery = { $and: [userQuery, EJSON.parse(query)] }; } diff --git a/apps/meteor/app/mailer/server/api.ts b/apps/meteor/app/mailer/server/api.ts index ae4634fff0ae..05e75145fd4e 100644 --- a/apps/meteor/app/mailer/server/api.ts +++ b/apps/meteor/app/mailer/server/api.ts @@ -9,6 +9,7 @@ import { escapeHTML } from '@rocket.chat/string-helpers'; import type { ISetting } from '@rocket.chat/core-typings'; import { settings } from '../../settings/server'; +import { Settings as SettingsRaw } from '../../models/server'; import { replaceVariables } from './replaceVariables'; import { Apps } from '../../apps/server'; import { validateEmail } from '../../../lib/emailValidator'; @@ -169,6 +170,8 @@ export const sendNoWrap = ({ html = undefined; } + SettingsRaw.incrementValueById('Triggered_Emails_Count'); + const email = { to, from, replyTo, subject, html, text, headers }; const eventResult = Promise.await(Apps.triggerEvent('IPreEmailSent', { email })); diff --git a/apps/meteor/app/markdown/lib/hljs.js b/apps/meteor/app/markdown/lib/hljs.js index 6a5232ba2f1c..58c486ac3b86 100644 --- a/apps/meteor/app/markdown/lib/hljs.js +++ b/apps/meteor/app/markdown/lib/hljs.js @@ -1,7 +1,7 @@ -import hljs from 'highlight.js/lib/highlight'; -import clean from 'highlight.js/lib/languages/clean'; -import markdown from 'highlight.js/lib/languages/markdown'; -import javascript from 'highlight.js/lib/languages/javascript'; +import hljs from 'hljs9/lib/highlight'; +import clean from 'hljs9/lib/languages/clean'; +import markdown from 'hljs9/lib/languages/markdown'; +import javascript from 'hljs9/lib/languages/javascript'; hljs.registerLanguage('markdown', markdown); hljs.registerLanguage('clean', clean); @@ -10,351 +10,351 @@ hljs.registerLanguage('javascript', javascript); export const register = async (lang) => { switch (lang) { case 'onec': - return hljs.registerLanguage('onec', (await import('highlight.js/lib/languages/1c')).default); + return hljs.registerLanguage('onec', (await import('hljs9/lib/languages/1c')).default); case 'abnf': - return hljs.registerLanguage('abnf', (await import('highlight.js/lib/languages/abnf')).default); + return hljs.registerLanguage('abnf', (await import('hljs9/lib/languages/abnf')).default); case 'accesslog': - return hljs.registerLanguage('accesslog', (await import('highlight.js/lib/languages/accesslog')).default); + return hljs.registerLanguage('accesslog', (await import('hljs9/lib/languages/accesslog')).default); case 'actionscript': - return hljs.registerLanguage('actionscript', (await import('highlight.js/lib/languages/actionscript')).default); + return hljs.registerLanguage('actionscript', (await import('hljs9/lib/languages/actionscript')).default); case 'ada': - return hljs.registerLanguage('ada', (await import('highlight.js/lib/languages/ada')).default); + return hljs.registerLanguage('ada', (await import('hljs9/lib/languages/ada')).default); case 'apache': - return hljs.registerLanguage('apache', (await import('highlight.js/lib/languages/apache')).default); + return hljs.registerLanguage('apache', (await import('hljs9/lib/languages/apache')).default); case 'applescript': - return hljs.registerLanguage('applescript', (await import('highlight.js/lib/languages/applescript')).default); + return hljs.registerLanguage('applescript', (await import('hljs9/lib/languages/applescript')).default); case 'arduino': - return hljs.registerLanguage('arduino', (await import('highlight.js/lib/languages/arduino')).default); + return hljs.registerLanguage('arduino', (await import('hljs9/lib/languages/arduino')).default); case 'armasm': - return hljs.registerLanguage('armasm', (await import('highlight.js/lib/languages/armasm')).default); + return hljs.registerLanguage('armasm', (await import('hljs9/lib/languages/armasm')).default); case 'asciidoc': - return hljs.registerLanguage('asciidoc', (await import('highlight.js/lib/languages/asciidoc')).default); + return hljs.registerLanguage('asciidoc', (await import('hljs9/lib/languages/asciidoc')).default); case 'aspectj': - return hljs.registerLanguage('aspectj', (await import('highlight.js/lib/languages/aspectj')).default); + return hljs.registerLanguage('aspectj', (await import('hljs9/lib/languages/aspectj')).default); case 'autohotkey': - return hljs.registerLanguage('autohotkey', (await import('highlight.js/lib/languages/autohotkey')).default); + return hljs.registerLanguage('autohotkey', (await import('hljs9/lib/languages/autohotkey')).default); case 'autoit': - return hljs.registerLanguage('autoit', (await import('highlight.js/lib/languages/autoit')).default); + return hljs.registerLanguage('autoit', (await import('hljs9/lib/languages/autoit')).default); case 'avrasm': - return hljs.registerLanguage('avrasm', (await import('highlight.js/lib/languages/avrasm')).default); + return hljs.registerLanguage('avrasm', (await import('hljs9/lib/languages/avrasm')).default); case 'awk': - return hljs.registerLanguage('awk', (await import('highlight.js/lib/languages/awk')).default); + return hljs.registerLanguage('awk', (await import('hljs9/lib/languages/awk')).default); case 'axapta': - return hljs.registerLanguage('axapta', (await import('highlight.js/lib/languages/axapta')).default); + return hljs.registerLanguage('axapta', (await import('hljs9/lib/languages/axapta')).default); case 'bash': - return hljs.registerLanguage('bash', (await import('highlight.js/lib/languages/bash')).default); + return hljs.registerLanguage('bash', (await import('hljs9/lib/languages/bash')).default); case 'basic': - return hljs.registerLanguage('basic', (await import('highlight.js/lib/languages/basic')).default); + return hljs.registerLanguage('basic', (await import('hljs9/lib/languages/basic')).default); case 'bnf': - return hljs.registerLanguage('bnf', (await import('highlight.js/lib/languages/bnf')).default); + return hljs.registerLanguage('bnf', (await import('hljs9/lib/languages/bnf')).default); case 'brainfuck': - return hljs.registerLanguage('brainfuck', (await import('highlight.js/lib/languages/brainfuck')).default); + return hljs.registerLanguage('brainfuck', (await import('hljs9/lib/languages/brainfuck')).default); case 'cal': - return hljs.registerLanguage('cal', (await import('highlight.js/lib/languages/cal')).default); + return hljs.registerLanguage('cal', (await import('hljs9/lib/languages/cal')).default); case 'capnproto': - return hljs.registerLanguage('capnproto', (await import('highlight.js/lib/languages/capnproto')).default); + return hljs.registerLanguage('capnproto', (await import('hljs9/lib/languages/capnproto')).default); case 'ceylon': - return hljs.registerLanguage('ceylon', (await import('highlight.js/lib/languages/ceylon')).default); + return hljs.registerLanguage('ceylon', (await import('hljs9/lib/languages/ceylon')).default); case 'clean': - return hljs.registerLanguage('clean', (await import('highlight.js/lib/languages/clean')).default); + return hljs.registerLanguage('clean', (await import('hljs9/lib/languages/clean')).default); case 'clojure': - return hljs.registerLanguage('clojure', (await import('highlight.js/lib/languages/clojure')).default); + return hljs.registerLanguage('clojure', (await import('hljs9/lib/languages/clojure')).default); case 'clojure-repl': - return hljs.registerLanguage('clojure-repl', (await import('highlight.js/lib/languages/clojure-repl')).default); + return hljs.registerLanguage('clojure-repl', (await import('hljs9/lib/languages/clojure-repl')).default); case 'cmake': - return hljs.registerLanguage('cmake', (await import('highlight.js/lib/languages/cmake')).default); + return hljs.registerLanguage('cmake', (await import('hljs9/lib/languages/cmake')).default); case 'coffeescript': - return hljs.registerLanguage('coffeescript', (await import('highlight.js/lib/languages/coffeescript')).default); + return hljs.registerLanguage('coffeescript', (await import('hljs9/lib/languages/coffeescript')).default); case 'coq': - return hljs.registerLanguage('coq', (await import('highlight.js/lib/languages/coq')).default); + return hljs.registerLanguage('coq', (await import('hljs9/lib/languages/coq')).default); case 'cos': - return hljs.registerLanguage('cos', (await import('highlight.js/lib/languages/cos')).default); + return hljs.registerLanguage('cos', (await import('hljs9/lib/languages/cos')).default); case 'cpp': - return hljs.registerLanguage('cpp', (await import('highlight.js/lib/languages/cpp')).default); + return hljs.registerLanguage('cpp', (await import('hljs9/lib/languages/cpp')).default); case 'crmsh': - return hljs.registerLanguage('crmsh', (await import('highlight.js/lib/languages/crmsh')).default); + return hljs.registerLanguage('crmsh', (await import('hljs9/lib/languages/crmsh')).default); case 'crystal': - return hljs.registerLanguage('crystal', (await import('highlight.js/lib/languages/crystal')).default); + return hljs.registerLanguage('crystal', (await import('hljs9/lib/languages/crystal')).default); case 'cs': - return hljs.registerLanguage('cs', (await import('highlight.js/lib/languages/cs')).default); + return hljs.registerLanguage('cs', (await import('hljs9/lib/languages/cs')).default); case 'csp': - return hljs.registerLanguage('csp', (await import('highlight.js/lib/languages/csp')).default); + return hljs.registerLanguage('csp', (await import('hljs9/lib/languages/csp')).default); case 'css': - return hljs.registerLanguage('css', (await import('highlight.js/lib/languages/css')).default); + return hljs.registerLanguage('css', (await import('hljs9/lib/languages/css')).default); case 'd': - return hljs.registerLanguage('d', (await import('highlight.js/lib/languages/d')).default); + return hljs.registerLanguage('d', (await import('hljs9/lib/languages/d')).default); case 'dart': - return hljs.registerLanguage('dart', (await import('highlight.js/lib/languages/dart')).default); + return hljs.registerLanguage('dart', (await import('hljs9/lib/languages/dart')).default); case 'delphi': - return hljs.registerLanguage('delphi', (await import('highlight.js/lib/languages/delphi')).default); + return hljs.registerLanguage('delphi', (await import('hljs9/lib/languages/delphi')).default); case 'diff': - return hljs.registerLanguage('diff', (await import('highlight.js/lib/languages/diff')).default); + return hljs.registerLanguage('diff', (await import('hljs9/lib/languages/diff')).default); case 'django': - return hljs.registerLanguage('django', (await import('highlight.js/lib/languages/django')).default); + return hljs.registerLanguage('django', (await import('hljs9/lib/languages/django')).default); case 'dns': - return hljs.registerLanguage('dns', (await import('highlight.js/lib/languages/dns')).default); + return hljs.registerLanguage('dns', (await import('hljs9/lib/languages/dns')).default); case 'dockerfile': - return hljs.registerLanguage('dockerfile', (await import('highlight.js/lib/languages/dockerfile')).default); + return hljs.registerLanguage('dockerfile', (await import('hljs9/lib/languages/dockerfile')).default); case 'dos': - return hljs.registerLanguage('dos', (await import('highlight.js/lib/languages/dos')).default); + return hljs.registerLanguage('dos', (await import('hljs9/lib/languages/dos')).default); case 'dsconfig': - return hljs.registerLanguage('dsconfig', (await import('highlight.js/lib/languages/dsconfig')).default); + return hljs.registerLanguage('dsconfig', (await import('hljs9/lib/languages/dsconfig')).default); case 'dts': - return hljs.registerLanguage('dts', (await import('highlight.js/lib/languages/dts')).default); + return hljs.registerLanguage('dts', (await import('hljs9/lib/languages/dts')).default); case 'dust': - return hljs.registerLanguage('dust', (await import('highlight.js/lib/languages/dust')).default); + return hljs.registerLanguage('dust', (await import('hljs9/lib/languages/dust')).default); case 'ebnf': - return hljs.registerLanguage('ebnf', (await import('highlight.js/lib/languages/ebnf')).default); + return hljs.registerLanguage('ebnf', (await import('hljs9/lib/languages/ebnf')).default); case 'elixir': - return hljs.registerLanguage('elixir', (await import('highlight.js/lib/languages/elixir')).default); + return hljs.registerLanguage('elixir', (await import('hljs9/lib/languages/elixir')).default); case 'elm': - return hljs.registerLanguage('elm', (await import('highlight.js/lib/languages/elm')).default); + return hljs.registerLanguage('elm', (await import('hljs9/lib/languages/elm')).default); case 'erb': - return hljs.registerLanguage('erb', (await import('highlight.js/lib/languages/erb')).default); + return hljs.registerLanguage('erb', (await import('hljs9/lib/languages/erb')).default); case 'erlang': - return hljs.registerLanguage('erlang', (await import('highlight.js/lib/languages/erlang')).default); + return hljs.registerLanguage('erlang', (await import('hljs9/lib/languages/erlang')).default); case 'excel': - return hljs.registerLanguage('excel', (await import('highlight.js/lib/languages/excel')).default); + return hljs.registerLanguage('excel', (await import('hljs9/lib/languages/excel')).default); case 'fix': - return hljs.registerLanguage('fix', (await import('highlight.js/lib/languages/fix')).default); + return hljs.registerLanguage('fix', (await import('hljs9/lib/languages/fix')).default); case 'flix': - return hljs.registerLanguage('flix', (await import('highlight.js/lib/languages/flix')).default); + return hljs.registerLanguage('flix', (await import('hljs9/lib/languages/flix')).default); case 'fortran': - return hljs.registerLanguage('fortran', (await import('highlight.js/lib/languages/fortran')).default); + return hljs.registerLanguage('fortran', (await import('hljs9/lib/languages/fortran')).default); case 'fsharp': - return hljs.registerLanguage('fsharp', (await import('highlight.js/lib/languages/fsharp')).default); + return hljs.registerLanguage('fsharp', (await import('hljs9/lib/languages/fsharp')).default); case 'gams': - return hljs.registerLanguage('gams', (await import('highlight.js/lib/languages/gams')).default); + return hljs.registerLanguage('gams', (await import('hljs9/lib/languages/gams')).default); case 'gauss': - return hljs.registerLanguage('gauss', (await import('highlight.js/lib/languages/gauss')).default); + return hljs.registerLanguage('gauss', (await import('hljs9/lib/languages/gauss')).default); case 'gcode': - return hljs.registerLanguage('gcode', (await import('highlight.js/lib/languages/gcode')).default); + return hljs.registerLanguage('gcode', (await import('hljs9/lib/languages/gcode')).default); case 'gherkin': - return hljs.registerLanguage('gherkin', (await import('highlight.js/lib/languages/gherkin')).default); + return hljs.registerLanguage('gherkin', (await import('hljs9/lib/languages/gherkin')).default); case 'glsl': - return hljs.registerLanguage('glsl', (await import('highlight.js/lib/languages/glsl')).default); + return hljs.registerLanguage('glsl', (await import('hljs9/lib/languages/glsl')).default); case 'go': - return hljs.registerLanguage('go', (await import('highlight.js/lib/languages/go')).default); + return hljs.registerLanguage('go', (await import('hljs9/lib/languages/go')).default); case 'golo': - return hljs.registerLanguage('golo', (await import('highlight.js/lib/languages/golo')).default); + return hljs.registerLanguage('golo', (await import('hljs9/lib/languages/golo')).default); case 'gradle': - return hljs.registerLanguage('gradle', (await import('highlight.js/lib/languages/gradle')).default); + return hljs.registerLanguage('gradle', (await import('hljs9/lib/languages/gradle')).default); case 'groovy': - return hljs.registerLanguage('groovy', (await import('highlight.js/lib/languages/groovy')).default); + return hljs.registerLanguage('groovy', (await import('hljs9/lib/languages/groovy')).default); case 'haml': - return hljs.registerLanguage('haml', (await import('highlight.js/lib/languages/haml')).default); + return hljs.registerLanguage('haml', (await import('hljs9/lib/languages/haml')).default); case 'handlebars': - return hljs.registerLanguage('handlebars', (await import('highlight.js/lib/languages/handlebars')).default); + return hljs.registerLanguage('handlebars', (await import('hljs9/lib/languages/handlebars')).default); case 'haskell': - return hljs.registerLanguage('haskell', (await import('highlight.js/lib/languages/haskell')).default); + return hljs.registerLanguage('haskell', (await import('hljs9/lib/languages/haskell')).default); case 'haxe': - return hljs.registerLanguage('haxe', (await import('highlight.js/lib/languages/haxe')).default); + return hljs.registerLanguage('haxe', (await import('hljs9/lib/languages/haxe')).default); case 'hsp': - return hljs.registerLanguage('hsp', (await import('highlight.js/lib/languages/hsp')).default); + return hljs.registerLanguage('hsp', (await import('hljs9/lib/languages/hsp')).default); case 'htmlbars': - return hljs.registerLanguage('htmlbars', (await import('highlight.js/lib/languages/htmlbars')).default); + return hljs.registerLanguage('htmlbars', (await import('hljs9/lib/languages/htmlbars')).default); case 'http': - return hljs.registerLanguage('http', (await import('highlight.js/lib/languages/http')).default); + return hljs.registerLanguage('http', (await import('hljs9/lib/languages/http')).default); case 'hy': - return hljs.registerLanguage('hy', (await import('highlight.js/lib/languages/hy')).default); + return hljs.registerLanguage('hy', (await import('hljs9/lib/languages/hy')).default); case 'inform7': - return hljs.registerLanguage('inform7', (await import('highlight.js/lib/languages/inform7')).default); + return hljs.registerLanguage('inform7', (await import('hljs9/lib/languages/inform7')).default); case 'ini': - return hljs.registerLanguage('ini', (await import('highlight.js/lib/languages/ini')).default); + return hljs.registerLanguage('ini', (await import('hljs9/lib/languages/ini')).default); case 'irpf90': - return hljs.registerLanguage('irpf90', (await import('highlight.js/lib/languages/irpf90')).default); + return hljs.registerLanguage('irpf90', (await import('hljs9/lib/languages/irpf90')).default); case 'java': - return hljs.registerLanguage('java', (await import('highlight.js/lib/languages/java')).default); + return hljs.registerLanguage('java', (await import('hljs9/lib/languages/java')).default); case 'javascript': - return hljs.registerLanguage('javascript', (await import('highlight.js/lib/languages/javascript')).default); + return hljs.registerLanguage('javascript', (await import('hljs9/lib/languages/javascript')).default); case 'jboss-cli': - return hljs.registerLanguage('jboss-cli', (await import('highlight.js/lib/languages/jboss-cli')).default); + return hljs.registerLanguage('jboss-cli', (await import('hljs9/lib/languages/jboss-cli')).default); case 'json': - return hljs.registerLanguage('json', (await import('highlight.js/lib/languages/json')).default); + return hljs.registerLanguage('json', (await import('hljs9/lib/languages/json')).default); case 'julia': - return hljs.registerLanguage('julia', (await import('highlight.js/lib/languages/julia')).default); + return hljs.registerLanguage('julia', (await import('hljs9/lib/languages/julia')).default); case 'julia-repl': - return hljs.registerLanguage('julia-repl', (await import('highlight.js/lib/languages/julia-repl')).default); + return hljs.registerLanguage('julia-repl', (await import('hljs9/lib/languages/julia-repl')).default); case 'kotlin': - return hljs.registerLanguage('kotlin', (await import('highlight.js/lib/languages/kotlin')).default); + return hljs.registerLanguage('kotlin', (await import('hljs9/lib/languages/kotlin')).default); case 'lasso': - return hljs.registerLanguage('lasso', (await import('highlight.js/lib/languages/lasso')).default); + return hljs.registerLanguage('lasso', (await import('hljs9/lib/languages/lasso')).default); case 'ldif': - return hljs.registerLanguage('ldif', (await import('highlight.js/lib/languages/ldif')).default); + return hljs.registerLanguage('ldif', (await import('hljs9/lib/languages/ldif')).default); case 'leaf': - return hljs.registerLanguage('leaf', (await import('highlight.js/lib/languages/leaf')).default); + return hljs.registerLanguage('leaf', (await import('hljs9/lib/languages/leaf')).default); case 'less': - return hljs.registerLanguage('less', (await import('highlight.js/lib/languages/less')).default); + return hljs.registerLanguage('less', (await import('hljs9/lib/languages/less')).default); case 'lisp': - return hljs.registerLanguage('lisp', (await import('highlight.js/lib/languages/lisp')).default); + return hljs.registerLanguage('lisp', (await import('hljs9/lib/languages/lisp')).default); case 'livecodeserver': - return hljs.registerLanguage('livecodeserver', (await import('highlight.js/lib/languages/livecodeserver')).default); + return hljs.registerLanguage('livecodeserver', (await import('hljs9/lib/languages/livecodeserver')).default); case 'livescript': - return hljs.registerLanguage('livescript', (await import('highlight.js/lib/languages/livescript')).default); + return hljs.registerLanguage('livescript', (await import('hljs9/lib/languages/livescript')).default); case 'llvm': - return hljs.registerLanguage('llvm', (await import('highlight.js/lib/languages/llvm')).default); + return hljs.registerLanguage('llvm', (await import('hljs9/lib/languages/llvm')).default); case 'lsl': - return hljs.registerLanguage('lsl', (await import('highlight.js/lib/languages/lsl')).default); + return hljs.registerLanguage('lsl', (await import('hljs9/lib/languages/lsl')).default); case 'lua': - return hljs.registerLanguage('lua', (await import('highlight.js/lib/languages/lua')).default); + return hljs.registerLanguage('lua', (await import('hljs9/lib/languages/lua')).default); case 'makefile': - return hljs.registerLanguage('makefile', (await import('highlight.js/lib/languages/makefile')).default); + return hljs.registerLanguage('makefile', (await import('hljs9/lib/languages/makefile')).default); case 'markdown': - return hljs.registerLanguage('markdown', (await import('highlight.js/lib/languages/markdown')).default); + return hljs.registerLanguage('markdown', (await import('hljs9/lib/languages/markdown')).default); case 'mathematica': - return hljs.registerLanguage('mathematica', (await import('highlight.js/lib/languages/mathematica')).default); + return hljs.registerLanguage('mathematica', (await import('hljs9/lib/languages/mathematica')).default); case 'matlab': - return hljs.registerLanguage('matlab', (await import('highlight.js/lib/languages/matlab')).default); + return hljs.registerLanguage('matlab', (await import('hljs9/lib/languages/matlab')).default); case 'maxima': - return hljs.registerLanguage('maxima', (await import('highlight.js/lib/languages/maxima')).default); + return hljs.registerLanguage('maxima', (await import('hljs9/lib/languages/maxima')).default); case 'mel': - return hljs.registerLanguage('mel', (await import('highlight.js/lib/languages/mel')).default); + return hljs.registerLanguage('mel', (await import('hljs9/lib/languages/mel')).default); case 'mercury': - return hljs.registerLanguage('mercury', (await import('highlight.js/lib/languages/mercury')).default); + return hljs.registerLanguage('mercury', (await import('hljs9/lib/languages/mercury')).default); case 'mipsasm': - return hljs.registerLanguage('mipsasm', (await import('highlight.js/lib/languages/mipsasm')).default); + return hljs.registerLanguage('mipsasm', (await import('hljs9/lib/languages/mipsasm')).default); case 'mizar': - return hljs.registerLanguage('mizar', (await import('highlight.js/lib/languages/mizar')).default); + return hljs.registerLanguage('mizar', (await import('hljs9/lib/languages/mizar')).default); case 'perl': - return hljs.registerLanguage('perl', (await import('highlight.js/lib/languages/perl')).default); + return hljs.registerLanguage('perl', (await import('hljs9/lib/languages/perl')).default); case 'mojolicious': - return hljs.registerLanguage('mojolicious', (await import('highlight.js/lib/languages/mojolicious')).default); + return hljs.registerLanguage('mojolicious', (await import('hljs9/lib/languages/mojolicious')).default); case 'monkey': - return hljs.registerLanguage('monkey', (await import('highlight.js/lib/languages/monkey')).default); + return hljs.registerLanguage('monkey', (await import('hljs9/lib/languages/monkey')).default); case 'moonscript': - return hljs.registerLanguage('moonscript', (await import('highlight.js/lib/languages/moonscript')).default); + return hljs.registerLanguage('moonscript', (await import('hljs9/lib/languages/moonscript')).default); case 'n1ql': - return hljs.registerLanguage('n1ql', (await import('highlight.js/lib/languages/n1ql')).default); + return hljs.registerLanguage('n1ql', (await import('hljs9/lib/languages/n1ql')).default); case 'nginx': - return hljs.registerLanguage('nginx', (await import('highlight.js/lib/languages/nginx')).default); + return hljs.registerLanguage('nginx', (await import('hljs9/lib/languages/nginx')).default); case 'nimrod': - return hljs.registerLanguage('nimrod', (await import('highlight.js/lib/languages/nimrod')).default); + return hljs.registerLanguage('nimrod', (await import('hljs9/lib/languages/nimrod')).default); case 'nix': - return hljs.registerLanguage('nix', (await import('highlight.js/lib/languages/nix')).default); + return hljs.registerLanguage('nix', (await import('hljs9/lib/languages/nix')).default); case 'nsis': - return hljs.registerLanguage('nsis', (await import('highlight.js/lib/languages/nsis')).default); + return hljs.registerLanguage('nsis', (await import('hljs9/lib/languages/nsis')).default); case 'objectivec': - return hljs.registerLanguage('objectivec', (await import('highlight.js/lib/languages/objectivec')).default); + return hljs.registerLanguage('objectivec', (await import('hljs9/lib/languages/objectivec')).default); case 'ocaml': - return hljs.registerLanguage('ocaml', (await import('highlight.js/lib/languages/ocaml')).default); + return hljs.registerLanguage('ocaml', (await import('hljs9/lib/languages/ocaml')).default); case 'openscad': - return hljs.registerLanguage('openscad', (await import('highlight.js/lib/languages/openscad')).default); + return hljs.registerLanguage('openscad', (await import('hljs9/lib/languages/openscad')).default); case 'oxygene': - return hljs.registerLanguage('oxygene', (await import('highlight.js/lib/languages/oxygene')).default); + return hljs.registerLanguage('oxygene', (await import('hljs9/lib/languages/oxygene')).default); case 'parser3': - return hljs.registerLanguage('parser3', (await import('highlight.js/lib/languages/parser3')).default); + return hljs.registerLanguage('parser3', (await import('hljs9/lib/languages/parser3')).default); case 'pf': - return hljs.registerLanguage('pf', (await import('highlight.js/lib/languages/pf')).default); + return hljs.registerLanguage('pf', (await import('hljs9/lib/languages/pf')).default); case 'php': - return hljs.registerLanguage('php', (await import('highlight.js/lib/languages/php')).default); + return hljs.registerLanguage('php', (await import('hljs9/lib/languages/php')).default); case 'pony': - return hljs.registerLanguage('pony', (await import('highlight.js/lib/languages/pony')).default); + return hljs.registerLanguage('pony', (await import('hljs9/lib/languages/pony')).default); case 'powershell': - return hljs.registerLanguage('powershell', (await import('highlight.js/lib/languages/powershell')).default); + return hljs.registerLanguage('powershell', (await import('hljs9/lib/languages/powershell')).default); case 'processing': - return hljs.registerLanguage('processing', (await import('highlight.js/lib/languages/processing')).default); + return hljs.registerLanguage('processing', (await import('hljs9/lib/languages/processing')).default); case 'profile': - return hljs.registerLanguage('profile', (await import('highlight.js/lib/languages/profile')).default); + return hljs.registerLanguage('profile', (await import('hljs9/lib/languages/profile')).default); case 'prolog': - return hljs.registerLanguage('prolog', (await import('highlight.js/lib/languages/prolog')).default); + return hljs.registerLanguage('prolog', (await import('hljs9/lib/languages/prolog')).default); case 'protobuf': - return hljs.registerLanguage('protobuf', (await import('highlight.js/lib/languages/protobuf')).default); + return hljs.registerLanguage('protobuf', (await import('hljs9/lib/languages/protobuf')).default); case 'puppet': - return hljs.registerLanguage('puppet', (await import('highlight.js/lib/languages/puppet')).default); + return hljs.registerLanguage('puppet', (await import('hljs9/lib/languages/puppet')).default); case 'purebasic': - return hljs.registerLanguage('purebasic', (await import('highlight.js/lib/languages/purebasic')).default); + return hljs.registerLanguage('purebasic', (await import('hljs9/lib/languages/purebasic')).default); case 'python': - return hljs.registerLanguage('python', (await import('highlight.js/lib/languages/python')).default); + return hljs.registerLanguage('python', (await import('hljs9/lib/languages/python')).default); case 'q': - return hljs.registerLanguage('q', (await import('highlight.js/lib/languages/q')).default); + return hljs.registerLanguage('q', (await import('hljs9/lib/languages/q')).default); case 'qml': - return hljs.registerLanguage('qml', (await import('highlight.js/lib/languages/qml')).default); + return hljs.registerLanguage('qml', (await import('hljs9/lib/languages/qml')).default); case 'r': - return hljs.registerLanguage('r', (await import('highlight.js/lib/languages/r')).default); + return hljs.registerLanguage('r', (await import('hljs9/lib/languages/r')).default); case 'rib': - return hljs.registerLanguage('rib', (await import('highlight.js/lib/languages/rib')).default); + return hljs.registerLanguage('rib', (await import('hljs9/lib/languages/rib')).default); case 'roboconf': - return hljs.registerLanguage('roboconf', (await import('highlight.js/lib/languages/roboconf')).default); + return hljs.registerLanguage('roboconf', (await import('hljs9/lib/languages/roboconf')).default); case 'rsl': - return hljs.registerLanguage('rsl', (await import('highlight.js/lib/languages/rsl')).default); + return hljs.registerLanguage('rsl', (await import('hljs9/lib/languages/rsl')).default); case 'ruleslanguage': - return hljs.registerLanguage('ruleslanguage', (await import('highlight.js/lib/languages/ruleslanguage')).default); + return hljs.registerLanguage('ruleslanguage', (await import('hljs9/lib/languages/ruleslanguage')).default); case 'rust': - return hljs.registerLanguage('rust', (await import('highlight.js/lib/languages/rust')).default); + return hljs.registerLanguage('rust', (await import('hljs9/lib/languages/rust')).default); case 'scala': - return hljs.registerLanguage('scala', (await import('highlight.js/lib/languages/scala')).default); + return hljs.registerLanguage('scala', (await import('hljs9/lib/languages/scala')).default); case 'scheme': - return hljs.registerLanguage('scheme', (await import('highlight.js/lib/languages/scheme')).default); + return hljs.registerLanguage('scheme', (await import('hljs9/lib/languages/scheme')).default); case 'scilab': - return hljs.registerLanguage('scilab', (await import('highlight.js/lib/languages/scilab')).default); + return hljs.registerLanguage('scilab', (await import('hljs9/lib/languages/scilab')).default); case 'scss': - return hljs.registerLanguage('scss', (await import('highlight.js/lib/languages/scss')).default); + return hljs.registerLanguage('scss', (await import('hljs9/lib/languages/scss')).default); case 'shell': - return hljs.registerLanguage('shell', (await import('highlight.js/lib/languages/shell')).default); + return hljs.registerLanguage('shell', (await import('hljs9/lib/languages/shell')).default); case 'smali': - return hljs.registerLanguage('smali', (await import('highlight.js/lib/languages/smali')).default); + return hljs.registerLanguage('smali', (await import('hljs9/lib/languages/smali')).default); case 'smalltalk': - return hljs.registerLanguage('smalltalk', (await import('highlight.js/lib/languages/smalltalk')).default); + return hljs.registerLanguage('smalltalk', (await import('hljs9/lib/languages/smalltalk')).default); case 'sml': - return hljs.registerLanguage('sml', (await import('highlight.js/lib/languages/sml')).default); + return hljs.registerLanguage('sml', (await import('hljs9/lib/languages/sml')).default); case 'sqf': - return hljs.registerLanguage('sqf', (await import('highlight.js/lib/languages/sqf')).default); + return hljs.registerLanguage('sqf', (await import('hljs9/lib/languages/sqf')).default); case 'sql': - return hljs.registerLanguage('sql', (await import('highlight.js/lib/languages/sql')).default); + return hljs.registerLanguage('sql', (await import('hljs9/lib/languages/sql')).default); case 'stan': - return hljs.registerLanguage('stan', (await import('highlight.js/lib/languages/stan')).default); + return hljs.registerLanguage('stan', (await import('hljs9/lib/languages/stan')).default); case 'stata': - return hljs.registerLanguage('stata', (await import('highlight.js/lib/languages/stata')).default); + return hljs.registerLanguage('stata', (await import('hljs9/lib/languages/stata')).default); case 'step21': - return hljs.registerLanguage('step21', (await import('highlight.js/lib/languages/step21')).default); + return hljs.registerLanguage('step21', (await import('hljs9/lib/languages/step21')).default); case 'stylus': - return hljs.registerLanguage('stylus', (await import('highlight.js/lib/languages/stylus')).default); + return hljs.registerLanguage('stylus', (await import('hljs9/lib/languages/stylus')).default); case 'subunit': - return hljs.registerLanguage('subunit', (await import('highlight.js/lib/languages/subunit')).default); + return hljs.registerLanguage('subunit', (await import('hljs9/lib/languages/subunit')).default); case 'swift': - return hljs.registerLanguage('swift', (await import('highlight.js/lib/languages/swift')).default); + return hljs.registerLanguage('swift', (await import('hljs9/lib/languages/swift')).default); case 'taggerscript': - return hljs.registerLanguage('taggerscript', (await import('highlight.js/lib/languages/taggerscript')).default); + return hljs.registerLanguage('taggerscript', (await import('hljs9/lib/languages/taggerscript')).default); case 'yaml': - return hljs.registerLanguage('yaml', (await import('highlight.js/lib/languages/yaml')).default); + return hljs.registerLanguage('yaml', (await import('hljs9/lib/languages/yaml')).default); case 'tap': - return hljs.registerLanguage('tap', (await import('highlight.js/lib/languages/tap')).default); + return hljs.registerLanguage('tap', (await import('hljs9/lib/languages/tap')).default); case 'tcl': - return hljs.registerLanguage('tcl', (await import('highlight.js/lib/languages/tcl')).default); + return hljs.registerLanguage('tcl', (await import('hljs9/lib/languages/tcl')).default); case 'tex': - return hljs.registerLanguage('tex', (await import('highlight.js/lib/languages/tex')).default); + return hljs.registerLanguage('tex', (await import('hljs9/lib/languages/tex')).default); case 'thrift': - return hljs.registerLanguage('thrift', (await import('highlight.js/lib/languages/thrift')).default); + return hljs.registerLanguage('thrift', (await import('hljs9/lib/languages/thrift')).default); case 'tp': - return hljs.registerLanguage('tp', (await import('highlight.js/lib/languages/tp')).default); + return hljs.registerLanguage('tp', (await import('hljs9/lib/languages/tp')).default); case 'twig': - return hljs.registerLanguage('twig', (await import('highlight.js/lib/languages/twig')).default); + return hljs.registerLanguage('twig', (await import('hljs9/lib/languages/twig')).default); case 'typescript': - return hljs.registerLanguage('typescript', (await import('highlight.js/lib/languages/typescript')).default); + return hljs.registerLanguage('typescript', (await import('hljs9/lib/languages/typescript')).default); case 'vala': - return hljs.registerLanguage('vala', (await import('highlight.js/lib/languages/vala')).default); + return hljs.registerLanguage('vala', (await import('hljs9/lib/languages/vala')).default); case 'vbnet': - return hljs.registerLanguage('vbnet', (await import('highlight.js/lib/languages/vbnet')).default); + return hljs.registerLanguage('vbnet', (await import('hljs9/lib/languages/vbnet')).default); case 'vbscript': - return hljs.registerLanguage('vbscript', (await import('highlight.js/lib/languages/vbscript')).default); + return hljs.registerLanguage('vbscript', (await import('hljs9/lib/languages/vbscript')).default); case 'vbscript-html': - return hljs.registerLanguage('vbscript-html(', (await import('highlight.js/lib/languages/vbscript-html')).default); + return hljs.registerLanguage('vbscript-html(', (await import('hljs9/lib/languages/vbscript-html')).default); case 'verilog': - return hljs.registerLanguage('verilog', (await import('highlight.js/lib/languages/verilog')).default); + return hljs.registerLanguage('verilog', (await import('hljs9/lib/languages/verilog')).default); case 'vhdl': - return hljs.registerLanguage('vhdl', (await import('highlight.js/lib/languages/vhdl')).default); + return hljs.registerLanguage('vhdl', (await import('hljs9/lib/languages/vhdl')).default); case 'vim': - return hljs.registerLanguage('vim', (await import('highlight.js/lib/languages/vim')).default); + return hljs.registerLanguage('vim', (await import('hljs9/lib/languages/vim')).default); case 'x86asm': - return hljs.registerLanguage('x86asm', (await import('highlight.js/lib/languages/x86asm')).default); + return hljs.registerLanguage('x86asm', (await import('hljs9/lib/languages/x86asm')).default); case 'xl': - return hljs.registerLanguage('xl', (await import('highlight.js/lib/languages/xl')).default); + return hljs.registerLanguage('xl', (await import('hljs9/lib/languages/xl')).default); case 'xquery': - return hljs.registerLanguage('xquery', (await import('highlight.js/lib/languages/xquery')).default); + return hljs.registerLanguage('xquery', (await import('hljs9/lib/languages/xquery')).default); case 'zephir': - return hljs.registerLanguage('zephir', (await import('highlight.js/lib/languages/zephir')).default); + return hljs.registerLanguage('zephir', (await import('hljs9/lib/languages/zephir')).default); default: - return hljs.registerLanguage('plaintext', (await import('highlight.js/lib/languages/plaintext')).default); + return hljs.registerLanguage('plaintext', (await import('hljs9/lib/languages/plaintext')).default); } }; diff --git a/apps/meteor/app/markdown/lib/parser/marked/marked.js b/apps/meteor/app/markdown/lib/parser/marked/marked.js index 7739fe77b7da..a1aff30618c4 100644 --- a/apps/meteor/app/markdown/lib/parser/marked/marked.js +++ b/apps/meteor/app/markdown/lib/parser/marked/marked.js @@ -1,6 +1,6 @@ import { Random } from 'meteor/random'; import _ from 'underscore'; -import _marked from 'marked'; +import { marked as _marked } from 'marked'; import createDOMPurify from 'dompurify'; import { unescapeHTML, escapeHTML } from '@rocket.chat/string-helpers'; @@ -87,7 +87,7 @@ export const marked = (message, { marked: { gfm, tables, breaks, pedantic, smart message.tokens = []; } - message.html = _marked(unescapeHTML(message.html), { + message.html = _marked.parse(unescapeHTML(message.html), { gfm, tables, breaks, @@ -100,7 +100,7 @@ export const marked = (message, { marked: { gfm, tables, breaks, pedantic, smart const window = getGlobalWindow(); const DomPurify = createDOMPurify(window); - message.html = DomPurify.sanitize(message.html, { ADD_ATTR: ['target'] }); + message.html = DomPurify.sanitize(message.html, { ADD_ATTR: ['target'], FORBID_ATTR: ['style'], FORBID_TAGS: ['style'] }); return message; }; diff --git a/apps/meteor/app/markdown/server/index.js b/apps/meteor/app/markdown/server/index.js index 4a4a56a13f89..9b4f3084c58b 100644 --- a/apps/meteor/app/markdown/server/index.js +++ b/apps/meteor/app/markdown/server/index.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { callbacks } from '../../../lib/callbacks'; -import { settings } from '../../settings'; +import { settings } from '../../settings/server'; import { createMarkdownMessageRenderer, createMarkdownNotificationRenderer } from '../lib/markdown'; import './settings'; diff --git a/apps/meteor/app/markdown/server/settings.ts b/apps/meteor/app/markdown/server/settings.ts index e25ed0a1872d..b2f231059097 100644 --- a/apps/meteor/app/markdown/server/settings.ts +++ b/apps/meteor/app/markdown/server/settings.ts @@ -19,6 +19,7 @@ settingsRegistry.add('Markdown_Parser', 'original', { group: 'Message', section: 'Markdown', public: true, + alert: 'Use_Legacy_Message_Template_alert', }); const enableQueryOriginal = { _id: 'Markdown_Parser', value: 'original' }; diff --git a/apps/meteor/app/mentions-flextab/client/actionButton.ts b/apps/meteor/app/mentions-flextab/client/actionButton.ts index c9f45e4920a8..11168a8cae7a 100644 --- a/apps/meteor/app/mentions-flextab/client/actionButton.ts +++ b/apps/meteor/app/mentions-flextab/client/actionButton.ts @@ -3,7 +3,7 @@ import { Template } from 'meteor/templating'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { MessageAction, RoomHistoryManager } from '../../ui-utils/client'; -import { messageArgs } from '../../ui-utils/client/lib/messageArgs'; +import { messageArgs } from '../../../client/lib/utils/messageArgs'; import { Rooms } from '../../models/client'; Meteor.startup(function () { diff --git a/apps/meteor/app/mentions-flextab/client/tabBar.ts b/apps/meteor/app/mentions-flextab/client/tabBar.ts index fa88a0f2d96d..1699005c02f1 100644 --- a/apps/meteor/app/mentions-flextab/client/tabBar.ts +++ b/apps/meteor/app/mentions-flextab/client/tabBar.ts @@ -1,10 +1,20 @@ +import { isRoomFederated } from '@rocket.chat/core-typings'; + import { addAction } from '../../../client/views/room/lib/Toolbox'; -addAction('mentions', { - groups: ['channel', 'group', 'team'], - id: 'mentions', - title: 'Mentions', - icon: 'at', - template: 'mentionsFlexTab', - order: 9, +addAction('mentions', ({ room }) => { + const federated = isRoomFederated(room); + + return { + groups: ['channel', 'group', 'team'], + id: 'mentions', + title: 'Mentions', + icon: 'at', + template: 'mentionsFlexTab', + ...(federated && { + 'disabled': true, + 'data-tooltip': 'Mentions_unavailable_for_federation', + }), + order: 9, + }; }); diff --git a/apps/meteor/app/mentions-flextab/client/views/mentionsFlexTab.js b/apps/meteor/app/mentions-flextab/client/views/mentionsFlexTab.js index 97fd7e1c5659..92b10b8a57d0 100644 --- a/apps/meteor/app/mentions-flextab/client/views/mentionsFlexTab.js +++ b/apps/meteor/app/mentions-flextab/client/views/mentionsFlexTab.js @@ -61,7 +61,10 @@ Template.mentionsFlexTab.onCreated(function () { this.autorun(async () => { const limit = this.limit.get(); - const { messages, total } = await APIClient.v1.get(`chat.getMentionedMessages?roomId=${this.data.rid}&count=${limit}`); + const { messages, total } = await APIClient.get('/v1/chat.getMentionedMessages', { + roomId: this.data.rid, + count: limit, + }); upsertMessageBulk({ msgs: messages }, this.messages); diff --git a/apps/meteor/app/mentions/client/mentionLink.css b/apps/meteor/app/mentions/client/mentionLink.css index 4915ce792ab9..e338b20ea1f2 100644 --- a/apps/meteor/app/mentions/client/mentionLink.css +++ b/apps/meteor/app/mentions/client/mentionLink.css @@ -16,7 +16,6 @@ &:hover { opacity: 0.6; - color: var(--mention-link-text-color); } &--me { diff --git a/apps/meteor/app/mentions/server/methods/getUserMentionsByChannel.js b/apps/meteor/app/mentions/server/methods/getUserMentionsByChannel.js index d554fe1b32a6..8318c8a120aa 100644 --- a/apps/meteor/app/mentions/server/methods/getUserMentionsByChannel.js +++ b/apps/meteor/app/mentions/server/methods/getUserMentionsByChannel.js @@ -1,7 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { Rooms, Users, Messages } from '../../../models'; +import { Rooms, Users, Messages } from '../../../models/server'; +import { canAccessRoom } from '../../../authorization/server'; Meteor.methods({ getUserMentionsByChannel({ roomId, options }) { @@ -13,16 +14,16 @@ Meteor.methods({ }); } + const user = Users.findOneById(Meteor.userId()); + const room = Rooms.findOneById(roomId); - if (!room) { + if (!room || !canAccessRoom(room, user)) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'getUserMentionsByChannel', }); } - const user = Users.findOneById(Meteor.userId()); - return Messages.findVisibleByMentionAndRoomId(user.username, roomId, options).fetch(); }, }); diff --git a/apps/meteor/app/mentions/server/server.js b/apps/meteor/app/mentions/server/server.js index 280211c9c7ba..c82a7c3449e8 100644 --- a/apps/meteor/app/mentions/server/server.js +++ b/apps/meteor/app/mentions/server/server.js @@ -2,9 +2,9 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import MentionsServer from './Mentions'; -import { settings } from '../../settings'; +import { settings } from '../../settings/server'; import { callbacks } from '../../../lib/callbacks'; -import { Users, Subscriptions, Rooms } from '../../models'; +import { Users, Subscriptions, Rooms } from '../../models/server'; import { api } from '../../../server/sdk/api'; export class MentionQueries { diff --git a/apps/meteor/app/message-mark-as-unread/client/actionButton.ts b/apps/meteor/app/message-mark-as-unread/client/actionButton.ts index d9c432643265..bccd985749db 100644 --- a/apps/meteor/app/message-mark-as-unread/client/actionButton.ts +++ b/apps/meteor/app/message-mark-as-unread/client/actionButton.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { RoomManager, MessageAction } from '../../ui-utils/client'; -import { messageArgs } from '../../ui-utils/client/lib/messageArgs'; +import { messageArgs } from '../../../client/lib/utils/messageArgs'; import { ChatSubscription } from '../../models/client'; import { handleError } from '../../../client/lib/utils/handleError'; import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; diff --git a/apps/meteor/app/message-pin/client/actionButton.ts b/apps/meteor/app/message-pin/client/actionButton.ts index 7ea4d1664e46..e69f6c272d30 100644 --- a/apps/meteor/app/message-pin/client/actionButton.ts +++ b/apps/meteor/app/message-pin/client/actionButton.ts @@ -4,7 +4,7 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { RoomHistoryManager, MessageAction } from '../../ui-utils/client'; -import { messageArgs } from '../../ui-utils/client/lib/messageArgs'; +import { messageArgs } from '../../../client/lib/utils/messageArgs'; import { settings } from '../../settings/client'; import { hasAtLeastOnePermission } from '../../authorization/client'; import { Rooms } from '../../models/client'; diff --git a/apps/meteor/app/message-pin/client/pinMessage.js b/apps/meteor/app/message-pin/client/pinMessage.js index 9e2ec2da0caa..b45bed7839c3 100644 --- a/apps/meteor/app/message-pin/client/pinMessage.js +++ b/apps/meteor/app/message-pin/client/pinMessage.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { settings } from '../../settings'; -import { ChatMessage, Subscriptions } from '../../models'; +import { ChatMessage, Subscriptions } from '../../models/client'; import { dispatchToastMessage } from '../../../client/lib/toast'; Meteor.methods({ diff --git a/apps/meteor/app/message-pin/client/tabBar.ts b/apps/meteor/app/message-pin/client/tabBar.ts index a5b5242d83b2..6065172f7cb6 100644 --- a/apps/meteor/app/message-pin/client/tabBar.ts +++ b/apps/meteor/app/message-pin/client/tabBar.ts @@ -1,10 +1,12 @@ import { useMemo } from 'react'; import { useSetting } from '@rocket.chat/ui-contexts'; +import { isRoomFederated } from '@rocket.chat/core-typings'; import { addAction } from '../../../client/views/room/lib/Toolbox'; -addAction('pinned-messages', () => { +addAction('pinned-messages', ({ room }) => { const pinningAllowed = useSetting('Message_AllowPinning'); + const federated = isRoomFederated(room); return useMemo( () => pinningAllowed @@ -14,9 +16,13 @@ addAction('pinned-messages', () => { title: 'Pinned_Messages', icon: 'pin', template: 'pinnedMessages', + ...(federated && { + 'data-tooltip': 'Pinned_messages_unavailable_for_federation', + 'disabled': true, + }), order: 11, } : null, - [pinningAllowed], + [pinningAllowed, federated], ); }); diff --git a/apps/meteor/app/message-pin/client/views/pinnedMessages.js b/apps/meteor/app/message-pin/client/views/pinnedMessages.js index 104777b58eee..75fc482f913c 100644 --- a/apps/meteor/app/message-pin/client/views/pinnedMessages.js +++ b/apps/meteor/app/message-pin/client/views/pinnedMessages.js @@ -62,7 +62,10 @@ Template.pinnedMessages.onCreated(function () { this.autorun(async () => { const limit = this.limit.get(); - const { messages, total } = await APIClient.v1.get(`chat.getPinnedMessages?roomId=${this.rid}&count=${limit}`); + const { messages, total } = await APIClient.get('/v1/chat.getPinnedMessages', { + roomId: this.rid, + count: limit, + }); upsertMessageBulk({ msgs: messages }, this.messages); diff --git a/apps/meteor/app/message-pin/server/pinMessage.js b/apps/meteor/app/message-pin/server/pinMessage.js index 6f9c11d51589..c176d16acd45 100644 --- a/apps/meteor/app/message-pin/server/pinMessage.js +++ b/apps/meteor/app/message-pin/server/pinMessage.js @@ -6,7 +6,7 @@ import { callbacks } from '../../../lib/callbacks'; import { isTheLastMessage } from '../../lib/server'; import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; import { canAccessRoom, hasPermission, roomAccessAttributes } from '../../authorization/server'; -import { Subscriptions, Messages, Users, Rooms } from '../../models'; +import { Subscriptions, Messages, Users, Rooms } from '../../models/server'; import { Apps, AppEvents } from '../../apps/server/orchestrator'; const recursiveRemove = (msg, deep = 1) => { diff --git a/apps/meteor/app/message-pin/server/startup/indexes.js b/apps/meteor/app/message-pin/server/startup/indexes.js index c73616e0d1a0..c6556421c38a 100644 --- a/apps/meteor/app/message-pin/server/startup/indexes.js +++ b/apps/meteor/app/message-pin/server/startup/indexes.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { Messages } from '../../../models'; +import { Messages } from '../../../models/server'; Meteor.startup(function () { return Meteor.defer(function () { diff --git a/apps/meteor/app/message-snippet/client/actionButton.js b/apps/meteor/app/message-snippet/client/actionButton.js deleted file mode 100644 index 342ff2b2854d..000000000000 --- a/apps/meteor/app/message-snippet/client/actionButton.js +++ /dev/null @@ -1,69 +0,0 @@ -// import { Meteor } from 'meteor/meteor'; -// import { MessageAction, modal } from '../../ui-utils'; -// import { messageArgs } from '../../ui-utils/client/lib/messageArgs'; -// import { t, handleError } from '../../utils'; -// import { settings } from '../../settings'; -// import { Subscriptions } from '../../models'; -// import { hasAtLeastOnePermission } from '../../authorization'; -// -// Meteor.startup(function() { -// MessageAction.addButton({ -// id: 'snippeted-message', -// icon: 'code', -// label: 'Snippet', -// context: [ -// 'snippeted', -// 'message', -// 'message-mobile', -// ], -// order: 10, -// group: 'menu', -// action() { -// const { msg: message } = messageArgs(this); -// -// modal.open({ -// title: 'Create a Snippet', -// text: 'The name of your snippet (with file extension):', -// type: 'input', -// showCancelButton: true, -// closeOnConfirm: false, -// inputPlaceholder: 'Snippet name', -// }, function(filename) { -// if (filename === false) { -// return false; -// } -// if (filename === '') { -// modal.showInputError('You need to write something!'); -// return false; -// } -// message.snippeted = true; -// Meteor.call('snippetMessage', message, filename, function(error) { -// if (error) { -// return handleError(error); -// } -// modal.open({ -// title: t('Nice'), -// text: `Snippet '${ filename }' created.`, -// type: 'success', -// timer: 2000, -// }); -// }); -// }); -// -// }, -// condition(message) { -// if (Subscriptions.findOne({ rid: message.rid, 'u._id': Meteor.userId() }) === undefined) { -// return false; -// } -// -// if (message.snippeted || ((settings.get('Message_AllowSnippeting') === undefined) || -// (settings.get('Message_AllowSnippeting') === null) || -// (settings.get('Message_AllowSnippeting')) === false)) { -// return false; -// } -// -// return hasAtLeastOnePermission('snippet-message', message.rid); -// }, -// }); -// -// }); diff --git a/apps/meteor/app/message-snippet/client/index.js b/apps/meteor/app/message-snippet/client/index.js index c3a4c091f880..d5b477773204 100644 --- a/apps/meteor/app/message-snippet/client/index.js +++ b/apps/meteor/app/message-snippet/client/index.js @@ -1,4 +1,3 @@ -import './actionButton'; import './messageType'; import './snippetMessage'; import './page/snippetPage.html'; diff --git a/apps/meteor/app/message-snippet/client/page/snippetPage.js b/apps/meteor/app/message-snippet/client/page/snippetPage.js index 48e2706302aa..0cd662305e7d 100644 --- a/apps/meteor/app/message-snippet/client/page/snippetPage.js +++ b/apps/meteor/app/message-snippet/client/page/snippetPage.js @@ -39,6 +39,6 @@ Template.snippetPage.onCreated(async function () { const snippetId = FlowRouter.getParam('snippetId'); this.message = new ReactiveVar({}); - const { message } = await APIClient.v1.get(`chat.getSnippetedMessageById?messageId=${snippetId}`); + const { message } = await APIClient.get('/v1/chat.getSnippetedMessageById', { messageId: snippetId }); this.message.set(message); }); diff --git a/apps/meteor/app/message-snippet/client/snippetMessage.js b/apps/meteor/app/message-snippet/client/snippetMessage.js index 5edefd30e423..3c8a35d6cfdc 100644 --- a/apps/meteor/app/message-snippet/client/snippetMessage.js +++ b/apps/meteor/app/message-snippet/client/snippetMessage.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { settings } from '../../settings'; -import { ChatMessage, Subscriptions } from '../../models'; +import { ChatMessage, Subscriptions } from '../../models/client'; Meteor.methods({ snippetMessage(message) { diff --git a/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.js b/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.js index 0dbefc6c01bb..4ee7fee88472 100644 --- a/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.js +++ b/apps/meteor/app/message-snippet/client/tabBar/views/snippetedMessages.js @@ -56,7 +56,10 @@ Template.snippetedMessages.onCreated(function () { this.autorun(async () => { const limit = this.limit.get(); - const { messages, total } = await APIClient.v1.get(`chat.getSnippetedMessages?roomId=${this.rid}&count=${limit}`); + const { messages, total } = await APIClient.get('/v1/chat.getSnippetedMessages', { + roomId: this.rid, + count: limit, + }); upsertMessageBulk({ msgs: messages }, this.messages); diff --git a/apps/meteor/app/message-snippet/server/methods/snippetMessage.js b/apps/meteor/app/message-snippet/server/methods/snippetMessage.js index b332e37b0fd3..987c3102c71d 100644 --- a/apps/meteor/app/message-snippet/server/methods/snippetMessage.js +++ b/apps/meteor/app/message-snippet/server/methods/snippetMessage.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; -import { Subscriptions, Messages, Users, Rooms } from '../../../models'; -import { settings } from '../../../settings'; +import { Subscriptions, Messages, Users, Rooms } from '../../../models/server'; +import { settings } from '../../../settings/server'; import { callbacks } from '../../../../lib/callbacks'; import { isTheLastMessage } from '../../../lib'; diff --git a/apps/meteor/app/message-snippet/server/requests.js b/apps/meteor/app/message-snippet/server/requests.js index d06fd06b075b..63f74bf59a00 100644 --- a/apps/meteor/app/message-snippet/server/requests.js +++ b/apps/meteor/app/message-snippet/server/requests.js @@ -1,7 +1,7 @@ import { WebApp } from 'meteor/webapp'; import { Cookies } from 'meteor/ostrio:cookies'; -import { Users, Rooms, Messages } from '../../models'; +import { Users, Rooms, Messages } from '../../models/server'; WebApp.connectHandlers.use('/snippet/download', function (req, res) { let rawCookies; diff --git a/apps/meteor/app/message-star/client/actionButton.ts b/apps/meteor/app/message-star/client/actionButton.ts index aa3597bafec9..981fce06dc53 100644 --- a/apps/meteor/app/message-star/client/actionButton.ts +++ b/apps/meteor/app/message-star/client/actionButton.ts @@ -5,7 +5,7 @@ import { FlowRouter } from 'meteor/kadira:flow-router'; import { settings } from '../../settings/client'; import { RoomHistoryManager, MessageAction } from '../../ui-utils/client'; -import { messageArgs } from '../../ui-utils/client/lib/messageArgs'; +import { messageArgs } from '../../../client/lib/utils/messageArgs'; import { Rooms } from '../../models/client'; import { handleError } from '../../../client/lib/utils/handleError'; import { dispatchToastMessage } from '../../../client/lib/toast'; @@ -16,7 +16,7 @@ Meteor.startup(function () { id: 'star-message', icon: 'star', label: 'Star', - context: ['starred', 'message', 'message-mobile', 'threads'], + context: ['starred', 'message', 'message-mobile', 'threads', 'federated'], action(_, props) { const { message = messageArgs(this).msg } = props; Meteor.call('starMessage', { ...message, starred: true }, function (error: any) { @@ -44,7 +44,7 @@ Meteor.startup(function () { id: 'unstar-message', icon: 'star', label: 'Unstar_Message', - context: ['starred', 'message', 'message-mobile', 'threads'], + context: ['starred', 'message', 'message-mobile', 'threads', 'federated'], action(_, props) { const { message = messageArgs(this).msg } = props; diff --git a/apps/meteor/app/message-star/client/starMessage.js b/apps/meteor/app/message-star/client/starMessage.js index 17178341b894..41e2c7ac7775 100644 --- a/apps/meteor/app/message-star/client/starMessage.js +++ b/apps/meteor/app/message-star/client/starMessage.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { settings } from '../../settings'; -import { ChatMessage, Subscriptions } from '../../models'; +import { ChatMessage, Subscriptions } from '../../models/client'; import { dispatchToastMessage } from '../../../client/lib/toast'; Meteor.methods({ diff --git a/apps/meteor/app/message-star/client/views/starredMessages.js b/apps/meteor/app/message-star/client/views/starredMessages.js index a0906d92b604..8b44b85e3f7f 100644 --- a/apps/meteor/app/message-star/client/views/starredMessages.js +++ b/apps/meteor/app/message-star/client/views/starredMessages.js @@ -61,7 +61,10 @@ Template.starredMessages.onCreated(function () { this.autorun(async () => { const limit = this.limit.get(); - const { messages, total } = await APIClient.v1.get(`chat.getStarredMessages?roomId=${this.rid}&count=${limit}`); + const { messages, total } = await APIClient.get('/v1/chat.getStarredMessages', { + roomId: this.rid, + count: limit, + }); upsertMessageBulk({ msgs: messages }, this.messages); diff --git a/apps/meteor/app/message-star/server/startup/indexes.js b/apps/meteor/app/message-star/server/startup/indexes.js index 12350e64a67a..a39f821a3155 100644 --- a/apps/meteor/app/message-star/server/startup/indexes.js +++ b/apps/meteor/app/message-star/server/startup/indexes.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { Messages } from '../../../models'; +import { Messages } from '../../../models/server'; Meteor.startup(function () { return Meteor.defer(function () { diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts index 0e9933aa68af..a9c208c9e462 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts @@ -7,10 +7,10 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import fiber from 'fibers'; import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; import { IUser, IIncomingMessage } from '@rocket.chat/core-typings'; +import { CredentialTokens } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; import { Users, Rooms } from '../../../models/server'; -import { CredentialTokens } from '../../../models/server/raw'; import { saveUserIdentity, createRoom, generateUsernameSuggestion, addUserToRoom } from '../../../lib/server/functions'; import { SAMLServiceProvider } from './ServiceProvider'; import { IServiceProviderOptions } from '../definition/IServiceProviderOptions'; diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts index f1cd3d563052..a6adefb97cd0 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts @@ -95,7 +95,7 @@ export class SAMLServiceProvider { return callback(null, target); } catch (error) { - return callback(error); + return callback(error instanceof Error ? error : String(error)); } }); } @@ -154,7 +154,7 @@ export class SAMLServiceProvider { } callback(null, target); } catch (error) { - callback(error); + callback(error instanceof Error ? error : String(error)); } }); } diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/LogoutRequest.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/LogoutRequest.ts index 22f5d05b6c36..35eb9d9a92b4 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/LogoutRequest.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/LogoutRequest.ts @@ -1,4 +1,4 @@ -import xmldom from 'xmldom'; +import xmldom from '@xmldom/xmldom'; import { SAMLUtils } from '../Utils'; import { IServiceProviderOptions } from '../../definition/IServiceProviderOptions'; @@ -44,7 +44,7 @@ export class LogoutRequestParser { const msg = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'StatusMessage'); SAMLUtils.log(`Unexpected msg from IDP. Does your session still exist at IDP? Idp returned: \n ${msg}`); - return callback(e, null); + return callback(e instanceof Error ? e : String(e), null); } } } diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/LogoutResponse.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/LogoutResponse.ts index 5922382e61c5..a34c2244876a 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/LogoutResponse.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/LogoutResponse.ts @@ -1,4 +1,4 @@ -import xmldom from 'xmldom'; +import xmldom from '@xmldom/xmldom'; import { SAMLUtils } from '../Utils'; import { IServiceProviderOptions } from '../../definition/IServiceProviderOptions'; diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/Response.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/Response.ts index bf92043a5976..c554df00c9f5 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/Response.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/Response.ts @@ -1,4 +1,4 @@ -import xmldom from 'xmldom'; +import xmldom from '@xmldom/xmldom'; import xmlenc from 'xml-encryption'; import xmlCrypto from 'xml-crypto'; @@ -62,7 +62,7 @@ export class ResponseParser { this.verifySignatures(response, assertionData, xml); } catch (e) { - return callback(e, null, false); + return callback(e instanceof Error ? e : String(e), null, false); } const profile: Record = {}; @@ -74,7 +74,7 @@ export class ResponseParser { try { issuer = this.getIssuer(assertion); } catch (e) { - return callback(e, null, false); + return callback(e instanceof Error ? e : String(e), null, false); } if (issuer) { @@ -95,14 +95,14 @@ export class ResponseParser { try { this.validateSubjectConditions(subject); } catch (e) { - return callback(e, null, false); + return callback(e instanceof Error ? e : String(e), null, false); } } try { this.validateAssertionConditions(assertion); } catch (e) { - return callback(e, null, false); + return callback(e instanceof Error ? e : String(e), null, false); } const authnStatement = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'AuthnStatement')[0]; diff --git a/apps/meteor/app/meteor-autocomplete/client/autocomplete-client.js b/apps/meteor/app/meteor-autocomplete/client/autocomplete-client.js index ee50a510cbca..55db42563b26 100755 --- a/apps/meteor/app/meteor-autocomplete/client/autocomplete-client.js +++ b/apps/meteor/app/meteor-autocomplete/client/autocomplete-client.js @@ -131,7 +131,7 @@ export default class AutoComplete { // console.debug 'Subscribing to <%s> in <%s>.<%s>', filter, rule.collection, rule.field this.setLoaded(false); const endpointName = rule.endpoint || 'users.autocomplete'; - const { items } = await APIClient.v1.get(`${endpointName}?selector=${JSON.stringify(selector)}`); + const { items } = await APIClient.get(`/v1/${endpointName}`, { selector: JSON.stringify(selector) }); AutoCompleteRecords.remove({}); items.forEach((item) => AutoCompleteRecords.insert(item)); this.setLoaded(true); diff --git a/apps/meteor/app/metrics/server/lib/collectMetrics.ts b/apps/meteor/app/metrics/server/lib/collectMetrics.ts index 61eacebba057..81b5d7ceccd6 100644 --- a/apps/meteor/app/metrics/server/lib/collectMetrics.ts +++ b/apps/meteor/app/metrics/server/lib/collectMetrics.ts @@ -6,11 +6,11 @@ import _ from 'underscore'; import gcStats from 'prometheus-gc-stats'; import { Meteor } from 'meteor/meteor'; import { Facts } from 'meteor/facts-base'; +import { Statistics } from '@rocket.chat/models'; import { Info, getOplogInfo } from '../../../utils/server'; import { getControl } from '../../../../server/lib/migrations'; import { settings } from '../../../settings/server'; -import { Statistics } from '../../../models/server/raw'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { metrics } from './metrics'; import { getAppsStatistics } from '../../../statistics/server/lib/getAppsStatistics'; diff --git a/apps/meteor/app/models/index.js b/apps/meteor/app/models/index.ts similarity index 100% rename from apps/meteor/app/models/index.js rename to apps/meteor/app/models/index.ts diff --git a/apps/meteor/app/models/server/index.js b/apps/meteor/app/models/server/index.js index 24957884d25d..da1ab9c8549c 100644 --- a/apps/meteor/app/models/server/index.js +++ b/apps/meteor/app/models/server/index.js @@ -10,10 +10,9 @@ import LivechatCustomField from './models/LivechatCustomField'; import LivechatDepartment from './models/LivechatDepartment'; import LivechatDepartmentAgents from './models/LivechatDepartmentAgents'; import LivechatRooms from './models/LivechatRooms'; -import LivechatVisitors from './models/LivechatVisitors'; import LivechatInquiry from './models/LivechatInquiry'; -import OmnichannelQueue from './models/OmnichannelQueue'; import ImportData from './models/ImportData'; +import './lib/watchModels'; export { AppsLogsModel } from './models/apps-logs-model'; export { AppsPersistenceModel } from './models/apps-persistence-model'; @@ -22,7 +21,6 @@ export { FederationRoomEvents } from './models/FederationRoomEvents'; export { MatrixBridgedRoom } from './models/MatrixBridgedRoom'; export { MatrixBridgedUser } from './models/MatrixBridgedUser'; - export { Base, BaseDb, @@ -36,8 +34,6 @@ export { LivechatDepartment, LivechatDepartmentAgents, LivechatRooms, - LivechatVisitors, LivechatInquiry, - OmnichannelQueue, ImportData, }; diff --git a/apps/meteor/app/models/server/lib/watchModels.ts b/apps/meteor/app/models/server/lib/watchModels.ts new file mode 100644 index 000000000000..0b9c50753126 --- /dev/null +++ b/apps/meteor/app/models/server/lib/watchModels.ts @@ -0,0 +1,73 @@ +import { + Messages, + Users, + Subscriptions, + Settings, + LivechatInquiry, + LivechatDepartmentAgents, + Rooms, + UsersSessions, + Roles, + LoginServiceConfiguration, + InstanceStatus, + IntegrationHistory, + Integrations, + EmailInbox, + PbxEvents, + Permissions, +} from '@rocket.chat/models'; + +import { api } from '../../../../server/sdk/api'; +import { BaseDbWatch } from '../models/_BaseDb'; +import { initWatchers } from '../../../../server/modules/watchers/watchers.module'; +import { isRunningMs } from '../../../../server/lib/isRunningMs'; +import { BaseRaw } from '../../../../server/models/raw/BaseRaw'; +import MessagesModel from '../models/Messages'; +import UsersModel from '../models/Users'; +import SubscriptionsModel from '../models/Subscriptions'; +import SettingsModel from '../models/Settings'; +import LivechatInquiryModel from '../models/LivechatInquiry'; +import LivechatDepartmentAgentsModel from '../models/LivechatDepartmentAgents'; +import RoomsModel from '../models/Rooms'; + +const map = { + [Messages.col.collectionName]: MessagesModel, + [Users.col.collectionName]: UsersModel, + [Subscriptions.col.collectionName]: SubscriptionsModel, + [Settings.col.collectionName]: SettingsModel, + [LivechatInquiry.col.collectionName]: LivechatInquiryModel, + [LivechatDepartmentAgents.col.collectionName]: LivechatDepartmentAgentsModel, + [Rooms.col.collectionName]: RoomsModel, +}; + +if (!isRunningMs()) { + const models = { + Messages, + Users, + Subscriptions, + Settings, + LivechatInquiry, + LivechatDepartmentAgents, + UsersSessions, + Permissions, + Roles, + Rooms, + LoginServiceConfiguration, + InstanceStatus, + IntegrationHistory, + Integrations, + EmailInbox, + PbxEvents, + }; + + initWatchers(models, api.broadcastLocal.bind(api), (model, fn) => { + const { collectionName } = (model as BaseRaw).col; + + const meteorModel = map[collectionName] || new BaseDbWatch(collectionName); + if (!meteorModel) { + return; + } + + meteorModel.on('change', fn); + }); +} diff --git a/apps/meteor/app/models/server/models/LivechatDepartmentAgents.js b/apps/meteor/app/models/server/models/LivechatDepartmentAgents.js index ea26943fe804..d3e9111b33f5 100644 --- a/apps/meteor/app/models/server/models/LivechatDepartmentAgents.js +++ b/apps/meteor/app/models/server/models/LivechatDepartmentAgents.js @@ -1,4 +1,3 @@ -import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { Base } from './_Base'; @@ -14,9 +13,6 @@ export class LivechatDepartmentAgents extends Base { this.tryEnsureIndex({ departmentEnabled: 1 }); this.tryEnsureIndex({ agentId: 1 }); this.tryEnsureIndex({ username: 1 }); - - const collectionObj = this.model.rawCollection(); - this.findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj); } findByDepartmentId(departmentId) { @@ -96,7 +92,7 @@ export class LivechatDepartmentAgents extends Base { const collectionObj = this.model.rawCollection(); - const agent = Promise.await(collectionObj.findAndModify(query, sort, update)); + const agent = Promise.await(collectionObj.findOneAndUpdate(query, update, { sort, returnNewDocument: 'after' })); if (agent && agent.value) { return { agentId: agent.value.agentId, @@ -159,7 +155,7 @@ export class LivechatDepartmentAgents extends Base { return this.find(query); } - getNextBotForDepartment(departmentId, ignoreAgentId) { + async getNextBotForDepartment(departmentId, ignoreAgentId) { const agents = this.findByDepartmentId(departmentId).fetch(); if (agents.length === 0) { @@ -188,7 +184,7 @@ export class LivechatDepartmentAgents extends Base { }, }; - const bot = this.findAndModify(query, sort, update); + const bot = await this.model.rawCollection().findOneAndUpdate(query, update, { sort, returnNewDocument: 'after' }); if (bot && bot.value) { return { agentId: bot.value.agentId, diff --git a/apps/meteor/app/models/server/models/LivechatInquiry.js b/apps/meteor/app/models/server/models/LivechatInquiry.js deleted file mode 100644 index c85169cc15a7..000000000000 --- a/apps/meteor/app/models/server/models/LivechatInquiry.js +++ /dev/null @@ -1,304 +0,0 @@ -import { Base } from './_Base'; - -export class LivechatInquiry extends Base { - constructor() { - super('livechat_inquiry'); - - this.tryEnsureIndex({ rid: 1 }); // room id corresponding to this inquiry - this.tryEnsureIndex({ name: 1 }); // name of the inquiry (client name for now) - this.tryEnsureIndex({ message: 1 }); // message sent by the client - this.tryEnsureIndex({ ts: 1 }); // timestamp - this.tryEnsureIndex({ department: 1 }); - this.tryEnsureIndex({ status: 1 }); // 'ready', 'queued', 'taken' - this.tryEnsureIndex({ queueOrder: 1, estimatedWaitingTimeQueue: 1, estimatedServiceTimeAt: 1 }); - this.tryEnsureIndex({ 'v.token': 1, 'status': 1 }); // visitor token and status - } - - findOneById(inquiryId) { - return this.findOne({ _id: inquiryId }); - } - - findOneByRoomId(rid, options) { - return this.findOne({ rid }, options); - } - - getNextInquiryQueued(department) { - return this.findOne( - { - status: 'queued', - ...(department && { department }), - }, - { - sort: { - queueOrder: 1, - estimatedWaitingTimeQueue: 1, - estimatedServiceTimeAt: 1, - }, - }, - ); - } - - getQueuedInquiries(options) { - return this.find({ status: 'queued' }, options); - } - - /* - * mark the inquiry as taken - */ - takeInquiry(inquiryId) { - this.update( - { - _id: inquiryId, - }, - { - $set: { status: 'taken', takenAt: new Date() }, - $unset: { defaultAgent: 1, estimatedInactivityCloseTimeAt: 1 }, - }, - ); - } - - /* - * mark inquiry as open - */ - openInquiry(inquiryId) { - return this.update( - { - _id: inquiryId, - }, - { - $set: { status: 'open' }, - }, - ); - } - - /* - * mark inquiry as queued - */ - queueInquiry(inquiryId) { - return this.update( - { - _id: inquiryId, - }, - { - $set: { status: 'queued', queuedAt: new Date() }, - $unset: { takenAt: 1 }, - }, - ); - } - - queueInquiryAndRemoveDefaultAgent(inquiryId) { - return this.update( - { - _id: inquiryId, - }, - { - $set: { status: 'queued', queuedAt: new Date() }, - $unset: { takenAt: 1, defaultAgent: 1 }, - }, - ); - } - - /* - * mark inquiry as ready - */ - readyInquiry(inquiryId) { - return this.update( - { - _id: inquiryId, - }, - { - $set: { - status: 'ready', - }, - }, - ); - } - - changeDepartmentIdByRoomId(rid, department) { - const query = { - rid, - }; - const update = { - $set: { - department, - }, - }; - - this.update(query, update); - } - - /* - * return the status of the inquiry (open or taken) - */ - getStatus(inquiryId) { - return this.findOne({ _id: inquiryId }).status; - } - - updateVisitorStatus(token, status) { - const query = { - 'v.token': token, - 'status': 'queued', - }; - - const update = { - $set: { - 'v.status': status, - }, - }; - - return this.update(query, update); - } - - setDefaultAgentById(inquiryId, defaultAgent) { - return this.update( - { - _id: inquiryId, - }, - { - $set: { - defaultAgent, - }, - }, - ); - } - - setNameByRoomId(rid, name) { - const query = { rid }; - - const update = { - $set: { - name, - }, - }; - return this.update(query, update); - } - - findOneByToken(token) { - const query = { - 'v.token': token, - 'status': 'queued', - }; - return this.findOne(query); - } - - async getCurrentSortedQueueAsync({ _id, department }) { - const collectionObj = this.model.rawCollection(); - const aggregate = [ - { - $match: { - status: 'queued', - ...(department && { department }), - }, - }, - { - $sort: { - ts: 1, - }, - }, - { - $group: { - _id: 1, - inquiry: { - $push: { - _id: '$_id', - rid: '$rid', - name: '$name', - ts: '$ts', - status: '$status', - department: '$department', - }, - }, - }, - }, - { - $unwind: { - path: '$inquiry', - includeArrayIndex: 'position', - }, - }, - { - $project: { - _id: '$inquiry._id', - rid: '$inquiry.rid', - name: '$inquiry.name', - ts: '$inquiry.ts', - status: '$inquiry.status', - department: '$inquiry.department', - position: 1, - }, - }, - ]; - - // To get the current room position in the queue, we need to apply the next $match after the $project - if (_id) { - aggregate.push({ $match: { _id } }); - } - - return collectionObj.aggregate(aggregate).toArray(); - } - - removeDefaultAgentById(inquiryId) { - return this.update( - { - _id: inquiryId, - }, - { - $unset: { defaultAgent: 1 }, - }, - ); - } - - /* - * remove the inquiry by roomId - */ - removeByRoomId(rid) { - return this.remove({ rid }); - } - - removeByVisitorToken(token) { - const query = { - 'v.token': token, - }; - - this.remove(query); - } - - getUnnatendedQueueItems(date) { - const query = { - status: 'queued', - estimatedInactivityCloseTimeAt: { $lte: new Date(date) }, - }; - return this.find(query); - } - - setEstimatedInactivityCloseTime(_id, date) { - return this.update( - { _id }, - { - $set: { - estimatedInactivityCloseTimeAt: new Date(date), - }, - }, - ); - } - - // This is a better solution, but update pipelines are not supported until version 4.2 of mongo - // leaving this here for when the time comes - /* updateEstimatedInactivityCloseTime(milisecondsToAdd) { - return this.model.rawCollection().updateMany( - { status: 'queued' }, - [{ - // in case this field doesn't exists, set at the last time the item was modified (updatedAt) - $set: { estimatedInactivityCloseTimeAt: '$_updatedAt' }, - }, { - $set: { - estimatedInactivityCloseTimeAt: { - $add: ['$estimatedInactivityCloseTimeAt', milisecondsToAdd], - }, - }, - }], - ); - } */ -} - -export default new LivechatInquiry(); diff --git a/apps/meteor/app/models/server/models/LivechatInquiry.ts b/apps/meteor/app/models/server/models/LivechatInquiry.ts new file mode 100644 index 000000000000..49126d4fce00 --- /dev/null +++ b/apps/meteor/app/models/server/models/LivechatInquiry.ts @@ -0,0 +1,277 @@ +import { ILivechatInquiryRecord } from '@rocket.chat/core-typings'; +import { FindOptions, FindCursor, UpdateResult, DeleteResult } from 'mongodb'; + +import { Base } from './_Base'; + +export class LivechatInquiry extends Base { + constructor() { + super('livechat_inquiry'); + + this.tryEnsureIndex({ rid: 1 }); // room id corresponding to this inquiry + this.tryEnsureIndex({ name: 1 }); // name of the inquiry (client name for now) + this.tryEnsureIndex({ message: 1 }); // message sent by the client + this.tryEnsureIndex({ ts: 1 }); // timestamp + this.tryEnsureIndex({ department: 1 }); + this.tryEnsureIndex({ status: 1 }); // 'ready', 'queued', 'taken' + this.tryEnsureIndex({ queueOrder: 1, estimatedWaitingTimeQueue: 1, estimatedServiceTimeAt: 1 }); + this.tryEnsureIndex({ 'v.token': 1, 'status': 1 }); // visitor token and status + this.tryEnsureIndex({ locked: 1, lockedAt: 1 }, { sparse: true }); // locked and lockedAt + } + + findOneById(inquiryId: string): ILivechatInquiryRecord { + return this.findOne({ _id: inquiryId }); + } + + findOneByRoomId(rid: string, options?: FindOptions): ILivechatInquiryRecord { + return this.findOne({ rid }, options); + } + + getNextInquiryQueued(department?: string): ILivechatInquiryRecord { + return this.findOne( + { + status: 'queued', + ...(department && { department }), + }, + { + sort: { + queueOrder: 1, + estimatedWaitingTimeQueue: 1, + estimatedServiceTimeAt: 1, + }, + }, + ); + } + + getQueuedInquiries(options?: FindOptions): FindCursor { + return this.find({ status: 'queued' }, options); + } + + /* + * mark the inquiry as taken + */ + takeInquiry(inquiryId: string): void { + this.update( + { + _id: inquiryId, + }, + { + $set: { status: 'taken', takenAt: new Date() }, + $unset: { defaultAgent: 1, estimatedInactivityCloseTimeAt: 1 }, + }, + ); + } + + /* + * mark inquiry as open + */ + openInquiry(inquiryId: string): UpdateResult { + return this.update( + { + _id: inquiryId, + }, + { + $set: { status: 'open' }, + }, + ); + } + + /* + * mark inquiry as queued + */ + queueInquiry(inquiryId: string): UpdateResult { + return this.update( + { + _id: inquiryId, + }, + { + $set: { status: 'queued', queuedAt: new Date() }, + $unset: { takenAt: 1 }, + }, + ); + } + + queueInquiryAndRemoveDefaultAgent(inquiryId: string): UpdateResult { + return this.update( + { + _id: inquiryId, + }, + { + $set: { status: 'queued', queuedAt: new Date() }, + $unset: { takenAt: 1, defaultAgent: 1 }, + }, + ); + } + + /* + * mark inquiry as ready + */ + readyInquiry(inquiryId: string): UpdateResult { + return this.update( + { + _id: inquiryId, + }, + { + $set: { + status: 'ready', + }, + }, + ); + } + + changeDepartmentIdByRoomId(rid: string, department: string): void { + const query = { + rid, + }; + const updateObj = { + $set: { + department, + }, + }; + + this.update(query, updateObj); + } + + /* + * return the status of the inquiry (open or taken) + */ + getStatus(inquiryId: string): ILivechatInquiryRecord['status'] { + return this.findOne({ _id: inquiryId }).status; + } + + updateVisitorStatus(token: string, status: string): UpdateResult { + const query = { + 'v.token': token, + 'status': 'queued', + }; + + const update = { + $set: { + 'v.status': status, + }, + }; + + return this.update(query, update); + } + + setDefaultAgentById(inquiryId: string, defaultAgent: ILivechatInquiryRecord['defaultAgent']): UpdateResult { + return this.update( + { + _id: inquiryId, + }, + { + $set: { + defaultAgent, + }, + }, + ); + } + + setNameByRoomId(rid: string, name: string): UpdateResult { + const query = { rid }; + + const update = { + $set: { + name, + }, + }; + return this.update(query, update); + } + + findOneByToken(token: string): ILivechatInquiryRecord { + const query = { + 'v.token': token, + 'status': 'queued', + }; + return this.findOne(query); + } + + async getCurrentSortedQueueAsync({ + _id, + department, + }: { + _id: string; + department: string; + }): Promise & { position: number }> { + const collectionObj = this.model.rawCollection(); + const aggregate = [ + { + $match: { + status: 'queued', + ...(department && { department }), + }, + }, + { + $sort: { + ts: 1, + }, + }, + { + $group: { + _id: 1, + inquiry: { + $push: { + _id: '$_id', + rid: '$rid', + name: '$name', + ts: '$ts', + status: '$status', + department: '$department', + }, + }, + }, + }, + { + $unwind: { + path: '$inquiry', + includeArrayIndex: 'position', + }, + }, + { + $project: { + _id: '$inquiry._id', + rid: '$inquiry.rid', + name: '$inquiry.name', + ts: '$inquiry.ts', + status: '$inquiry.status', + department: '$inquiry.department', + position: 1, + }, + }, + ] as any[]; + + // To get the current room position in the queue, we need to apply the next $match after the $project + if (_id) { + aggregate.push({ $match: { _id } }); + } + + return collectionObj.aggregate(aggregate).toArray(); + } + + removeDefaultAgentById(inquiryId: string): UpdateResult { + return this.update( + { + _id: inquiryId, + }, + { + $unset: { defaultAgent: 1 }, + }, + ); + } + + /* + * remove the inquiry by roomId + */ + removeByRoomId(rid: string): DeleteResult { + return this.remove({ rid }); + } + + removeByVisitorToken(token: string): void { + const query = { + 'v.token': token, + }; + + this.remove(query); + } +} + +export default new LivechatInquiry(); diff --git a/apps/meteor/app/models/server/models/LivechatRooms.js b/apps/meteor/app/models/server/models/LivechatRooms.js index 4ce7aeb74ec2..25239366f589 100644 --- a/apps/meteor/app/models/server/models/LivechatRooms.js +++ b/apps/meteor/app/models/server/models/LivechatRooms.js @@ -1,9 +1,9 @@ import s from 'underscore.string'; import _ from 'underscore'; +import { Settings } from '@rocket.chat/models'; import { Base } from './_Base'; import Rooms from './Rooms'; -import Settings from './Settings'; export class LivechatRooms extends Base { constructor(...args) { @@ -23,11 +23,16 @@ export class LivechatRooms extends Base { this.tryEnsureIndex({ t: 1, departmentId: 1, closedAt: 1 }, { partialFilterExpression: { closedAt: { $exists: true } } }); this.tryEnsureIndex({ source: 1 }, { sparse: true }); this.tryEnsureIndex({ departmentAncestors: 1 }, { sparse: true }); - } - - findLivechat(filter = {}, offset = 0, limit = 20) { - const query = Object.assign(filter, { t: 'l' }); - return this.find(query, { sort: { ts: -1 }, offset, limit }); + this.tryEnsureIndex( + { 't': 1, 'open': 1, 'source.type': 1, 'v.status': 1 }, + { + partialFilterExpression: { + 't': { $eq: 'l' }, + 'open': { $eq: true }, + 'source.type': { $eq: 'widget' }, + }, + }, + ); } findOneByIdOrName(_idOrName, options) { @@ -248,7 +253,7 @@ export class LivechatRooms extends Base { return this.findOne(query, options); } - updateRoomCount = function () { + updateRoomCount = async function () { const query = { _id: 'Livechat_Room_Count', }; @@ -259,9 +264,8 @@ export class LivechatRooms extends Base { }, }; - const livechatCount = Settings.findAndModify(query, null, update); - - return livechatCount.value.value; + const livechatCount = await Settings.findOneAndUpdate(query, update, { returnDocument: 'after' }); + return livechatCount.value; }; findOpenByVisitorToken(visitorToken, options) { @@ -325,15 +329,6 @@ export class LivechatRooms extends Base { return this.find(query, options); } - findByVisitorId(visitorId) { - const query = { - 't': 'l', - 'v._id': visitorId, - }; - - return this.find(query); - } - findOneOpenByRoomIdAndVisitorToken(roomId, visitorToken, options) { const query = { 't': 'l', diff --git a/apps/meteor/app/models/server/models/LivechatVisitors.js b/apps/meteor/app/models/server/models/LivechatVisitors.js deleted file mode 100644 index 01642e36e84d..000000000000 --- a/apps/meteor/app/models/server/models/LivechatVisitors.js +++ /dev/null @@ -1,267 +0,0 @@ -import _ from 'underscore'; -import s from 'underscore.string'; - -import { Base } from './_Base'; -import Settings from './Settings'; - -export class LivechatVisitors extends Base { - constructor() { - super('livechat_visitor'); - - this.tryEnsureIndex({ token: 1 }); - this.tryEnsureIndex({ 'phone.phoneNumber': 1 }, { sparse: true }); - this.tryEnsureIndex({ 'visitorEmails.address': 1 }, { sparse: true }); - this.tryEnsureIndex({ name: 1 }, { sparse: true }); - this.tryEnsureIndex({ username: 1 }); - this.tryEnsureIndex({ 'contactManager.username': 1 }, { sparse: true }); - } - - /** - * Gets visitor by token - * @param {string} token - Visitor token - */ - getVisitorByToken(token, options) { - const query = { - token, - }; - - return this.findOne(query, options); - } - - /** - * Find visitors by _id - * @param {string} token - Visitor token - */ - findById(_id, options) { - const query = { - _id, - }; - - return this.find(query, options); - } - - /** - * Find One visitor by _id - */ - findOneById(_id, options) { - const query = { - _id, - }; - - return this.findOne(query, options); - } - - /** - * Gets visitor by token - * @param {string} token - Visitor token - */ - findVisitorByToken(token) { - const query = { - token, - }; - - return this.find(query); - } - - updateLivechatDataByToken(token, key, value, overwrite = true) { - const query = { - token, - }; - - if (!overwrite) { - const user = this.findOne(query, { fields: { livechatData: 1 } }); - if (user.livechatData && typeof user.livechatData[key] !== 'undefined') { - return true; - } - } - - const update = { - $set: { - [`livechatData.${key}`]: value, - }, - }; - - return this.update(query, update); - } - - updateLastAgentByToken(token, lastAgent) { - const query = { - token, - }; - - const update = { - $set: { - lastAgent, - }, - }; - - return this.update(query, update); - } - - /** - * Find a visitor by their phone number - * @return {object} User from db - */ - findOneVisitorByPhone(phone) { - const query = { - 'phone.phoneNumber': phone, - }; - - return this.findOne(query); - } - - getVisitorsBetweenDate(date) { - const query = { - _updatedAt: { - $gte: date.gte, // ISO Date, ts >= date.gte - $lt: date.lt, // ISODate, ts < date.lt - }, - }; - - return this.find(query, { fields: { _id: 1 } }); - } - - /** - * Get the next visitor name - * @return {string} The next visitor name - */ - getNextVisitorUsername() { - const query = { - _id: 'Livechat_guest_count', - }; - - const update = { - $inc: { - value: 1, - }, - }; - - const livechatCount = Settings.findAndModify(query, null, update); - - return `guest-${livechatCount.value.value + 1}`; - } - - updateById(_id, update) { - return this.update({ _id }, update); - } - - saveGuestById(_id, data) { - const setData = {}; - const unsetData = {}; - - if (data.name) { - if (!_.isEmpty(s.trim(data.name))) { - setData.name = s.trim(data.name); - } else { - unsetData.name = 1; - } - } - - if (data.email) { - if (!_.isEmpty(s.trim(data.email))) { - setData.visitorEmails = [{ address: s.trim(data.email) }]; - } else { - unsetData.visitorEmails = 1; - } - } - - if (data.phone) { - if (!_.isEmpty(s.trim(data.phone))) { - setData.phone = [{ phoneNumber: s.trim(data.phone) }]; - } else { - unsetData.phone = 1; - } - } - - if (data.livechatData) { - Object.keys(data.livechatData).forEach((key) => { - const value = s.trim(data.livechatData[key]); - if (value) { - setData[`livechatData.${key}`] = value; - } else { - unsetData[`livechatData.${key}`] = 1; - } - }); - } - - const update = {}; - - if (!_.isEmpty(setData)) { - update.$set = setData; - } - - if (!_.isEmpty(unsetData)) { - update.$unset = unsetData; - } - - if (_.isEmpty(update)) { - return true; - } - - return this.update({ _id }, update); - } - - findOneGuestByEmailAddress(emailAddress) { - const query = { - 'visitorEmails.address': String(emailAddress).toLowerCase(), - }; - - return this.findOne(query); - } - - saveGuestEmailPhoneById(_id, emails, phones) { - const update = { - $addToSet: {}, - }; - - const saveEmail = [] - .concat(emails) - .filter((email) => email && email.trim()) - .map((email) => ({ address: email })); - - if (saveEmail.length > 0) { - update.$addToSet.visitorEmails = { $each: saveEmail }; - } - - const savePhone = [] - .concat(phones) - .filter((phone) => phone && phone.trim().replace(/[^\d]/g, '')) - .map((phone) => ({ phoneNumber: phone })); - - if (savePhone.length > 0) { - update.$addToSet.phone = { $each: savePhone }; - } - - if (!update.$addToSet.visitorEmails && !update.$addToSet.phone) { - return; - } - - return this.update({ _id }, update); - } - - // REMOVE - removeDepartmentById(_id) { - return this.update({ _id }, { $unset: { department: 1 } }); - } - - removeById(_id) { - const query = { _id }; - return this.remove(query); - } - - removeContactManagerByUsername(manager) { - const query = { - contactManager: { - username: manager, - }, - }; - const update = { - $unset: { - contactManager: 1, - }, - }; - return this.update(query, update); - } -} - -export default new LivechatVisitors(); diff --git a/apps/meteor/app/models/server/models/Messages.js b/apps/meteor/app/models/server/models/Messages.js index ebb91426337e..813568a6234f 100644 --- a/apps/meteor/app/models/server/models/Messages.js +++ b/apps/meteor/app/models/server/models/Messages.js @@ -173,7 +173,7 @@ export class Messages extends Base { ); } - countVisibleByRoomIdBetweenTimestampsInclusive(roomId, afterTimestamp, beforeTimestamp, options) { + countVisibleByRoomIdBetweenTimestampsInclusive(roomId, afterTimestamp, beforeTimestamp) { const query = { _hidden: { $ne: true, @@ -185,7 +185,7 @@ export class Messages extends Base { }, }; - return this.find(query, options).count(); + return this.find(query).count(); } // FIND @@ -471,27 +471,6 @@ export class Messages extends Base { return this.find(query, options); } - findPinnedByRoom(roomId, options) { - const query = { - t: { $ne: 'rm' }, - _hidden: { $ne: true }, - pinned: true, - rid: roomId, - }; - - return this.find(query, options); - } - - findSnippetedByRoom(roomId, options) { - const query = { - _hidden: { $ne: true }, - snippeted: true, - rid: roomId, - }; - - return this.find(query, options); - } - getLastTimestamp(options = { fields: { _id: 0, ts: 1 } }) { options.sort = { ts: -1 }; options.limit = 1; @@ -534,19 +513,6 @@ export class Messages extends Base { return this.findOne(query, options); } - findByRoomIdAndType(roomId, type, options) { - const query = { - rid: roomId, - t: type, - }; - - if (options == null) { - options = {}; - } - - return this.find(query, options); - } - findByRoomId(roomId, options) { const query = { rid: roomId, diff --git a/apps/meteor/app/models/server/models/OmnichannelQueue.js b/apps/meteor/app/models/server/models/OmnichannelQueue.js deleted file mode 100644 index fb37a83af6d7..000000000000 --- a/apps/meteor/app/models/server/models/OmnichannelQueue.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Base } from './_Base'; - -export class OmnichannelQueue extends Base { - constructor() { - super('omnichannel_queue'); - } -} - -export default new OmnichannelQueue(); diff --git a/apps/meteor/app/models/server/models/Rooms.js b/apps/meteor/app/models/server/models/Rooms.js index 593fdab626da..d86fb37323c2 100644 --- a/apps/meteor/app/models/server/models/Rooms.js +++ b/apps/meteor/app/models/server/models/Rooms.js @@ -17,9 +17,6 @@ export class Rooms extends Base { this.tryEnsureIndex({ t: 1 }); this.tryEnsureIndex({ 'u._id': 1 }); this.tryEnsureIndex({ ts: 1 }); - // Tokenpass - this.tryEnsureIndex({ 'tokenpass.tokens.token': 1 }, { sparse: true }); - this.tryEnsureIndex({ tokenpass: 1 }, { sparse: true }); // discussions this.tryEnsureIndex({ prid: 1 }, { sparse: true }); this.tryEnsureIndex({ fname: 1 }, { sparse: true }); @@ -27,6 +24,8 @@ export class Rooms extends Base { this.tryEnsureIndex({ uids: 1 }, { sparse: true }); this.tryEnsureIndex({ createdOTR: 1 }, { sparse: true }); this.tryEnsureIndex({ encrypted: 1 }, { sparse: true }); // used on statistics + this.tryEnsureIndex({ broadcast: 1 }, { sparse: true }); // used on statistics + this.tryEnsureIndex({ 'streamingOptions.type': 1 }, { sparse: true }); // used on statistics this.tryEnsureIndex( { @@ -52,20 +51,6 @@ export class Rooms extends Base { return this.findOne(query, options); } - setJitsiTimeout(_id, time) { - const query = { - _id, - }; - - const update = { - $set: { - jitsiTimeout: time, - }, - }; - - return this.update(query, update); - } - setCallStatus(_id, status) { const query = { _id, @@ -95,38 +80,6 @@ export class Rooms extends Base { return this.update(query, update); } - findByTokenpass(tokens) { - const query = { - 'tokenpass.tokens.token': { - $in: tokens, - }, - }; - - return this._db.find(query).fetch(); - } - - setTokensById(_id, tokens) { - const update = { - $set: { - 'tokenpass.tokens.token': tokens, - }, - }; - - return this.update({ _id }, update); - } - - findAllTokenChannels() { - const query = { - tokenpass: { $exists: true }, - }; - const options = { - fields: { - tokenpass: 1, - }, - }; - return this._db.find(query, options); - } - setReactionsInLastMessage(roomId, lastMessage) { return this.update({ _id: roomId }, { $set: { 'lastMessage.reactions': lastMessage.reactions } }); } @@ -242,16 +195,6 @@ export class Rooms extends Base { return this.update({ _id }, update); } - setTokenpassById(_id, tokenpass) { - const update = { - $set: { - tokenpass, - }, - }; - - return this.update({ _id }, update); - } - setReadOnlyById(_id, readOnly) { const query = { _id, @@ -374,7 +317,7 @@ export class Rooms extends Base { let channelName = s.trim(name); try { // TODO evaluate if this function call should be here - const { getValidRoomName } = Promise.await(import('../../../utils/lib/getValidRoomName')); + const { getValidRoomName } = Promise.await(import('../../../utils/server/lib/getValidRoomName')); channelName = getValidRoomName(channelName, null, { allowDuplicates: true }); } catch (e) { console.error(e); @@ -443,16 +386,6 @@ export class Rooms extends Base { return this.find(query, options); } - findByTypes(types, discussion = false, options = {}) { - const query = { - t: { - $in: types, - }, - prid: { $exists: discussion }, - }; - return this.find(query, options); - } - findByUserId(userId, options) { const query = { 'u._id': userId }; @@ -488,23 +421,6 @@ export class Rooms extends Base { return this.find(query, options); } - findBySubscriptionTypeAndUserId(type, userId, options) { - const data = Subscriptions.findByUserIdAndType(userId, type, { - fields: { rid: 1 }, - }) - .fetch() - .map((item) => item.rid); - - const query = { - t: type, - _id: { - $in: data, - }, - }; - - return this.find(query, options); - } - findBySubscriptionUserIdUpdatedAfter(userId, _updatedAt, options) { const ids = Subscriptions.findByUserId(userId, { fields: { rid: 1 } }) .fetch() @@ -537,41 +453,6 @@ export class Rooms extends Base { return this.find(query, options); } - findByNameContaining(name, discussion = false, options = {}) { - const nameRegex = new RegExp(s.trim(escapeRegExp(name)), 'i'); - - const query = { - prid: { $exists: discussion }, - $or: [ - { name: nameRegex }, - { - t: 'd', - usernames: nameRegex, - }, - ], - }; - return this.find(query, options); - } - - findByNameContainingAndTypes(name, types, discussion = false, options = {}) { - const nameRegex = new RegExp(s.trim(escapeRegExp(name)), 'i'); - - const query = { - t: { - $in: types, - }, - prid: { $exists: discussion }, - $or: [ - { name: nameRegex }, - { - t: 'd', - usernames: nameRegex, - }, - ], - }; - return this.find(query, options); - } - findByNameAndType(name, type, options) { const query = { t: type, @@ -602,92 +483,6 @@ export class Rooms extends Base { return this._db.find(query, options); } - findByNameOrFNameAndRoomIdsIncludingTeamRooms(text, teamIds, roomIds, options) { - const searchTerm = text && new RegExp(text, 'i'); - - const query = { - $and: [ - { teamMain: { $exists: false } }, - { prid: { $exists: false } }, - { - $or: [ - { - t: 'c', - teamId: { $exists: false }, - }, - { - t: 'c', - teamId: { $in: teamIds }, - }, - ...(roomIds?.length > 0 - ? [ - { - _id: { - $in: roomIds, - }, - }, - ] - : []), - ], - }, - ...(searchTerm - ? [ - { - $or: [ - { - name: searchTerm, - }, - { - fname: searchTerm, - }, - ], - }, - ] - : []), - ], - }; - - return this._db.find(query, options); - } - - findContainingNameOrFNameInIdsAsTeamMain(text, rids, options) { - const query = { - teamMain: true, - $and: [ - { - $or: [ - { - t: 'p', - _id: { - $in: rids, - }, - }, - { - t: 'c', - }, - ], - }, - ], - }; - - if (text) { - const regex = new RegExp(text, 'i'); - - query.$and.push({ - $or: [ - { - name: regex, - }, - { - fname: regex, - }, - ], - }); - } - - return this._db.find(query, options); - } - findByNameAndTypeNotDefault(name, type, options) { const query = { t: type, @@ -1047,6 +842,11 @@ export class Rooms extends Base { return this.update(query, update); } + /** + * @param {string} _id + * @param {string?} messageId + * @returns {Promise} + */ resetLastMessageById(_id, messageId = undefined) { const query = { _id }; const lastMessage = Messages.getLastVisibleMessageSentWithNoTypeByRoomId(_id, messageId); diff --git a/apps/meteor/app/models/server/models/Settings.js b/apps/meteor/app/models/server/models/Settings.js index c2ef98d35476..490b4b2822eb 100644 --- a/apps/meteor/app/models/server/models/Settings.js +++ b/apps/meteor/app/models/server/models/Settings.js @@ -1,5 +1,3 @@ -import { Meteor } from 'meteor/meteor'; - import { Base } from './_Base'; export class Settings extends Base { @@ -8,9 +6,6 @@ export class Settings extends Base { this.tryEnsureIndex({ blocked: 1 }, { sparse: 1 }); this.tryEnsureIndex({ hidden: 1 }, { sparse: 1 }); - - const collectionObj = this.model.rawCollection(); - this.findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj); } // FIND diff --git a/apps/meteor/app/models/server/models/Users.js b/apps/meteor/app/models/server/models/Users.js index 3e8eb705d3d3..725f90a0be2f 100644 --- a/apps/meteor/app/models/server/models/Users.js +++ b/apps/meteor/app/models/server/models/Users.js @@ -59,12 +59,8 @@ export class Users extends Base { this.tryEnsureIndex({ statusLivechat: 1 }, { sparse: true }); this.tryEnsureIndex({ extension: 1 }, { sparse: true, unique: true }); this.tryEnsureIndex({ language: 1 }, { sparse: true }); - this.tryEnsureIndex({ 'active': 1, 'services.email2fa.enabled': 1 }, { sparse: true }); // used by statistics this.tryEnsureIndex({ 'active': 1, 'services.totp.enabled': 1 }, { sparse: true }); // used by statistics - - const collectionObj = this.model.rawCollection(); - this.findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj); } getLoginTokensByUserId(userId) { @@ -206,10 +202,10 @@ export class Users extends Base { return this.find(query); } - getNextAgent(ignoreAgentId, extraQuery) { + async getNextAgent(ignoreAgentId, extraQuery) { // TODO: Create class Agent // fetch all unavailable agents, and exclude them from the selection - const unavailableAgents = Promise.await(this.getUnavailableAgents(null, extraQuery)).map((u) => u.username); + const unavailableAgents = (await this.getUnavailableAgents(null, extraQuery)).map((u) => u.username); const extraFilters = { ...(ignoreAgentId && { _id: { $ne: ignoreAgentId } }), // limit query to remove booked agents @@ -229,7 +225,7 @@ export class Users extends Base { }, }; - const user = this.findAndModify(query, sort, update); + const user = await this.model.rawCollection().findOneAndUpdate(query, update, { sort, returnDocument: 'after' }); if (user && user.value) { return { agentId: user.value._id, @@ -243,7 +239,7 @@ export class Users extends Base { return []; } - getNextBotAgent(ignoreAgentId) { + async getNextBotAgent(ignoreAgentId) { // TODO: Create class Agent const query = { roles: { @@ -263,7 +259,7 @@ export class Users extends Base { }, }; - const user = this.findAndModify(query, sort, update); + const user = await this.model.rawCollection().findOneAndUpdate(query, update, { sort, returnDocument: 'after' }); if (user && user.value) { return { agentId: user.value._id, @@ -338,30 +334,6 @@ export class Users extends Base { return this.findOne(query, options); } - setTokenpassTcaBalances(_id, tcaBalances) { - const update = { - $set: { - 'services.tokenpass.tcaBalances': tcaBalances, - }, - }; - - return this.update(_id, update); - } - - getTokenBalancesByUserId(userId) { - const query = { - _id: userId, - }; - - const options = { - fields: { - 'services.tokenpass.tcaBalances': 1, - }, - }; - - return this.findOne(query, options); - } - roleBaseQuery(userId) { return { _id: userId }; } @@ -670,7 +642,7 @@ export class Users extends Base { return this.find(query, options); } - findOneByAppId(appId, options) { + findOneByAppId(appId, options = {}) { const query = { appId }; return this.findOne(query, options); @@ -900,81 +872,6 @@ export class Users extends Base { return this.find(query, options); } - findByActiveUsersExcept( - searchTerm, - exceptions, - options, - forcedSearchFields, - extraQuery = [], - { startsWith = false, endsWith = false } = {}, - ) { - if (exceptions == null) { - exceptions = []; - } - if (options == null) { - options = {}; - } - if (!_.isArray(exceptions)) { - exceptions = [exceptions]; - } - - // if the search term is empty, don't need to have the $or statement (because it would be an empty regex) - if (!searchTerm) { - const query = { - $and: [ - { - active: true, - username: { $exists: true, $nin: exceptions }, - }, - ...extraQuery, - ], - }; - - return this._db.find(query, options); - } - - const termRegex = new RegExp((startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''), 'i'); - - const searchFields = forcedSearchFields || settings.get('Accounts_SearchFields').trim().split(','); - - const orStmt = _.reduce( - searchFields, - function (acc, el) { - acc.push({ [el.trim()]: termRegex }); - return acc; - }, - [], - ); - - const query = { - $and: [ - { - active: true, - username: { $exists: true, $nin: exceptions }, - $or: orStmt, - }, - ...extraQuery, - ], - }; - - // do not use cache - return this._db.find(query, options); - } - - findByActiveLocalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) { - const extraQuery = [ - { - $or: [{ federation: { $exists: false } }, { 'federation.origin': localDomain }], - }, - ]; - return this.findByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); - } - - findByActiveExternalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) { - const extraQuery = [{ federation: { $exists: true } }, { 'federation.origin': { $ne: localDomain } }]; - return this.findByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); - } - findUsersByNameOrUsername(nameOrUsername, options) { const query = { username: { @@ -1068,7 +965,7 @@ export class Users extends Base { } /** - * @param {import('mongodb').FilterQuery} fields + * @param {import('mongodb').Filter} fields */ getOldest(fields = { _id: 1 }) { const query = { diff --git a/apps/meteor/app/models/server/models/_BaseDb.js b/apps/meteor/app/models/server/models/_BaseDb.js index 64bc40d081f8..b984ad02f2f9 100644 --- a/apps/meteor/app/models/server/models/_BaseDb.js +++ b/apps/meteor/app/models/server/models/_BaseDb.js @@ -7,21 +7,11 @@ import _ from 'underscore'; import { setUpdatedAt } from '../lib/setUpdatedAt'; import { metrics } from '../../../metrics/server/lib/metrics'; import { getOplogHandle } from './_oplogHandle'; -import { SystemLogger } from '../../../../server/lib/logger/system'; import { isRunningMs } from '../../../../server/lib/isRunningMs'; +import { trash } from '../../../../server/database/trash'; const baseName = 'rocketchat_'; -export const trash = new Mongo.Collection(`${baseName}_trash`); -try { - trash._ensureIndex({ __collection__: 1 }); - trash._ensureIndex({ _deletedAt: 1 }, { expireAfterSeconds: 60 * 60 * 24 * 30 }); - - trash._ensureIndex({ rid: 1, __collection__: 1, _deletedAt: 1 }); -} catch (e) { - SystemLogger.error(e); -} - const actions = { i: 'insert', u: 'update', diff --git a/apps/meteor/app/models/server/models/_oplogHandle.ts b/apps/meteor/app/models/server/models/_oplogHandle.ts index 98f0e5608329..a3c83187d3c2 100644 --- a/apps/meteor/app/models/server/models/_oplogHandle.ts +++ b/apps/meteor/app/models/server/models/_oplogHandle.ts @@ -1,25 +1,29 @@ +import { Readable } from 'stream'; + import { Meteor } from 'meteor/meteor'; import { MongoInternals, OplogHandle } from 'meteor/mongo'; import semver from 'semver'; -import { MongoClient, Cursor, Timestamp, Db } from 'mongodb'; +import { MongoClient } from 'mongodb'; +import type { Timestamp, Db } from 'mongodb'; import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { urlParser } from './_oplogUrlParser'; import { isRunningMs } from '../../../../server/lib/isRunningMs'; +const ignoreChangeStream = ['yes', 'true'].includes(String(process.env.IGNORE_CHANGE_STREAM).toLowerCase()); + class CustomOplogHandle { dbName: string; client: MongoClient; - stream: Cursor; + stream: Readable; db: Db; usingChangeStream: boolean; async isChangeStreamAvailable(): Promise { - if (process.env.IGNORE_CHANGE_STREAM) { + if (ignoreChangeStream) { return false; } @@ -34,7 +38,7 @@ class CustomOplogHandle { await mongo.db.admin().command({ replSetGetStatus: 1 }); } catch (e) { - if (e.message.startsWith('not authorized')) { + if (e instanceof Error && e.message.startsWith('not authorized')) { console.info( 'Change Stream is available for your installation, give admin permissions to your database user to use this improved version.', ); @@ -47,37 +51,26 @@ class CustomOplogHandle { async start(): Promise { this.usingChangeStream = await this.isChangeStreamAvailable(); - const oplogUrl = this.usingChangeStream ? process.env.MONGO_URL : process.env.MONGO_OPLOG_URL; - let urlParsed; - try { - urlParsed = await urlParser(oplogUrl); - } catch (e) { - throw Error(`Error parsing database URL (${oplogUrl})`); - } - - if (!this.usingChangeStream && (!oplogUrl || urlParsed.defaultDatabase !== 'local')) { - throw Error("$MONGO_OPLOG_URL must be set to the 'local' database of a Mongo replica set"); - } - - if (!oplogUrl) { + const oplogUrl = this.usingChangeStream ? process.env.MONGO_URL : process.env.MONGO_OPLOG_URL; + if (!oplogUrl || !process.env.MONGO_URL) { throw Error('$MONGO_URL must be set'); } - if (process.env.MONGO_OPLOG_URL) { - const urlParsed = await urlParser(process.env.MONGO_URL); - this.dbName = urlParsed.defaultDatabase; - } - const mongoOptions = process.env.MONGO_OPTIONS ? JSON.parse(process.env.MONGO_OPTIONS) : null; this.client = new MongoClient(oplogUrl, { ...mongoOptions, - useUnifiedTopology: true, - useNewUrlParser: true, - poolSize: this.usingChangeStream ? 15 : 1, + minPoolSize: this.usingChangeStream ? 15 : 1, }); + if (!this.usingChangeStream && this.client.options.dbName !== 'local') { + throw Error("$MONGO_OPLOG_URL must be set to the 'local' database of a Mongo replica set"); + } + + const mongoClient = new MongoClient(process.env.MONGO_URL); + this.dbName = mongoClient.options.dbName; + await this.client.connect(); this.db = this.client.db(); @@ -129,7 +122,7 @@ class CustomOplogHandle { } _onOplogEntryOplog(query: { collection: string }, callback: Function): void { - this.stream.on( + this.stream?.on( 'data', Meteor.bindEnvironment((buffer) => { const doc = buffer as any; @@ -170,7 +163,7 @@ class CustomOplogHandle { // o: event.fullDocument, o: { $set: event.updateDescription.updatedFields, - $unset: event.updateDescription.removedFields.reduce((obj, field) => { + $unset: event.updateDescription.removedFields?.reduce((obj, field) => { obj[field as string] = true; return obj; }, {} as Record), @@ -214,7 +207,7 @@ if (!isRunningMs()) { try { oplogHandle = Promise.await(new CustomOplogHandle().start()); } catch (e) { - console.error(e.message); + console.error(e instanceof Error ? e.message : e); } } } diff --git a/apps/meteor/app/models/server/models/_oplogUrlParser.js b/apps/meteor/app/models/server/models/_oplogUrlParser.js deleted file mode 100644 index 986874cefccd..000000000000 --- a/apps/meteor/app/models/server/models/_oplogUrlParser.js +++ /dev/null @@ -1,5 +0,0 @@ -import { promisify } from 'util'; - -import { parseConnectionString } from 'mongodb/lib/core'; - -export const urlParser = promisify(parseConnectionString); diff --git a/apps/meteor/app/models/server/raw/Analytics.ts b/apps/meteor/app/models/server/raw/Analytics.ts deleted file mode 100644 index bde3461b020f..000000000000 --- a/apps/meteor/app/models/server/raw/Analytics.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { Random } from 'meteor/random'; -import { AggregationCursor, Cursor, SortOptionObject, UpdateWriteOpResult } from 'mongodb'; -import type { IAnalytic, IRoom } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -type T = IAnalytic; - -export class AnalyticsRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { date: 1 } }, { key: { 'room._id': 1, 'date': 1 }, unique: true }]; - } - - saveMessageSent({ room, date }: { room: IRoom; date: IAnalytic['date'] }): Promise { - return this.updateMany( - { date, 'room._id': room._id, 'type': 'messages' }, - { - $set: { - room: { - _id: room._id, - name: room.fname || room.name, - t: room.t, - usernames: room.usernames || [], - }, - }, - $setOnInsert: { - _id: Random.id(), - date, - type: 'messages', - }, - $inc: { messages: 1 }, - }, - { upsert: true }, - ); - } - - saveUserData({ date }: { date: IAnalytic['date'] }): Promise { - return this.updateMany( - { date, type: 'users' }, - { - $setOnInsert: { - _id: Random.id(), - date, - type: 'users', - }, - $inc: { users: 1 }, - }, - { upsert: true }, - ); - } - - saveMessageDeleted({ room, date }: { room: { _id: string }; date: IAnalytic['date'] }): Promise { - return this.updateMany( - { date, 'room._id': room._id }, - { - $inc: { messages: -1 }, - }, - ); - } - - getMessagesSentTotalByDate({ - start, - end, - options = {}, - }: { - start: IAnalytic['date']; - end: IAnalytic['date']; - options?: { sort?: SortOptionObject; count?: number }; - }): AggregationCursor<{ - _id: IAnalytic['date']; - messages: number; - }> { - return this.col.aggregate<{ - _id: IAnalytic['date']; - messages: number; - }>([ - { - $match: { - type: 'messages', - date: { $gte: start, $lte: end }, - }, - }, - { - $group: { - _id: '$date', - messages: { $sum: '$messages' }, - }, - }, - ...(options.sort ? [{ $sort: options.sort }] : []), - ...(options.count ? [{ $limit: options.count }] : []), - ]); - } - - getMessagesOrigin({ start, end }: { start: IAnalytic['date']; end: IAnalytic['date'] }): AggregationCursor<{ - t: IRoom['t']; - messages: number; - }> { - const params = [ - { - $match: { - type: 'messages', - date: { $gte: start, $lte: end }, - }, - }, - { - $group: { - _id: { t: '$room.t' }, - messages: { $sum: '$messages' }, - }, - }, - { - $project: { - _id: 0, - t: '$_id.t', - messages: 1, - }, - }, - ]; - return this.col.aggregate(params); - } - - getMostPopularChannelsByMessagesSentQuantity({ - start, - end, - options = {}, - }: { - start: IAnalytic['date']; - end: IAnalytic['date']; - options?: { sort?: SortOptionObject; count?: number }; - }): AggregationCursor<{ - t: IRoom['t']; - name: string; - messages: number; - usernames: string[]; - }> { - return this.col.aggregate([ - { - $match: { - type: 'messages', - date: { $gte: start, $lte: end }, - }, - }, - { - $group: { - _id: { t: '$room.t', name: '$room.name', usernames: '$room.usernames' }, - messages: { $sum: '$messages' }, - }, - }, - { - $project: { - _id: 0, - t: '$_id.t', - name: '$_id.name', - usernames: '$_id.usernames', - messages: 1, - }, - }, - ...(options.sort ? [{ $sort: options.sort }] : []), - ...(options.count ? [{ $limit: options.count }] : []), - ]); - } - - getTotalOfRegisteredUsersByDate({ - start, - end, - options = {}, - }: { - start: IAnalytic['date']; - end: IAnalytic['date']; - options?: { sort?: SortOptionObject; count?: number }; - }): AggregationCursor<{ - _id: IAnalytic['date']; - users: number; - }> { - return this.col.aggregate<{ - _id: IAnalytic['date']; - users: number; - }>([ - { - $match: { - type: 'users', - date: { $gte: start, $lte: end }, - }, - }, - { - $group: { - _id: '$date', - users: { $sum: '$users' }, - }, - }, - ...(options.sort ? [{ $sort: options.sort }] : []), - ...(options.count ? [{ $limit: options.count }] : []), - ]); - } - - findByTypeBeforeDate({ type, date }: { type: T['type']; date: T['date'] }): Cursor { - return this.find({ type, date: { $lte: date } }); - } -} diff --git a/apps/meteor/app/models/server/raw/Avatars.ts b/apps/meteor/app/models/server/raw/Avatars.ts deleted file mode 100644 index ba1af66f7ef7..000000000000 --- a/apps/meteor/app/models/server/raw/Avatars.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { DeleteWriteOpResultObject, UpdateWriteOpResult } from 'mongodb'; -import { IAvatar as T } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class AvatarsRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [ - { key: { name: 1 }, sparse: true }, - { key: { rid: 1 }, sparse: true }, - ]; - } - - insertAvatarFileInit(name: string, userId: string, store: string, file: { name: string }, extra: object): Promise { - const fileData = { - name, - userId, - store, - complete: false, - uploading: true, - progress: 0, - extension: file.name.split('.').pop(), - uploadedAt: new Date(), - }; - - Object.assign(fileData, file, extra); - - return this.updateOne({ _id: name }, fileData, { upsert: true }); - } - - updateFileComplete(fileId: string, userId: string, file: object): Promise | undefined { - if (!fileId) { - return; - } - - const filter = { - _id: fileId, - userId, - }; - - const update = { - $set: { - complete: true, - uploading: false, - progress: 1, - }, - }; - - update.$set = Object.assign(file, update.$set); - - return this.updateOne(filter, update); - } - - async findOneByName(name: string): Promise { - return this.findOne({ name }); - } - - async findOneByRoomId(rid: string): Promise { - return this.findOne({ rid }); - } - - async updateFileNameById(fileId: string, name: string): Promise { - const filter = { _id: fileId }; - const update = { - $set: { - name, - }, - }; - return this.updateOne(filter, update); - } - - async deleteFile(fileId: string): Promise { - return this.deleteOne({ _id: fileId }); - } -} diff --git a/apps/meteor/app/models/server/raw/Banners.ts b/apps/meteor/app/models/server/raw/Banners.ts deleted file mode 100644 index 04a90fb8e2e6..000000000000 --- a/apps/meteor/app/models/server/raw/Banners.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Cursor, FindOneOptions, UpdateWriteOpResult, WithoutProjection, InsertOneWriteOpResult } from 'mongodb'; -import { BannerPlatform, IBanner } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -type T = IBanner; -export class BannersRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { platform: 1, startAt: 1, expireAt: 1 } }, { key: { platform: 1, startAt: 1, expireAt: 1, active: 1 } }]; - } - - create(doc: IBanner): Promise> { - const invalidPlatform = doc.platform?.some((platform) => !Object.values(BannerPlatform).includes(platform)); - if (invalidPlatform) { - throw new Error('Invalid platform'); - } - - if (doc.startAt > doc.expireAt) { - throw new Error('Start date cannot be later than expire date'); - } - - if (doc.expireAt < new Date()) { - throw new Error('Cannot create banner already expired'); - } - - return this.insertOne({ - active: true, - ...doc, - }); - } - - findActiveByRoleOrId( - roles: string[], - platform: BannerPlatform, - bannerId?: string, - options?: WithoutProjection>, - ): Cursor { - const today = new Date(); - - const query = { - ...(bannerId && { _id: bannerId }), - platform, - startAt: { $lte: today }, - expireAt: { $gte: today }, - active: { $ne: false }, - $or: [{ roles: { $in: roles } }, { roles: { $exists: false } }], - }; - - return this.col.find(query, options); - } - - disable(bannerId: string): Promise { - return this.col.updateOne({ _id: bannerId, active: { $ne: false } }, { $set: { active: false, inactivedAt: new Date() } }); - } -} diff --git a/apps/meteor/app/models/server/raw/BannersDismiss.ts b/apps/meteor/app/models/server/raw/BannersDismiss.ts deleted file mode 100644 index 98ac83a76f3b..000000000000 --- a/apps/meteor/app/models/server/raw/BannersDismiss.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Cursor, FindOneOptions, WithoutProjection } from 'mongodb'; -import type { IBannerDismiss } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class BannersDismissRaw extends BaseRaw { - modelIndexes(): IndexSpecification[] { - return [{ key: { userId: 1, bannerId: 1 } }]; - } - - findByUserIdAndBannerId(userId: string, bannerIds: string[]): Cursor; - - findByUserIdAndBannerId( - userId: string, - bannerIds: string[], - options: WithoutProjection>, - ): Cursor; - - findByUserIdAndBannerId

( - userId: string, - bannerIds: string[], - options: FindOneOptions

, - ): Cursor

; - - findByUserIdAndBannerId

( - userId: string, - bannerIds: string[], - options?: undefined | WithoutProjection> | FindOneOptions

, - ): Cursor

| Cursor { - const query = { - userId, - bannerId: { $in: bannerIds }, - }; - - return options ? this.col.find(query, options) : this.col.find(query); - } -} diff --git a/apps/meteor/app/models/server/raw/BaseRaw.ts b/apps/meteor/app/models/server/raw/BaseRaw.ts deleted file mode 100644 index a1121f34cdb9..000000000000 --- a/apps/meteor/app/models/server/raw/BaseRaw.ts +++ /dev/null @@ -1,405 +0,0 @@ -import { - Collection, - CollectionInsertOneOptions, - CommonOptions, - Cursor, - DeleteWriteOpResultObject, - FilterQuery, - FindAndModifyWriteOpResultObject, - FindOneAndUpdateOption, - FindOneOptions, - IndexSpecification, - InsertOneWriteOpResult, - InsertWriteOpResult, - ObjectID, - ObjectId, - OptionalId, - UpdateManyOptions, - UpdateOneOptions, - UpdateQuery, - UpdateWriteOpResult, - WithId, - WithoutProjection, - WriteOpResult, -} from 'mongodb'; -import { IRocketChatRecord, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; - -import { setUpdatedAt } from '../lib/setUpdatedAt'; - -export { IndexSpecification } from 'mongodb'; - -// [extracted from @types/mongo] TypeScript Omit (Exclude to be specific) does not work for objects with an "any" indexed type, and breaks discriminated unions -type EnhancedOmit = string | number extends keyof T - ? T // T has indexed type e.g. { _id: string; [k: string]: any; } or it is "any" - : T extends any - ? Pick> // discriminated unions - : never; - -// [extracted from @types/mongo] -type ExtractIdType = TSchema extends { _id: infer U } // user has defined a type for _id - ? {} extends U - ? Exclude - : unknown extends U - ? ObjectId - : U - : ObjectId; - -export type ModelOptionalId = EnhancedOmit & { _id?: ExtractIdType }; -// InsertionModel forces both _id and _updatedAt to be optional, regardless of how they are declared in T -export type InsertionModel = EnhancedOmit, '_updatedAt'> & { - _updatedAt?: Date; -}; - -export interface IBaseRaw { - col: Collection; -} - -const baseName = 'rocketchat_'; - -type DefaultFields = Record | Record | void; -type ResultFields = Defaults extends void - ? Base - : Defaults[keyof Defaults] extends 1 - ? Pick - : Omit; - -const warnFields = - process.env.NODE_ENV !== 'production' - ? (...rest: any): void => { - console.warn(...rest, new Error().stack); - } - : new Function(); - -export class BaseRaw = undefined> implements IBaseRaw { - public readonly defaultFields: C; - - protected name: string; - - private preventSetUpdatedAt: boolean; - - public readonly trash?: Collection>; - - constructor(public readonly col: Collection, trash?: Collection, options?: { preventSetUpdatedAt?: boolean }) { - this.name = this.col.collectionName.replace(baseName, ''); - this.trash = trash as unknown as Collection>; - - const indexes = this.modelIndexes(); - if (indexes?.length) { - this.col.createIndexes(indexes).catch((e) => { - console.warn(`Error creating indexes for ${this.name}`, e); - }); - } - - this.preventSetUpdatedAt = options?.preventSetUpdatedAt ?? false; - } - - protected modelIndexes(): IndexSpecification[] | void { - // noop - } - - private doNotMixInclusionAndExclusionFields(options: FindOneOptions = {}): FindOneOptions { - const optionsDef = this.ensureDefaultFields(options); - if (optionsDef?.projection === undefined) { - return optionsDef; - } - - const projection: Record = optionsDef?.projection; - const keys = Object.keys(projection); - const removeKeys = keys.filter((key) => projection[key] === 0); - if (keys.length > removeKeys.length) { - removeKeys.forEach((key) => delete projection[key]); - } - - return { - ...optionsDef, - projection, - }; - } - - private ensureDefaultFields(options?: undefined): C extends void ? undefined : WithoutProjection>; - - private ensureDefaultFields(options: WithoutProjection>): WithoutProjection>; - - private ensureDefaultFields

(options: FindOneOptions

): FindOneOptions

; - - private ensureDefaultFields

(options?: any): FindOneOptions

| undefined | WithoutProjection> { - if (this.defaultFields === undefined) { - return options; - } - - const { fields: deprecatedFields, projection, ...rest } = options || {}; - - if (deprecatedFields) { - warnFields("Using 'fields' in models is deprecated.", options); - } - - const fields = { ...deprecatedFields, ...projection }; - - return { - projection: this.defaultFields, - ...(fields && Object.values(fields).length && { projection: fields }), - ...rest, - }; - } - - public findOneAndUpdate( - query: FilterQuery, - update: UpdateQuery | T, - options?: FindOneAndUpdateOption, - ): Promise> { - return this.col.findOneAndUpdate(query, update, options); - } - - async findOneById(_id: string, options?: WithoutProjection> | undefined): Promise; - - async findOneById

(_id: string, options: FindOneOptions

): Promise

; - - async findOneById

(_id: string, options?: any): Promise { - const query = { _id } as FilterQuery; - const optionsDef = this.doNotMixInclusionAndExclusionFields(options); - return this.col.findOne(query, optionsDef); - } - - async findOne(query?: FilterQuery | string, options?: undefined): Promise; - - async findOne(query: FilterQuery | string, options: WithoutProjection>): Promise; - - async findOne

(query: FilterQuery | string, options: FindOneOptions

): Promise

; - - async findOne

(query: FilterQuery | string = {}, options?: any): Promise { - const q = typeof query === 'string' ? ({ _id: query } as FilterQuery) : query; - - const optionsDef = this.doNotMixInclusionAndExclusionFields(options); - return this.col.findOne(q, optionsDef); - } - - // findUsersInRoles(): void { - // throw new Error('[overwrite-function] You must overwrite this function in the extended classes'); - // } - - find(query?: FilterQuery): Cursor>; - - find(query: FilterQuery, options: WithoutProjection>): Cursor>; - - find

(query: FilterQuery, options: FindOneOptions

): Cursor

; - - find

(query: FilterQuery | undefined = {}, options?: any): Cursor

| Cursor { - const optionsDef = this.doNotMixInclusionAndExclusionFields(options); - return this.col.find(query, optionsDef); - } - - update( - filter: FilterQuery, - update: UpdateQuery | Partial, - options?: UpdateOneOptions & { multi?: boolean }, - ): Promise { - this.setUpdatedAt(update); - return this.col.update(filter, update, options); - } - - updateOne( - filter: FilterQuery, - update: UpdateQuery | Partial, - options?: UpdateOneOptions & { multi?: boolean }, - ): Promise { - this.setUpdatedAt(update); - return this.col.updateOne(filter, update, options); - } - - updateMany(filter: FilterQuery, update: UpdateQuery | Partial, options?: UpdateManyOptions): Promise { - this.setUpdatedAt(update); - return this.col.updateMany(filter, update, options); - } - - insertMany(docs: Array>, options?: CollectionInsertOneOptions): Promise>> { - docs = docs.map((doc) => { - if (!doc._id || typeof doc._id !== 'string') { - const oid = new ObjectID(); - return { _id: oid.toHexString(), ...doc }; - } - this.setUpdatedAt(doc); - return doc; - }); - - // TODO reavaluate following type casting - return this.col.insertMany(docs as unknown as Array>, options); - } - - insertOne(doc: InsertionModel, options?: CollectionInsertOneOptions): Promise>> { - if (!doc._id || typeof doc._id !== 'string') { - const oid = new ObjectID(); - doc = { _id: oid.toHexString(), ...doc }; - } - - this.setUpdatedAt(doc); - - // TODO reavaluate following type casting - return this.col.insertOne(doc as unknown as OptionalId, options); - } - - removeById(_id: string): Promise { - return this.deleteOne({ _id } as FilterQuery); - } - - async deleteOne( - filter: FilterQuery, - options?: CommonOptions & { bypassDocumentValidation?: boolean }, - ): Promise { - if (!this.trash) { - return this.col.deleteOne(filter, options); - } - - const doc = (await this.findOne(filter)) as unknown as (IRocketChatRecord & T) | undefined; - - if (doc) { - const { _id, ...record } = doc; - - const trash = { - ...record, - - _deletedAt: new Date(), - __collection__: this.name, - } as RocketChatRecordDeleted; - - // since the operation is not atomic, we need to make sure that the record is not already deleted/inserted - await this.trash?.updateOne( - { _id } as FilterQuery>, - { $set: trash }, - { - upsert: true, - }, - ); - } - - return this.col.deleteOne(filter, options); - } - - async deleteMany(filter: FilterQuery, options?: CommonOptions): Promise { - if (!this.trash) { - return this.col.deleteMany(filter, options); - } - - const cursor = this.find(filter); - - const ids: string[] = []; - for await (const doc of cursor) { - const { _id, ...record } = doc as unknown as IRocketChatRecord & T; - - const trash = { - ...record, - - _deletedAt: new Date(), - __collection__: this.name, - } as RocketChatRecordDeleted; - - ids.push(_id); - - // since the operation is not atomic, we need to make sure that the record is not already deleted/inserted - await this.trash?.updateOne( - { _id } as FilterQuery>, - { $set: trash }, - { - upsert: true, - }, - ); - } - - return this.col.deleteMany({ _id: { $in: ids } } as unknown as FilterQuery, options); - } - - // Trash - trashFind

>( - query: FilterQuery>, - options: FindOneOptions

? RocketChatRecordDeleted : P>, - ): Cursor> | undefined { - if (!this.trash) { - return undefined; - } - const { trash } = this; - - return trash.find( - { - __collection__: this.name, - ...query, - }, - options, - ); - } - - trashFindOneById(_id: string): Promise | null>; - - trashFindOneById( - _id: string, - options: WithoutProjection>, - ): Promise> | null>; - - trashFindOneById

( - _id: string, - options: FindOneOptions

? RocketChatRecordDeleted : P>, - ): Promise

; - - async trashFindOneById

>( - _id: string, - options?: - | undefined - | WithoutProjection> - | FindOneOptions

? RocketChatRecordDeleted : P>, - ): Promise | null> { - const query = { - _id, - __collection__: this.name, - } as FilterQuery>; - - if (!this.trash) { - return null; - } - const { trash } = this; - - return trash.findOne(query, options); - } - - private setUpdatedAt(record: UpdateQuery | InsertionModel): void { - if (this.preventSetUpdatedAt) { - return; - } - setUpdatedAt(record); - } - - trashFindDeletedAfter(deletedAt: Date): Cursor>; - - trashFindDeletedAfter( - deletedAt: Date, - query: FilterQuery>, - options: WithoutProjection>, - ): Cursor>; - - trashFindDeletedAfter

>( - deletedAt: Date, - query: FilterQuery

, - options: FindOneOptions

? RocketChatRecordDeleted : P>, - ): Cursor>; - - trashFindDeletedAfter

>( - deletedAt: Date, - query?: FilterQuery>, - options?: - | WithoutProjection> - | FindOneOptions

? RocketChatRecordDeleted : P>, - ): Cursor> { - const q = { - __collection__: this.name, - _deletedAt: { - $gt: deletedAt, - }, - ...query, - } as FilterQuery>; - - const { trash } = this; - - if (!trash) { - throw new Error('Trash is not enabled for this collection'); - } - - return trash.find(q, options as any); - } -} diff --git a/apps/meteor/app/models/server/raw/CredentialTokens.ts b/apps/meteor/app/models/server/raw/CredentialTokens.ts deleted file mode 100644 index aad8ddd08d17..000000000000 --- a/apps/meteor/app/models/server/raw/CredentialTokens.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ICredentialToken as T } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class CredentialTokensRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { expireAt: 1 }, sparse: true, expireAfterSeconds: 0 }]; - } - - async create(_id: string, userInfo: T['userInfo']): Promise { - const validForMilliseconds = 60000; // Valid for 60 seconds - const token = { - _id, - userInfo, - expireAt: new Date(Date.now() + validForMilliseconds), - }; - - await this.insertOne(token); - return token; - } - - findOneNotExpiredById(_id: string): Promise { - const query = { - _id, - expireAt: { $gt: new Date() }, - }; - - return this.findOne(query); - } -} diff --git a/apps/meteor/app/models/server/raw/CustomSounds.ts b/apps/meteor/app/models/server/raw/CustomSounds.ts deleted file mode 100644 index f83948d6cfa9..000000000000 --- a/apps/meteor/app/models/server/raw/CustomSounds.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Cursor, FindOneOptions, InsertOneWriteOpResult, UpdateWriteOpResult, WithId, WithoutProjection } from 'mongodb'; -import { ICustomSound as T } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class CustomSoundsRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { name: 1 } }]; - } - - // find - findByName(name: string, options: WithoutProjection>): Cursor { - const query = { - name, - }; - - return this.find(query, options); - } - - findByNameExceptId(name: string, except: string, options: WithoutProjection>): Cursor { - const query = { - _id: { $nin: [except] }, - name, - }; - - return this.find(query, options); - } - - // update - setName(_id: string, name: string): Promise { - const update = { - $set: { - name, - }, - }; - - return this.updateOne({ _id }, update); - } - - // INSERT - create(data: T): Promise>> { - return this.insertOne(data); - } -} diff --git a/apps/meteor/app/models/server/raw/CustomUserStatus.ts b/apps/meteor/app/models/server/raw/CustomUserStatus.ts deleted file mode 100644 index be94faa2833a..000000000000 --- a/apps/meteor/app/models/server/raw/CustomUserStatus.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Cursor, FindOneOptions, InsertOneWriteOpResult, UpdateWriteOpResult, WithId, WithoutProjection } from 'mongodb'; -import { ICustomUserStatus as T } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class CustomUserStatusRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { name: 1 } }]; - } - - // find one by name - async findOneByName(name: string, options: WithoutProjection>): Promise { - return this.findOne({ name }, options); - } - - // find - findByName(name: string, options: WithoutProjection>): Cursor { - const query = { - name, - }; - - return this.find(query, options); - } - - findByNameExceptId(name: string, except: string, options: WithoutProjection>): Cursor { - const query = { - _id: { $nin: [except] }, - name, - }; - - return this.find(query, options); - } - - // update - setName(_id: string, name: string): Promise { - const update = { - $set: { - name, - }, - }; - - return this.updateOne({ _id }, update); - } - - setStatusType(_id: string, statusType: string): Promise { - const update = { - $set: { - statusType, - }, - }; - - return this.updateOne({ _id }, update); - } - - // INSERT - create(data: T): Promise>> { - return this.insertOne(data); - } -} diff --git a/apps/meteor/app/models/server/raw/EmailInbox.ts b/apps/meteor/app/models/server/raw/EmailInbox.ts deleted file mode 100644 index 73bd24cadfd2..000000000000 --- a/apps/meteor/app/models/server/raw/EmailInbox.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { IEmailInbox } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class EmailInboxRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { email: 1 }, unique: true }]; - } -} diff --git a/apps/meteor/app/models/server/raw/EmailMessageHistory.ts b/apps/meteor/app/models/server/raw/EmailMessageHistory.ts deleted file mode 100644 index f2eefedffe94..000000000000 --- a/apps/meteor/app/models/server/raw/EmailMessageHistory.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { InsertOneWriteOpResult, WithId } from 'mongodb'; -import { IEmailMessageHistory as T } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class EmailMessageHistoryRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { createdAt: 1 }, expireAfterSeconds: 60 * 60 * 24 }]; - } - - async create({ _id, email }: T): Promise>> { - return this.insertOne({ - _id, - email, - createdAt: new Date(), - }); - } -} diff --git a/apps/meteor/app/models/server/raw/EmojiCustom.ts b/apps/meteor/app/models/server/raw/EmojiCustom.ts deleted file mode 100644 index 30186cbdd3e4..000000000000 --- a/apps/meteor/app/models/server/raw/EmojiCustom.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Cursor, FindOneOptions, InsertOneWriteOpResult, UpdateWriteOpResult, WithId, WithoutProjection } from 'mongodb'; -import { IEmojiCustom as T } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class EmojiCustomRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { name: 1 } }, { key: { aliases: 1 } }, { key: { extension: 1 } }]; - } - - // find - findByNameOrAlias(emojiName: string, options: WithoutProjection>): Cursor { - let name = emojiName; - - if (typeof emojiName === 'string') { - name = emojiName.replace(/:/g, ''); - } - - const query = { - $or: [{ name }, { aliases: name }], - }; - - return this.find(query, options); - } - - findByNameOrAliasExceptID(name: string, except: string, options: WithoutProjection>): Cursor { - const query = { - _id: { $nin: [except] }, - $or: [{ name }, { aliases: name }], - }; - - return this.find(query, options); - } - - // update - setName(_id: string, name: string): Promise { - const update = { - $set: { - name, - }, - }; - - return this.updateOne({ _id }, update); - } - - setAliases(_id: string, aliases: string): Promise { - const update = { - $set: { - aliases, - }, - }; - - return this.updateOne({ _id }, update); - } - - setExtension(_id: string, extension: string): Promise { - const update = { - $set: { - extension, - }, - }; - - return this.updateOne({ _id }, update); - } - - // INSERT - create(data: T): Promise>> { - return this.insertOne(data); - } -} diff --git a/apps/meteor/app/models/server/raw/ExportOperations.ts b/apps/meteor/app/models/server/raw/ExportOperations.ts deleted file mode 100644 index 9ce76509fff3..000000000000 --- a/apps/meteor/app/models/server/raw/ExportOperations.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Cursor, UpdateWriteOpResult } from 'mongodb'; -import type { IExportOperation } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -type T = IExportOperation; - -export class ExportOperationsRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { userId: 1 } }, { key: { status: 1 } }]; - } - - findOnePending(): Promise { - const query = { - status: { $nin: ['completed', 'skipped'] }, - }; - - return this.findOne(query); - } - - async create(data: T): Promise { - const result = await this.insertOne({ - ...data, - createdAt: new Date(), - }); - - return result.insertedId; - } - - findLastOperationByUser(userId: string, fullExport = false): Promise { - const query = { - userId, - fullExport, - }; - - return this.findOne(query, { sort: { createdAt: -1 } }); - } - - findAllPendingBeforeMyRequest(requestDay: Date): Cursor { - const query = { - status: { $nin: ['completed', 'skipped'] }, - createdAt: { $lt: requestDay }, - }; - - return this.find(query); - } - - updateOperation(data: T): Promise { - const update = { - $set: { - roomList: data.roomList, - status: data.status, - fileList: data.fileList, - generatedFile: data.generatedFile, - fileId: data.fileId, - userNameTable: data.userNameTable, - userData: data.userData, - generatedUserFile: data.generatedUserFile, - generatedAvatar: data.generatedAvatar, - exportPath: data.exportPath, - assetsPath: data.assetsPath, - }, - }; - - return this.updateOne({ _id: data._id }, update); - } -} diff --git a/apps/meteor/app/models/server/raw/FederationKeys.ts b/apps/meteor/app/models/server/raw/FederationKeys.ts deleted file mode 100644 index 3b2c505b09aa..000000000000 --- a/apps/meteor/app/models/server/raw/FederationKeys.ts +++ /dev/null @@ -1,70 +0,0 @@ -import NodeRSA from 'node-rsa'; - -import { BaseRaw } from './BaseRaw'; - -type T = { - type: 'private' | 'public'; - key: string; -}; - -export class FederationKeysRaw extends BaseRaw { - async getKey(type: T['type']): Promise { - const keyResource = await this.findOne({ type }); - - if (!keyResource) { - return null; - } - - return keyResource.key; - } - - loadKey(keyData: NodeRSA.Key, type: T['type']): NodeRSA { - return new NodeRSA(keyData, `pkcs8-${type}-pem`); - } - - async generateKeys(): Promise<{ - privateKey: '' | NodeRSA | null; - publicKey: '' | NodeRSA | null; - }> { - const key = new NodeRSA({ b: 512 }); - - key.generateKeyPair(); - - await this.deleteMany({}); - - await this.insertOne({ - type: 'private', - key: key.exportKey('pkcs8-private-pem').replace(/\n|\r/g, ''), - }); - - await this.insertOne({ - type: 'public', - key: key.exportKey('pkcs8-public-pem').replace(/\n|\r/g, ''), - }); - - return { - privateKey: await this.getPrivateKey(), - publicKey: await this.getPublicKey(), - }; - } - - async getPrivateKey(): Promise<'' | NodeRSA | null> { - const keyData = await this.getKey('private'); - - return keyData && this.loadKey(keyData, 'private'); - } - - getPrivateKeyString(): Promise { - return this.getKey('private'); - } - - async getPublicKey(): Promise<'' | NodeRSA | null> { - const keyData = await this.getKey('public'); - - return keyData && this.loadKey(keyData, 'public'); - } - - getPublicKeyString(): Promise { - return this.getKey('public'); - } -} diff --git a/apps/meteor/app/models/server/raw/FederationServers.ts b/apps/meteor/app/models/server/raw/FederationServers.ts deleted file mode 100644 index 06af3db6e886..000000000000 --- a/apps/meteor/app/models/server/raw/FederationServers.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { UpdateWriteOpResult } from 'mongodb'; -import type { IFederationServer } from '@rocket.chat/core-typings'; - -import { Users } from './index'; -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class FederationServersRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { domain: 1 } }]; - } - - saveDomain(domain: string): Promise { - return this.updateOne( - { domain }, - { - $setOnInsert: { - domain, - }, - }, - { upsert: true }, - ); - } - - async refreshServers(): Promise { - const domains = await Users.getDistinctFederationDomains(); - - for await (const domain of domains) { - await this.saveDomain(domain); - } - - await this.deleteMany({ domain: { $nin: domains } }); - } -} diff --git a/apps/meteor/app/models/server/raw/ImportData.ts b/apps/meteor/app/models/server/raw/ImportData.ts deleted file mode 100644 index 0873627a6309..000000000000 --- a/apps/meteor/app/models/server/raw/ImportData.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Cursor } from 'mongodb'; -import type { IImportRecord, IImportUserRecord, IImportMessageRecord, IImportChannelRecord } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -export class ImportDataRaw extends BaseRaw { - getAllUsers(): Cursor { - return this.find({ dataType: 'user' }) as Cursor; - } - - getAllMessages(): Cursor { - return this.find({ dataType: 'message' }) as Cursor; - } - - getAllChannels(): Cursor { - return this.find({ dataType: 'channel' }) as Cursor; - } -} diff --git a/apps/meteor/app/models/server/raw/InstanceStatus.ts b/apps/meteor/app/models/server/raw/InstanceStatus.ts deleted file mode 100644 index 80974cf4c6d4..000000000000 --- a/apps/meteor/app/models/server/raw/InstanceStatus.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { IInstanceStatus } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -export class InstanceStatusRaw extends BaseRaw {} diff --git a/apps/meteor/app/models/server/raw/IntegrationHistory.ts b/apps/meteor/app/models/server/raw/IntegrationHistory.ts deleted file mode 100644 index 1fcf4230586a..000000000000 --- a/apps/meteor/app/models/server/raw/IntegrationHistory.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { IIntegrationHistory } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class IntegrationHistoryRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [ - { key: { 'integration._id': 1, 'integration._createdBy._id': 1 } }, - { key: { _updatedAt: 1 }, expireAfterSeconds: 30 * 24 * 60 * 60 }, - ]; - } - - removeByIntegrationId(integrationId: string): ReturnType['deleteMany']> { - return this.deleteMany({ 'integration._id': integrationId }); - } - - findOneByIntegrationIdAndHistoryId(integrationId: string, historyId: string): Promise { - return this.findOne({ 'integration._id': integrationId, '_id': historyId }); - } -} diff --git a/apps/meteor/app/models/server/raw/Integrations.ts b/apps/meteor/app/models/server/raw/Integrations.ts deleted file mode 100644 index 844d046ed28f..000000000000 --- a/apps/meteor/app/models/server/raw/Integrations.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { IIntegration, IUser } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class IntegrationsRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { type: 1 } }]; - } - - findOneByUrl(url: string): Promise { - return this.findOne({ url }); - } - - updateRoomName(oldRoomName: string, newRoomName: string): ReturnType['updateMany']> { - const hashedOldRoomName = `#${oldRoomName}`; - const hashedNewRoomName = `#${newRoomName}`; - - return this.updateMany( - { - channel: hashedOldRoomName, - }, - { - $set: { - 'channel.$': hashedNewRoomName, - }, - }, - ); - } - - findOneByIdAndCreatedByIfExists({ - _id, - createdBy, - }: { - _id: IIntegration['_id']; - createdBy?: IUser['_id']; - }): Promise { - return this.findOne({ - _id, - ...(createdBy && { '_createdBy._id': createdBy }), - }); - } - - disableByUserId(userId: string): ReturnType['updateMany']> { - return this.updateMany({ userId }, { $set: { enabled: false } }); - } -} diff --git a/apps/meteor/app/models/server/raw/Invites.ts b/apps/meteor/app/models/server/raw/Invites.ts deleted file mode 100644 index ef55e009ca8e..000000000000 --- a/apps/meteor/app/models/server/raw/Invites.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { UpdateWriteOpResult } from 'mongodb'; -import type { IInvite } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -type T = IInvite; - -export class InvitesRaw extends BaseRaw { - findOneByUserRoomMaxUsesAndExpiration(userId: string, rid: string, maxUses: number, daysToExpire: number): Promise { - return this.findOne({ - rid, - userId, - days: daysToExpire, - maxUses, - ...(daysToExpire > 0 ? { expires: { $gt: new Date() } } : {}), - ...(maxUses > 0 ? { uses: { $lt: maxUses } } : {}), - }); - } - - increaseUsageById(_id: string, uses = 1): Promise { - return this.updateOne( - { _id }, - { - $inc: { - uses, - }, - }, - ); - } - - async countUses(): Promise { - const [result] = await this.col - .aggregate<{ totalUses: number } | undefined>([{ $group: { _id: null, totalUses: { $sum: '$uses' } } }]) - .toArray(); - - return result?.totalUses || 0; - } -} diff --git a/apps/meteor/app/models/server/raw/LivechatAgentActivity.ts b/apps/meteor/app/models/server/raw/LivechatAgentActivity.ts deleted file mode 100644 index 4e8aa305c281..000000000000 --- a/apps/meteor/app/models/server/raw/LivechatAgentActivity.ts +++ /dev/null @@ -1,205 +0,0 @@ -import moment from 'moment'; -import type { ILivechatAgentActivity, IServiceHistory } from '@rocket.chat/core-typings'; -import type { AggregationCursor, Cursor, FindAndModifyWriteOpResultObject, IndexSpecification, UpdateWriteOpResult } from 'mongodb'; - -import { BaseRaw } from './BaseRaw'; - -export class LivechatAgentActivityRaw extends BaseRaw { - modelIndexes(): IndexSpecification[] { - return [{ key: { date: 1 } }, { key: { agentId: 1, date: 1 }, unique: true }]; - } - - findOneByAgendIdAndDate(agentId: string, date: ILivechatAgentActivity['date']): Promise { - return this.findOne({ agentId, date }); - } - - async createOrUpdate( - data: Partial> = {}, - ): Promise | undefined> { - const { date, agentId, lastStartedAt } = data; - - if (!date || !agentId) { - return; - } - - return this.findOneAndUpdate( - { agentId, date }, - { - $unset: { - lastStoppedAt: 1, - }, - $set: { - lastStartedAt: lastStartedAt || new Date(), - }, - $setOnInsert: { - date, - agentId, - }, - }, - ); - } - - updateLastStoppedAt({ - agentId, - date, - lastStoppedAt, - availableTime, - }: Pick): Promise { - const query = { - agentId, - date, - }; - const update = { - $inc: { availableTime }, - $set: { - lastStoppedAt, - }, - }; - return this.updateMany(query, update); - } - - updateServiceHistory({ - agentId, - date, - serviceHistory, - }: Pick & { serviceHistory: IServiceHistory }): Promise { - const query = { - agentId, - date, - }; - const update = { - $addToSet: { - serviceHistory, - }, - }; - return this.updateMany(query, update); - } - - findOpenSessions(): Cursor { - const query = { - lastStoppedAt: { $exists: false }, - }; - - return this.find(query); - } - - findAllAverageAvailableServiceTime({ date, departmentId }: { date: Date; departmentId: string }): Promise { - const match = { $match: { date } }; - const lookup = { - $lookup: { - from: 'rocketchat_livechat_department_agents', - localField: 'agentId', - foreignField: 'agentId', - as: 'departments', - }, - }; - const unwind = { - $unwind: { - path: '$departments', - preserveNullAndEmptyArrays: true, - }, - }; - const departmentsMatch = { - $match: { - 'departments.departmentId': departmentId, - }, - }; - const sumAvailableTimeWithCurrentTime = { - $sum: [{ $divide: [{ $subtract: [new Date(), '$lastStartedAt'] }, 1000] }, '$availableTime'], - }; - const group = { - $group: { - _id: null, - allAvailableTimeInSeconds: { - $sum: { - $cond: [{ $ifNull: ['$lastStoppedAt', false] }, '$availableTime', sumAvailableTimeWithCurrentTime], - }, - }, - rooms: { $sum: 1 }, - }, - }; - const project = { - $project: { - averageAvailableServiceTimeInSeconds: { - $trunc: { - $cond: [{ $eq: ['$rooms', 0] }, 0, { $divide: ['$allAvailableTimeInSeconds', '$rooms'] }], - }, - }, - }, - }; - const params = [match] as object[]; - if (departmentId && departmentId !== 'undefined') { - params.push(lookup); - params.push(unwind); - params.push(departmentsMatch); - } - params.push(group); - params.push(project); - return this.col.aggregate(params).toArray(); - } - - findAvailableServiceTimeHistory({ - start, - end, - fullReport, - onlyCount = false, - options = {}, - }: { - start: string; - end: string; - fullReport: boolean; - onlyCount: boolean; - options: any; - }): AggregationCursor { - const match = { - $match: { - date: { - $gte: parseInt(moment(start).format('YYYYMMDD')), - $lte: parseInt(moment(end).format('YYYYMMDD')), - }, - }, - }; - const lookup = { - $lookup: { - from: 'users', - localField: 'agentId', - foreignField: '_id', - as: 'user', - }, - }; - const unwind = { - $unwind: { - path: '$user', - }, - }; - const group = { - $group: { - _id: { _id: '$user._id', username: '$user.username' }, - serviceHistory: { $first: '$serviceHistory' }, - availableTimeInSeconds: { $sum: '$availableTime' }, - }, - }; - const project = { - $project: { - _id: 0, - username: '$_id.username', - availableTimeInSeconds: 1, - ...(fullReport && { serviceHistory: 1 }), - }, - }; - - const sort = { $sort: options.sort || { username: 1 } }; - const params = [match, lookup, unwind, group, project, sort] as object[]; - if (onlyCount) { - params.push({ $count: 'total' }); - return this.col.aggregate(params); - } - if (options.offset) { - params.push({ $skip: options.offset }); - } - if (options.count) { - params.push({ $limit: options.count }); - } - return this.col.aggregate(params, { allowDiskUse: true }); - } -} diff --git a/apps/meteor/app/models/server/raw/LivechatBusinessHours.ts b/apps/meteor/app/models/server/raw/LivechatBusinessHours.ts deleted file mode 100644 index 161009c6bee2..000000000000 --- a/apps/meteor/app/models/server/raw/LivechatBusinessHours.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { FindOneOptions, ObjectId, WithoutProjection } from 'mongodb'; -import { ILivechatBusinessHour, LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -export interface IWorkHoursCronJobsItem { - day: string; - times: string[]; -} - -export interface IWorkHoursCronJobsWrapper { - start: IWorkHoursCronJobsItem[]; - finish: IWorkHoursCronJobsItem[]; -} - -export class LivechatBusinessHoursRaw extends BaseRaw { - async findOneDefaultBusinessHour(options?: undefined): Promise; - - async findOneDefaultBusinessHour( - options: WithoutProjection>, - ): Promise; - - async findOneDefaultBusinessHour

( - options: FindOneOptions

, - ): Promise

; - - findOneDefaultBusinessHour

(options?: any): Promise { - return this.findOne({ type: LivechatBusinessHourTypes.DEFAULT }, options); - } - - findActiveAndOpenBusinessHoursByDay(day: string, options?: any): Promise { - return this.find( - { - active: true, - workHours: { - $elemMatch: { - $or: [{ 'start.cron.dayOfWeek': day }, { 'finish.cron.dayOfWeek': day }], - open: true, - }, - }, - }, - options, - ).toArray(); - } - - findDefaultActiveAndOpenBusinessHoursByDay(day: string, options?: any): Promise { - return this.find( - { - type: LivechatBusinessHourTypes.DEFAULT, - active: true, - workHours: { - $elemMatch: { - $or: [{ 'start.cron.dayOfWeek': day, 'finish.cron.dayOfWeek': day }], - open: true, - }, - }, - }, - options, - ).toArray(); - } - - async insertOne(data: Omit): Promise { - return this.col.insertOne({ - _id: new ObjectId().toHexString(), - ...{ ts: new Date() }, - ...data, - }); - } - - findHoursToScheduleJobs(): Promise { - return this.col - .aggregate([ - { - $facet: { - start: [ - { $match: { active: true } }, - { $project: { _id: 0, workHours: 1 } }, - { $unwind: { path: '$workHours' } }, - { $match: { 'workHours.open': true } }, - { - $group: { - _id: { day: '$workHours.start.cron.dayOfWeek' }, - times: { $addToSet: '$workHours.start.cron.time' }, - }, - }, - { - $project: { - _id: 0, - day: '$_id.day', - times: 1, - }, - }, - ], - finish: [ - { $match: { active: true } }, - { $project: { _id: 0, workHours: 1 } }, - { $unwind: { path: '$workHours' } }, - { $match: { 'workHours.open': true } }, - { - $group: { - _id: { day: '$workHours.finish.cron.dayOfWeek' }, - times: { $addToSet: '$workHours.finish.cron.time' }, - }, - }, - { - $project: { - _id: 0, - day: '$_id.day', - times: 1, - }, - }, - ], - }, - }, - ]) - .toArray() as any; - } - - async findActiveBusinessHoursToOpen( - day: string, - start: string, - type?: LivechatBusinessHourTypes, - options?: any, - ): Promise { - const query: Record = { - active: true, - workHours: { - $elemMatch: { - 'start.cron.dayOfWeek': day, - 'start.cron.time': start, - 'open': true, - }, - }, - }; - if (type) { - query.type = type; - } - return this.col.find(query, options).toArray(); - } - - async findActiveBusinessHoursToClose( - day: string, - finish: string, - type?: LivechatBusinessHourTypes, - options?: any, - ): Promise { - const query: Record = { - active: true, - workHours: { - $elemMatch: { - 'finish.cron.dayOfWeek': day, - 'finish.cron.time': finish, - 'open': true, - }, - }, - }; - if (type) { - query.type = type; - } - return this.col.find(query, options).toArray(); - } -} diff --git a/apps/meteor/app/models/server/raw/LivechatCustomField.ts b/apps/meteor/app/models/server/raw/LivechatCustomField.ts deleted file mode 100644 index 60593e573eb9..000000000000 --- a/apps/meteor/app/models/server/raw/LivechatCustomField.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { ILivechatCustomField } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -export class LivechatCustomFieldRaw extends BaseRaw {} diff --git a/apps/meteor/app/models/server/raw/LivechatDepartment.ts b/apps/meteor/app/models/server/raw/LivechatDepartment.ts deleted file mode 100644 index 8ef4fe5ac1c7..000000000000 --- a/apps/meteor/app/models/server/raw/LivechatDepartment.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { FindOneOptions, Cursor, FilterQuery, WriteOpResult } from 'mongodb'; -import type { ILivechatDepartmentRecord } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -export class LivechatDepartmentRaw extends BaseRaw { - findInIds(departmentsIds: string[], options: FindOneOptions): Cursor { - const query = { _id: { $in: departmentsIds } }; - return this.find(query, options); - } - - findByNameRegexWithExceptionsAndConditions( - searchTerm: string, - exceptions: string[] = [], - conditions: FilterQuery = {}, - options: FindOneOptions = {}, - ): Cursor { - if (!Array.isArray(exceptions)) { - exceptions = [exceptions]; - } - - const nameRegex = new RegExp(`^${escapeRegExp(searchTerm).trim()}`, 'i'); - - const query = { - name: nameRegex, - _id: { - $nin: exceptions, - }, - ...conditions, - }; - - return this.find(query, options); - } - - findByBusinessHourId(businessHourId: string, options: FindOneOptions): Cursor { - const query = { businessHourId }; - return this.find(query, options); - } - - findEnabledByBusinessHourId( - businessHourId: string, - options: FindOneOptions, - ): Cursor { - const query = { businessHourId, enabled: true }; - return this.find(query, options); - } - - findEnabledByListOfBusinessHourIdsAndDepartmentIds( - businessHourIds: string[], - departmentIds: string[], - options: FindOneOptions, - ): Cursor { - const query: FilterQuery = { - enabled: true, - businessHourId: { - $in: businessHourIds, - }, - _id: { - $in: departmentIds, - }, - }; - return this.find(query, options); - } - - addBusinessHourToDepartmentsByIds(ids: string[] = [], businessHourId: string): Promise { - const query = { - _id: { $in: ids }, - }; - - const update = { - $set: { - businessHourId, - }, - }; - - return this.col.update(query, update, { multi: true }); - } - - removeBusinessHourFromDepartmentsByIdsAndBusinessHourId(ids: string[] = [], businessHourId: string): Promise { - const query = { - _id: { $in: ids }, - businessHourId, - }; - - const update = { - $unset: { - businessHourId: 1, - }, - }; - - return this.col.update(query, update, { multi: true }); - } - - removeBusinessHourFromDepartmentsByBusinessHourId(businessHourId: string): Promise { - const query = { - businessHourId, - }; - - const update = { - $unset: { - businessHourId: 1, - }, - }; - - return this.col.update(query, update, { multi: true }); - } -} diff --git a/apps/meteor/app/models/server/raw/LivechatDepartmentAgents.ts b/apps/meteor/app/models/server/raw/LivechatDepartmentAgents.ts deleted file mode 100644 index 0a5c56f90a23..000000000000 --- a/apps/meteor/app/models/server/raw/LivechatDepartmentAgents.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { Cursor, FilterQuery, WithoutProjection, FindOneOptions } from 'mongodb'; -import type { ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -export class LivechatDepartmentAgentsRaw extends BaseRaw { - findUsersInQueue(usersList: string[]): Cursor; - - findUsersInQueue( - usersList: string[], - options: WithoutProjection>, - ): Cursor; - - findUsersInQueue

( - usersList: string[], - options: FindOneOptions

, - ): Cursor

; - - findUsersInQueue

( - usersList: string[], - options?: - | undefined - | WithoutProjection> - | FindOneOptions

, - ): Cursor | Cursor

{ - const query: FilterQuery = {}; - - if (Array.isArray(usersList) && usersList.length) { - // TODO: Remove - query.username = { - $in: usersList, - }; - } - - if (options === undefined) { - return this.find(query); - } - - return this.find(query, options); - } - - findByAgentId(agentId: string): Cursor { - return this.find({ agentId }); - } - - findAgentsByDepartmentId(departmentId: string): Cursor; - - findAgentsByDepartmentId( - departmentId: string, - options: WithoutProjection>, - ): Cursor; - - findAgentsByDepartmentId

( - departmentId: string, - options: FindOneOptions

, - ): Cursor

; - - findAgentsByDepartmentId

( - departmentId: string, - options?: - | undefined - | WithoutProjection> - | FindOneOptions

, - ): Cursor | Cursor

{ - const query = { departmentId }; - - if (options === undefined) { - return this.find(query); - } - - return this.find(query, options); - } - - findActiveDepartmentsByAgentId(agentId: string): Cursor; - - findActiveDepartmentsByAgentId( - agentId: string, - options: WithoutProjection>, - ): Cursor; - - findActiveDepartmentsByAgentId

( - agentId: string, - options: FindOneOptions

, - ): Cursor

; - - findActiveDepartmentsByAgentId

( - agentId: string, - options?: - | undefined - | WithoutProjection> - | FindOneOptions

, - ): Cursor | Cursor

{ - const query = { - agentId, - departmentEnabled: true, - }; - - if (options === undefined) { - return this.find(query); - } - - return this.find(query, options); - } - - findByDepartmentIds(departmentIds: string[], options = {}): Cursor { - return this.find({ departmentId: { $in: departmentIds } }, options); - } - - findAgentsByAgentIdAndBusinessHourId(_agentId: string, _businessHourId: string): [] { - return []; - } -} diff --git a/apps/meteor/app/models/server/raw/LivechatInquiry.ts b/apps/meteor/app/models/server/raw/LivechatInquiry.ts deleted file mode 100644 index 4a39b54673e3..000000000000 --- a/apps/meteor/app/models/server/raw/LivechatInquiry.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { FindOneOptions, MongoDistinctPreferences, UpdateWriteOpResult } from 'mongodb'; -import { IMessage, ILivechatInquiryRecord, LivechatInquiryStatus } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -export class LivechatInquiryRaw extends BaseRaw { - findOneQueuedByRoomId(rid: string): Promise<(ILivechatInquiryRecord & { status: LivechatInquiryStatus.QUEUED }) | null> { - const query = { - rid, - status: LivechatInquiryStatus.QUEUED, - }; - return this.findOne(query) as unknown as Promise<(ILivechatInquiryRecord & { status: LivechatInquiryStatus.QUEUED }) | null>; - } - - findOneByRoomId( - rid: string, - options: FindOneOptions, - ): Promise { - const query = { - rid, - }; - return this.findOne(query, options); - } - - getDistinctQueuedDepartments(options: MongoDistinctPreferences): Promise { - return this.col.distinct('department', { status: LivechatInquiryStatus.QUEUED }, options); - } - - async setDepartmentByInquiryId(inquiryId: string, department: string): Promise { - const updated = await this.findOneAndUpdate({ _id: inquiryId }, { $set: { department } }, { returnDocument: 'after' }); - return updated.value; - } - - async setLastMessageByRoomId(rid: string, message: IMessage): Promise { - return this.updateOne({ rid }, { $set: { lastMessage: message } }); - } -} diff --git a/apps/meteor/app/models/server/raw/LivechatTrigger.ts b/apps/meteor/app/models/server/raw/LivechatTrigger.ts deleted file mode 100644 index 813d13d7ca95..000000000000 --- a/apps/meteor/app/models/server/raw/LivechatTrigger.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Cursor, UpdateWriteOpResult } from 'mongodb'; -import type { ILivechatTrigger } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class LivechatTriggerRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { enabled: 1 } }]; - } - - findEnabled(): Cursor { - return this.find({ enabled: true }); - } - - updateById(_id: string, data: ILivechatTrigger): Promise { - return this.updateOne({ _id }, { $set: data }); - } -} diff --git a/apps/meteor/app/models/server/raw/LivechatVisitors.ts b/apps/meteor/app/models/server/raw/LivechatVisitors.ts deleted file mode 100644 index 8cb7ebbfb75f..000000000000 --- a/apps/meteor/app/models/server/raw/LivechatVisitors.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { AggregationCursor, Cursor, FilterQuery, FindOneOptions, WithoutProjection } from 'mongodb'; -import type { ILivechatVisitor } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -export class LivechatVisitorsRaw extends BaseRaw { - findOneById(_id: string, options: WithoutProjection>): Promise { - const query = { - _id, - }; - - return this.findOne(query, options); - } - - getVisitorByToken(token: string, options: WithoutProjection>): Promise { - const query = { - token, - }; - - return this.findOne(query, options); - } - - getVisitorsBetweenDate({ start, end, department }: { start: Date; end: Date; department: string }): Cursor { - const query = { - _updatedAt: { - $gte: new Date(start), - $lt: new Date(end), - }, - ...(department && department !== 'undefined' && { department }), - }; - - return this.find(query, { projection: { _id: 1 } }); - } - - findByNameRegexWithExceptionsAndConditions

( - searchTerm: string, - exceptions: string[] = [], - conditions: FilterQuery = {}, - options: FindOneOptions

= {}, - ): AggregationCursor< - P & { - custom_name: string; - } - > { - if (!Array.isArray(exceptions)) { - exceptions = [exceptions]; - } - - const nameRegex = new RegExp(`^${escapeRegExp(searchTerm).trim()}`, 'i'); - - const match = { - $match: { - name: nameRegex, - _id: { - $nin: exceptions, - }, - ...conditions, - }, - }; - - const { projection, sort, skip, limit } = options; - const project = { - $project: { - // TODO: move this logic to client - // eslint-disable-next-line @typescript-eslint/camelcase - custom_name: { $concat: ['$username', ' - ', '$name'] }, - ...projection, - }, - }; - - const order = { $sort: sort || { name: 1 } }; - const params: Record[] = [match, order, skip && { $skip: skip }, limit && { $limit: limit }, project].filter( - Boolean, - ) as Record[]; - - return this.col.aggregate(params); - } - - /** - * Find visitors by their email or phone or username or name - * @return [{object}] List of Visitors from db - */ - findVisitorsByEmailOrPhoneOrNameOrUsername( - _emailOrPhoneOrNameOrUsername: string, - options: FindOneOptions, - ): Cursor { - const filter = new RegExp(_emailOrPhoneOrNameOrUsername, 'i'); - const query = { - $or: [ - { - 'visitorEmails.address': _emailOrPhoneOrNameOrUsername, - }, - { - 'phone.phoneNumber': _emailOrPhoneOrNameOrUsername, - }, - { - name: filter, - }, - { - username: filter, - }, - ], - }; - - return this.find(query, options); - } -} diff --git a/apps/meteor/app/models/server/raw/LoginServiceConfiguration.ts b/apps/meteor/app/models/server/raw/LoginServiceConfiguration.ts deleted file mode 100644 index edbe9b6e3681..000000000000 --- a/apps/meteor/app/models/server/raw/LoginServiceConfiguration.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { ILoginServiceConfiguration } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -export class LoginServiceConfigurationRaw extends BaseRaw {} diff --git a/apps/meteor/app/models/server/raw/Messages.js b/apps/meteor/app/models/server/raw/Messages.js deleted file mode 100644 index 7f84cac25420..000000000000 --- a/apps/meteor/app/models/server/raw/Messages.js +++ /dev/null @@ -1,233 +0,0 @@ -import { escapeRegExp } from '@rocket.chat/string-helpers'; - -import { BaseRaw } from './BaseRaw'; - -export class MessagesRaw extends BaseRaw { - findVisibleByMentionAndRoomId(username, rid, options) { - const query = { - '_hidden': { $ne: true }, - 'mentions.username': username, - rid, - }; - - return this.find(query, options); - } - - findStarredByUserAtRoom(userId, roomId, options) { - const query = { - '_hidden': { $ne: true }, - 'starred._id': userId, - 'rid': roomId, - }; - - return this.find(query, options); - } - - findByRoomIdAndType(roomId, type, options) { - const query = { - rid: roomId, - t: type, - }; - - if (options == null) { - options = {}; - } - - return this.find(query, options); - } - - findSnippetedByRoom(roomId, options) { - const query = { - _hidden: { $ne: true }, - snippeted: true, - rid: roomId, - }; - - return this.find(query, options); - } - - findDiscussionsByRoom(rid, options) { - const query = { rid, drid: { $exists: true } }; - - return this.find(query, options); - } - - findDiscussionsByRoomAndText(rid, text, options) { - const query = { - rid, - drid: { $exists: true }, - msg: new RegExp(escapeRegExp(text), 'i'), - }; - - return this.find(query, options); - } - - findAllNumberOfTransferredRooms({ start, end, departmentId, onlyCount = false, options = {} }) { - const match = { - $match: { - t: 'livechat_transfer_history', - ts: { $gte: new Date(start), $lte: new Date(end) }, - }, - }; - const lookup = { - $lookup: { - from: 'rocketchat_room', - localField: 'rid', - foreignField: '_id', - as: 'room', - }, - }; - const unwind = { - $unwind: { - path: '$room', - preserveNullAndEmptyArrays: true, - }, - }; - const group = { - $group: { - _id: { - _id: null, - departmentId: '$room.departmentId', - }, - numberOfTransferredRooms: { $sum: 1 }, - }, - }; - const project = { - $project: { - _id: { $ifNull: ['$_id.departmentId', null] }, - numberOfTransferredRooms: 1, - }, - }; - const firstParams = [match, lookup, unwind]; - if (departmentId) { - firstParams.push({ - $match: { - 'room.departmentId': departmentId, - }, - }); - } - const sort = { $sort: options.sort || { name: 1 } }; - const params = [...firstParams, group, project, sort]; - if (onlyCount) { - params.push({ $count: 'total' }); - return this.col.aggregate(params); - } - if (options.offset) { - params.push({ $skip: options.offset }); - } - if (options.count) { - params.push({ $limit: options.count }); - } - return this.col.aggregate(params, { allowDiskUse: true }); - } - - getTotalOfMessagesSentByDate({ start, end, options = {} }) { - const params = [ - { $match: { t: { $exists: false }, ts: { $gte: start, $lte: end } } }, - { - $lookup: { - from: 'rocketchat_room', - localField: 'rid', - foreignField: '_id', - as: 'room', - }, - }, - { - $unwind: { - path: '$room', - }, - }, - { - $group: { - _id: { - _id: '$room._id', - name: { - $cond: [{ $ifNull: ['$room.fname', false] }, '$room.fname', '$room.name'], - }, - t: '$room.t', - usernames: { - $cond: [{ $ifNull: ['$room.usernames', false] }, '$room.usernames', []], - }, - date: { - $concat: [{ $substr: ['$ts', 0, 4] }, { $substr: ['$ts', 5, 2] }, { $substr: ['$ts', 8, 2] }], - }, - }, - messages: { $sum: 1 }, - }, - }, - { - $project: { - _id: 0, - date: '$_id.date', - room: { - _id: '$_id._id', - name: '$_id.name', - t: '$_id.t', - usernames: '$_id.usernames', - }, - type: 'messages', - messages: 1, - }, - }, - ]; - if (options.sort) { - params.push({ $sort: options.sort }); - } - if (options.count) { - params.push({ $limit: options.count }); - } - return this.col.aggregate(params).toArray(); - } - - findLivechatClosedMessages(rid, options) { - return this.find( - { - rid, - $or: [{ t: { $exists: false } }, { t: 'livechat-close' }], - }, - options, - ); - } - - async countRoomsWithStarredMessages(options) { - const [queryResult] = await this.col - .aggregate( - [{ $match: { 'starred._id': { $exists: true } } }, { $group: { _id: '$rid' } }, { $group: { _id: null, total: { $sum: 1 } } }], - options, - ) - .toArray(); - - 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(); - - return queryResult?.total || 0; - } - - async countE2EEMessages(options) { - return this.find({ t: 'e2e' }, options).count(); - } - - findPinned(options) { - const query = { - t: { $ne: 'rm' }, - _hidden: { $ne: true }, - pinned: true, - }; - - return this.find(query, options); - } - - findStarred(options) { - const query = { - '_hidden': { $ne: true }, - 'starred._id': { $exists: true }, - }; - - return this.find(query, options); - } -} diff --git a/apps/meteor/app/models/server/raw/NotificationQueue.ts b/apps/meteor/app/models/server/raw/NotificationQueue.ts deleted file mode 100644 index 4297c1226566..000000000000 --- a/apps/meteor/app/models/server/raw/NotificationQueue.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { UpdateWriteOpResult } from 'mongodb'; -import type { INotification } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class NotificationQueueRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [ - { key: { uid: 1 } }, - { key: { ts: 1 }, expireAfterSeconds: 2 * 60 * 60 }, - { key: { schedule: 1 }, sparse: true }, - { key: { sending: 1 }, sparse: true }, - { key: { error: 1 }, sparse: true }, - ]; - } - - unsetSendingById(_id: string): Promise { - return this.updateOne( - { _id }, - { - $unset: { - sending: 1, - }, - }, - ); - } - - setErrorById(_id: string, error: any): Promise { - return this.updateOne( - { - _id, - }, - { - $set: { - error, - }, - $unset: { - sending: 1, - }, - }, - ); - } - - clearScheduleByUserId(uid: string): Promise { - return this.updateMany( - { - uid, - schedule: { $exists: true }, - }, - { - $unset: { - schedule: 1, - }, - }, - ); - } - - async clearQueueByUserId(uid: string): Promise { - const op = await this.deleteMany({ - uid, - }); - - return op.deletedCount; - } - - async findNextInQueueOrExpired(expired: Date): Promise { - const now = new Date(); - - const result = await this.col.findOneAndUpdate( - { - $and: [ - { - $or: [{ sending: { $exists: false } }, { sending: { $lte: expired } }], - }, - { - $or: [{ schedule: { $exists: false } }, { schedule: { $lte: now } }], - }, - { - error: { $exists: false }, - }, - ], - }, - { - $set: { - sending: now, - }, - }, - { - sort: { - ts: 1, - }, - }, - ); - - return result.value; - } -} diff --git a/apps/meteor/app/models/server/raw/Nps.ts b/apps/meteor/app/models/server/raw/Nps.ts deleted file mode 100644 index 0703da39640f..000000000000 --- a/apps/meteor/app/models/server/raw/Nps.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { UpdateWriteOpResult } from 'mongodb'; -import { INps, NPSStatus } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -type T = INps; -export class NpsRaw extends BaseRaw { - modelIndexes(): IndexSpecification[] { - return [{ key: { status: 1, expireAt: 1 } }]; - } - - // get expired surveys still in progress - async getOpenExpiredAndStartSending(): Promise { - const today = new Date(); - - const query = { - status: NPSStatus.OPEN, - expireAt: { $lte: today }, - }; - const update = { - $set: { - status: NPSStatus.SENDING, - }, - }; - const { value } = await this.col.findOneAndUpdate(query, update, { sort: { expireAt: 1 } }); - - return value; - } - - // get expired surveys already sending results - async getOpenExpiredAlreadySending(): Promise { - const today = new Date(); - - const query = { - status: NPSStatus.SENDING, - expireAt: { $lte: today }, - }; - - return this.col.findOne(query); - } - - updateStatusById(_id: INps['_id'], status: INps['status']): Promise { - const update = { - $set: { - status, - }, - }; - return this.col.updateOne({ _id }, update); - } - - save({ - _id, - startAt, - expireAt, - createdBy, - status, - }: Pick): Promise { - return this.col.updateOne( - { - _id, - }, - { - $set: { - startAt, - _updatedAt: new Date(), - }, - $setOnInsert: { - expireAt, - createdBy, - createdAt: new Date(), - status, - }, - }, - { - upsert: true, - }, - ); - } - - closeAllByStatus(status: NPSStatus): Promise { - const query = { - status, - }; - - const update = { - $set: { - status: NPSStatus.CLOSED, - }, - }; - - return this.col.updateMany(query, update); - } -} diff --git a/apps/meteor/app/models/server/raw/NpsVote.ts b/apps/meteor/app/models/server/raw/NpsVote.ts deleted file mode 100644 index 999b96311191..000000000000 --- a/apps/meteor/app/models/server/raw/NpsVote.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { ObjectId, Cursor, FindOneOptions, UpdateWriteOpResult, WithoutProjection } from 'mongodb'; -import { INpsVote, INpsVoteStatus } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -type T = INpsVote; -export class NpsVoteRaw extends BaseRaw { - modelIndexes(): IndexSpecification[] { - return [{ key: { npsId: 1, status: 1, sentAt: 1 } }, { key: { npsId: 1, identifier: 1 }, unique: true }]; - } - - findNotSentByNpsId(npsId: string, options?: WithoutProjection>): Cursor { - const query = { - npsId, - status: INpsVoteStatus.NEW, - }; - return this.col.find(query, options).sort({ ts: 1 }).limit(1000); - } - - findByNpsIdAndStatus(npsId: string, status: INpsVoteStatus, options?: WithoutProjection>): Cursor { - const query = { - npsId, - status, - }; - return this.col.find(query, options); - } - - findByNpsId(npsId: string, options?: WithoutProjection>): Cursor { - const query = { - npsId, - }; - return this.col.find(query, options); - } - - save(vote: Omit): Promise { - const { npsId, identifier } = vote; - - const query = { - npsId, - identifier, - }; - const update = { - $set: { - ...vote, - _updatedAt: new Date(), - }, - $setOnInsert: { - _id: new ObjectId().toHexString(), - }, - }; - - return this.col.updateOne(query, update, { upsert: true }); - } - - updateVotesToSent(voteIds: string[]): Promise { - const query = { - _id: { $in: voteIds }, - }; - const update = { - $set: { - status: INpsVoteStatus.SENT, - }, - }; - return this.col.updateMany(query, update); - } - - updateOldSendingToNewByNpsId(npsId: string): Promise { - const fiveMinutes = new Date(); - fiveMinutes.setMinutes(fiveMinutes.getMinutes() - 5); - - const query = { - npsId, - status: INpsVoteStatus.SENDING, - sentAt: { $lt: fiveMinutes }, - }; - const update = { - $set: { - status: INpsVoteStatus.NEW, - }, - $unset: { - sentAt: 1 as 1, // why do you do this to me TypeScript? - }, - }; - return this.col.updateMany(query, update); - } -} diff --git a/apps/meteor/app/models/server/raw/OAuthApps.ts b/apps/meteor/app/models/server/raw/OAuthApps.ts deleted file mode 100644 index 760aae203ac8..000000000000 --- a/apps/meteor/app/models/server/raw/OAuthApps.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IOAuthApps } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -export class OAuthAppsRaw extends BaseRaw { - findOneAuthAppByIdOrClientId(props: { clientId: string } | { appId: string }): Promise { - return this.findOne({ - ...('appId' in props && { _id: props.appId }), - ...('clientId' in props && { _id: props.clientId }), - }); - } -} diff --git a/apps/meteor/app/models/server/raw/OEmbedCache.ts b/apps/meteor/app/models/server/raw/OEmbedCache.ts deleted file mode 100644 index c43b38c979d7..000000000000 --- a/apps/meteor/app/models/server/raw/OEmbedCache.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { DeleteWriteOpResultObject } from 'mongodb'; -import type { IOEmbedCache } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -type T = IOEmbedCache; - -export class OEmbedCacheRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { updatedAt: 1 } }]; - } - - async createWithIdAndData(_id: string, data: any): Promise { - const record = { - _id, - data, - updatedAt: new Date(), - }; - record._id = (await this.insertOne(record)).insertedId; - return record; - } - - removeAfterDate(date: Date): Promise { - const query = { - updatedAt: { - $lte: date, - }, - }; - return this.deleteMany(query); - } -} diff --git a/apps/meteor/app/models/server/raw/OmnichannelQueue.ts b/apps/meteor/app/models/server/raw/OmnichannelQueue.ts deleted file mode 100644 index ef79f55baca0..000000000000 --- a/apps/meteor/app/models/server/raw/OmnichannelQueue.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import type { IOmnichannelQueueStatus } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -const UNIQUE_QUEUE_ID = 'queue'; -export class OmnichannelQueueRaw extends BaseRaw { - initQueue() { - return this.col.updateOne( - { - _id: UNIQUE_QUEUE_ID, - }, - { - $unset: { - stoppedAt: 1, - }, - $set: { - startedAt: new Date(), - locked: false, - }, - }, - { - upsert: true, - }, - ); - } - - stopQueue() { - return this.col.updateOne( - { - _id: UNIQUE_QUEUE_ID, - }, - { - $set: { - stoppedAt: new Date(), - locked: false, - }, - }, - ); - } - - async lockQueue() { - const date = new Date(); - const result = await this.col.findOneAndUpdate( - { - _id: UNIQUE_QUEUE_ID, - $or: [ - { - locked: true, - lockedAt: { - $lte: new Date(date.getTime() - 5000), - }, - }, - { - locked: false, - }, - ], - }, - { - $set: { - locked: true, - // apply 5 secs lock lifetime - lockedAt: new Date(), - }, - }, - { - sort: { - _id: 1, - }, - }, - ); - - return result.value; - } - - async unlockQueue() { - const result = await this.col.findOneAndUpdate( - { - _id: UNIQUE_QUEUE_ID, - }, - { - $set: { - locked: false, - }, - $unset: { - lockedAt: 1, - }, - }, - { - sort: { - _id: 1, - }, - }, - ); - - return result.value; - } -} diff --git a/apps/meteor/app/models/server/raw/PbxEvents.ts b/apps/meteor/app/models/server/raw/PbxEvents.ts deleted file mode 100644 index 6b0c79a3e2a8..000000000000 --- a/apps/meteor/app/models/server/raw/PbxEvents.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Cursor } from 'mongodb'; -import type { IPbxEvent } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class PbxEventsRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { uniqueId: 1 }, unique: true }]; - } - - findByEvents(callUniqueId: string, events: string[]): Cursor { - return this.find( - { - $or: [ - { - callUniqueId, - }, - { - callUniqueIdFallback: callUniqueId, - }, - ], - event: { - $in: events, - }, - }, - { - sort: { - ts: 1, - }, - }, - ); - } - - findOneByEvent(callUniqueId: string, event: string): Promise { - return this.findOne({ - $or: [ - { - callUniqueId, - }, - { - callUniqueIdFallback: callUniqueId, - }, - ], - event, - }); - } - - findOneByUniqueId(callUniqueId: string): Promise { - return this.findOne({ - $or: [ - { - callUniqueId, - }, - { - callUniqueIdFallback: callUniqueId, - }, - ], - }); - } -} diff --git a/apps/meteor/app/models/server/raw/Permissions.ts b/apps/meteor/app/models/server/raw/Permissions.ts deleted file mode 100644 index c7b1d772eb68..000000000000 --- a/apps/meteor/app/models/server/raw/Permissions.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { IPermission, IRole } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -export class PermissionsRaw extends BaseRaw { - async createOrUpdate(name: string, roles: IRole['_id'][]): Promise { - const exists = await this.findOne>( - { - _id: name, - roles, - }, - { fields: { _id: 1 } }, - ); - - if (exists) { - return exists._id; - } - - return this.update({ _id: name }, { $set: { roles } }, { upsert: true }).then((result) => result.result._id); - } - - async create(id: string, roles: IRole['_id'][]): Promise { - const exists = await this.findOneById>(id, { fields: { _id: 1 } }); - - if (exists) { - return exists._id; - } - - return this.update({ _id: id }, { $set: { roles } }, { upsert: true }).then((result) => result.result._id); - } - - async addRole(permission: string, role: IRole['_id']): Promise { - await this.update({ _id: permission, roles: { $ne: role } }, { $addToSet: { roles: role } }); - } - - async setRoles(permission: string, roles: IRole['_id'][]): Promise { - await this.update({ _id: permission }, { $set: { roles } }); - } - - async removeRole(permission: string, role: IRole['_id']): Promise { - await this.update({ _id: permission, roles: role }, { $pull: { roles: role } }); - } -} diff --git a/apps/meteor/app/models/server/raw/ReadReceipts.ts b/apps/meteor/app/models/server/raw/ReadReceipts.ts deleted file mode 100644 index 95b99d35f791..000000000000 --- a/apps/meteor/app/models/server/raw/ReadReceipts.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Cursor } from 'mongodb'; -import { ReadReceipt } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class ReadReceiptsRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { roomId: 1, userId: 1, messageId: 1 }, unique: true }, { key: { messageId: 1 } }]; - } - - findByMessageId(messageId: string): Cursor { - return this.find({ messageId }); - } -} diff --git a/apps/meteor/app/models/server/raw/Reports.ts b/apps/meteor/app/models/server/raw/Reports.ts deleted file mode 100644 index 5b0e0b4a4ba6..000000000000 --- a/apps/meteor/app/models/server/raw/Reports.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { IReport, IMessage } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -export class ReportsRaw extends BaseRaw { - createWithMessageDescriptionAndUserId(message: IMessage, description: string, userId: string): ReturnType['insertOne']> { - const record: Pick = { - message, - description, - ts: new Date(), - userId, - }; - return this.insertOne(record); - } -} diff --git a/apps/meteor/app/models/server/raw/Roles.ts b/apps/meteor/app/models/server/raw/Roles.ts deleted file mode 100644 index 0d774be584fc..000000000000 --- a/apps/meteor/app/models/server/raw/Roles.ts +++ /dev/null @@ -1,262 +0,0 @@ -import type { - Collection, - Cursor, - FilterQuery, - FindOneOptions, - InsertOneWriteOpResult, - UpdateWriteOpResult, - WithId, - WithoutProjection, -} from 'mongodb'; -import type { IRole, IUser, IRoom } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; -import { SubscriptionsRaw } from './Subscriptions'; -import { UsersRaw } from './Users'; - -type ScopedModelRoles = { - Subscriptions: SubscriptionsRaw; - Users: UsersRaw; -}; - -export class RolesRaw extends BaseRaw { - constructor(public readonly col: Collection, private readonly models: ScopedModelRoles, trash?: Collection) { - super(col, trash); - } - - findByUpdatedDate(updatedAfterDate: Date, options?: FindOneOptions): Cursor { - const query = { - _updatedAt: { $gte: new Date(updatedAfterDate) }, - }; - - return options ? this.find(query, options) : this.find(query); - } - - async addUserRoles(userId: IUser['_id'], roles: IRole['_id'][], scope?: IRoom['_id']): Promise { - if (process.env.NODE_ENV === 'development' && (scope === 'Users' || scope === 'Subscriptions')) { - throw new Error('Roles.addUserRoles method received a role scope instead of a scope value.'); - } - - if (!Array.isArray(roles)) { - roles = [roles]; - process.env.NODE_ENV === 'development' && console.warn('[WARN] RolesRaw.addUserRoles: roles should be an array'); - } - - for await (const roleId of roles) { - const role = await this.findOneById>(roleId, { projection: { scope: 1 } }); - - if (!role) { - process.env.NODE_ENV === 'development' && console.warn(`[WARN] RolesRaw.addUserRoles: role: ${roleId} not found`); - continue; - } - switch (role.scope) { - case 'Subscriptions': - await this.models.Subscriptions.addRolesByUserId(userId, [role._id], scope); - break; - case 'Users': - default: - await this.models.Users.addRolesByUserId(userId, [role._id]); - } - } - return true; - } - - async isUserInRoles(userId: IUser['_id'], roles: IRole['_id'][], scope?: IRoom['_id']): Promise { - if (process.env.NODE_ENV === 'development' && (scope === 'Users' || scope === 'Subscriptions')) { - throw new Error('Roles.isUserInRoles method received a role scope instead of a scope value.'); - } - - for await (const roleId of roles) { - const role = await this.findOneById>(roleId, { projection: { scope: 1 } }); - - if (!role) { - continue; - } - - switch (role.scope) { - case 'Subscriptions': - if (await this.models.Subscriptions.isUserInRole(userId, roleId, scope)) { - return true; - } - break; - case 'Users': - default: - if (await this.models.Users.isUserInRole(userId, roleId)) { - return true; - } - } - } - return false; - } - - async removeUserRoles(userId: IUser['_id'], roles: IRole['_id'][], scope?: IRoom['_id']): Promise { - if (process.env.NODE_ENV === 'development' && (scope === 'Users' || scope === 'Subscriptions')) { - throw new Error('Roles.removeUserRoles method received a role scope instead of a scope value.'); - } - - for await (const roleId of roles) { - const role = await this.findOneById>(roleId, { projection: { scope: 1 } }); - - if (!role) { - continue; - } - - switch (role.scope) { - case 'Subscriptions': - scope && (await this.models.Subscriptions.removeRolesByUserId(userId, [roleId], scope)); - break; - case 'Users': - default: - await this.models.Users.removeRolesByUserId(userId, [roleId]); - } - } - return true; - } - - async findOneByIdOrName(_idOrName: IRole['_id'] | IRole['name'], options?: undefined): Promise; - - async findOneByIdOrName( - _idOrName: IRole['_id'] | IRole['name'], - options: WithoutProjection>, - ): Promise; - - async findOneByIdOrName

( - _idOrName: IRole['_id'] | IRole['name'], - options: FindOneOptions

, - ): Promise

; - - findOneByIdOrName

(_idOrName: IRole['_id'] | IRole['name'], options?: any): Promise { - const query: FilterQuery = { - $or: [ - { - _id: _idOrName, - }, - { - name: _idOrName, - }, - ], - }; - - return this.findOne(query, options); - } - - async findOneByName

(name: IRole['name'], options?: any): Promise { - const query: FilterQuery = { - name, - }; - - return this.findOne(query, options); - } - - findInIds

(ids: IRole['_id'][], options?: FindOneOptions): P extends Pick ? Cursor

: Cursor { - const query: FilterQuery = { - name: { - $in: ids, - }, - }; - - return this.find(query, options || {}) as P extends Pick ? Cursor

: Cursor; - } - - findAllExceptIds

(ids: IRole['_id'][], options?: FindOneOptions): P extends Pick ? Cursor

: Cursor { - const query: FilterQuery = { - _id: { - $nin: ids, - }, - }; - - return this.find(query, options || {}) as P extends Pick ? Cursor

: Cursor; - } - - updateById( - _id: IRole['_id'], - name: IRole['name'], - scope: IRole['scope'], - description: IRole['description'] = '', - mandatory2fa: IRole['mandatory2fa'] = false, - ): Promise { - const queryData = { - name, - scope, - description, - mandatory2fa, - }; - - return this.updateOne({ _id }, { $set: queryData }, { upsert: true }); - } - - findUsersInRole(roleId: IRole['_id'], scope?: IRoom['_id']): Promise>; - - findUsersInRole( - roleId: IRole['_id'], - scope: IRoom['_id'] | undefined, - options: WithoutProjection>, - ): Promise>; - - findUsersInRole

( - roleId: IRole['_id'], - scope: IRoom['_id'] | undefined, - options: FindOneOptions

, - ): Promise>; - - async findUsersInRole

( - roleId: IRole['_id'], - scope: IRoom['_id'] | undefined, - options?: any | undefined, - ): Promise | Cursor

> { - if (process.env.NODE_ENV === 'development' && (scope === 'Users' || scope === 'Subscriptions')) { - throw new Error('Roles.findUsersInRole method received a role scope instead of a scope value.'); - } - - const role = await this.findOneById>(roleId, { projection: { scope: 1 } }); - - if (!role) { - throw new Error('RolesRaw.findUsersInRole: role not found'); - } - - switch (role.scope) { - case 'Subscriptions': - return this.models.Subscriptions.findUsersInRoles([role._id], scope, options); - case 'Users': - default: - return this.models.Users.findUsersInRoles([role._id], null, options); - } - } - - createWithRandomId( - name: IRole['name'], - scope: IRole['scope'] = 'Users', - description = '', - protectedRole = true, - mandatory2fa = false, - ): Promise>> { - const role = { - name, - scope, - description, - protected: protectedRole, - mandatory2fa, - }; - - return this.insertOne(role); - } - - async canAddUserToRole(uid: IUser['_id'], roleId: IRole['_id'], scope?: IRoom['_id']): Promise { - if (process.env.NODE_ENV === 'development' && (scope === 'Users' || scope === 'Subscriptions')) { - throw new Error('Roles.canAddUserToRole method received a role scope instead of a scope value.'); - } - - const role = await this.findOne({ _id: roleId }, { fields: { scope: 1 } } as FindOneOptions); - if (!role) { - return false; - } - - switch (role.scope) { - case 'Subscriptions': - return this.models.Subscriptions.isUserInRoleScope(uid, scope); - case 'Users': - default: - return this.models.Users.isUserInRoleScope(uid); - } - } -} diff --git a/apps/meteor/app/models/server/raw/ServerEvents.ts b/apps/meteor/app/models/server/raw/ServerEvents.ts deleted file mode 100644 index e169524574d5..000000000000 --- a/apps/meteor/app/models/server/raw/ServerEvents.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { IServerEvent, ServerEventType } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class ServerEventsRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { t: 1, ip: 1, ts: -1 } }, { key: { 't': 1, 'u.username': 1, 'ts': -1 } }]; - } - - async findLastFailedAttemptByIp(ip: string): Promise { - return this.findOne( - { - ip, - t: ServerEventType.FAILED_LOGIN_ATTEMPT, - }, - { sort: { ts: -1 } }, - ); - } - - async findLastFailedAttemptByUsername(username: string): Promise { - return this.findOne( - { - 'u.username': username, - 't': ServerEventType.FAILED_LOGIN_ATTEMPT, - }, - { sort: { ts: -1 } }, - ); - } - - async countFailedAttemptsByUsernameSince(username: string, since: Date): Promise { - return this.find({ - 'u.username': username, - 't': ServerEventType.FAILED_LOGIN_ATTEMPT, - 'ts': { - $gte: since, - }, - }).count(); - } - - countFailedAttemptsByIpSince(ip: string, since: Date): Promise { - return this.find({ - ip, - t: ServerEventType.FAILED_LOGIN_ATTEMPT, - ts: { - $gte: since, - }, - }).count(); - } - - countFailedAttemptsByIp(ip: string): Promise { - return this.find({ - ip, - t: ServerEventType.FAILED_LOGIN_ATTEMPT, - }).count(); - } - - countFailedAttemptsByUsername(username: string): Promise { - return this.find({ - 'u.username': username, - 't': ServerEventType.FAILED_LOGIN_ATTEMPT, - }).count(); - } -} diff --git a/apps/meteor/app/models/server/raw/Sessions.ts b/apps/meteor/app/models/server/raw/Sessions.ts deleted file mode 100644 index fa955bc06f1a..000000000000 --- a/apps/meteor/app/models/server/raw/Sessions.ts +++ /dev/null @@ -1,1317 +0,0 @@ -import { - AggregationCursor, - BulkWriteOperation, - BulkWriteOpResultObject, - Collection, - UpdateWriteOpResult, - FilterQuery, - Cursor, -} from 'mongodb'; -import type { - ISession, - UserSessionAggregation, - DeviceSessionAggregation, - OSSessionAggregation, - UserSessionAggregationResult, - DeviceSessionAggregationResult, - OSSessionAggregationResult, - IUser, -} from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification, ModelOptionalId } from './BaseRaw'; - -type DestructuredDate = { year: number; month: number; day: number }; -type DestructuredDateWithType = { - year: number; - month: number; - day: number; - type?: 'month' | 'week'; -}; -type DestructuredRange = { start: DestructuredDate; end: DestructuredDate }; -type DateRange = { start: Date; end: Date }; - -const matchBasedOnDate = (start: DestructuredDate, end: DestructuredDate): FilterQuery => { - if (start.year === end.year && start.month === end.month) { - return { - year: start.year, - month: start.month, - day: { $gte: start.day, $lte: end.day }, - }; - } - - if (start.year === end.year) { - return { - year: start.year, - $and: [ - { - $or: [ - { - month: { $gt: start.month }, - }, - { - month: start.month, - day: { $gte: start.day }, - }, - ], - }, - { - $or: [ - { - month: { $lt: end.month }, - }, - { - month: end.month, - day: { $lte: end.day }, - }, - ], - }, - ], - }; - } - - return { - $and: [ - { - $or: [ - { - year: { $gt: start.year }, - }, - { - year: start.year, - month: { $gt: start.month }, - }, - { - year: start.year, - month: start.month, - day: { $gte: start.day }, - }, - ], - }, - { - $or: [ - { - year: { $lt: end.year }, - }, - { - year: end.year, - month: { $lt: end.month }, - }, - { - year: end.year, - month: end.month, - day: { $lte: end.day }, - }, - ], - }, - ], - }; -}; - -const getGroupSessionsByHour = ( - _id: { range: string; day: string; month: string; year: string } | string, -): { listGroup: object; countGroup: object } => { - const isOpenSession = { $not: ['$session.closedAt'] }; - const isAfterLoginAt = { $gte: ['$range', { $hour: '$session.loginAt' }] }; - const isBeforeClosedAt = { $lte: ['$range', { $hour: '$session.closedAt' }] }; - - const listGroup = { - $group: { - _id, - usersList: { - $addToSet: { - $cond: [ - { - $or: [{ $and: [isOpenSession, isAfterLoginAt] }, { $and: [isAfterLoginAt, isBeforeClosedAt] }], - }, - '$session.userId', - '$$REMOVE', - ], - }, - }, - }, - }; - - const countGroup = { - $addFields: { - users: { $size: '$usersList' }, - }, - }; - - return { listGroup, countGroup }; -}; - -const getSortByFullDate = (): { year: number; month: number; day: number } => ({ - year: -1, - month: -1, - day: -1, -}); - -const getProjectionByFullDate = (): { day: string; month: string; year: string } => ({ - day: '$_id.day', - month: '$_id.month', - year: '$_id.year', -}); - -export const aggregates = { - dailySessionsOfYesterday( - collection: Collection, - { year, month, day }: DestructuredDate, - ): AggregationCursor< - Pick & { - time: number; - sessions: number; - devices: ISession['device'][]; - _computedAt: string; - } - > { - return collection.aggregate< - Pick & { - time: number; - sessions: number; - devices: ISession['device'][]; - _computedAt: string; - } - >( - [ - { - $match: { - userId: { $exists: true }, - lastActivityAt: { $exists: true }, - device: { $exists: true }, - type: 'session', - $or: [ - { - year: { $lt: year }, - }, - { - year, - month: { $lt: month }, - }, - { - year, - month, - day: { $lte: day }, - }, - ], - }, - }, - { - $project: { - userId: 1, - device: 1, - day: 1, - month: 1, - year: 1, - mostImportantRole: 1, - time: { $trunc: { $divide: [{ $subtract: ['$lastActivityAt', '$loginAt'] }, 1000] } }, - }, - }, - { - $match: { - time: { $gt: 0 }, - }, - }, - { - $group: { - _id: { - userId: '$userId', - device: '$device', - day: '$day', - month: '$month', - year: '$year', - }, - mostImportantRole: { $first: '$mostImportantRole' }, - time: { $sum: '$time' }, - sessions: { $sum: 1 }, - }, - }, - { - $sort: { - time: -1, - }, - }, - { - $group: { - _id: { - userId: '$_id.userId', - day: '$_id.day', - month: '$_id.month', - year: '$_id.year', - }, - mostImportantRole: { $first: '$mostImportantRole' }, - time: { $sum: '$time' }, - sessions: { $sum: '$sessions' }, - devices: { - $push: { - sessions: '$sessions', - time: '$time', - device: '$_id.device', - }, - }, - }, - }, - { - $sort: { - _id: 1, - }, - }, - { - $project: { - _id: 0, - type: { $literal: 'user_daily' }, - _computedAt: { $literal: new Date() }, - day: '$_id.day', - month: '$_id.month', - year: '$_id.year', - userId: '$_id.userId', - mostImportantRole: 1, - time: 1, - sessions: 1, - devices: 1, - }, - }, - ], - { allowDiskUse: true }, - ); - }, - - async getUniqueUsersOfYesterday( - collection: Collection, - { year, month, day }: DestructuredDate, - ): Promise { - return collection - .aggregate([ - { - $match: { - year, - month, - day, - type: 'user_daily', - }, - }, - { - $group: { - _id: { - day: '$day', - month: '$month', - year: '$year', - mostImportantRole: '$mostImportantRole', - }, - count: { - $sum: 1, - }, - sessions: { - $sum: '$sessions', - }, - time: { - $sum: '$time', - }, - }, - }, - { - $group: { - _id: { - day: '$day', - month: '$month', - year: '$year', - }, - roles: { - $push: { - role: '$_id.mostImportantRole', - count: '$count', - sessions: '$sessions', - time: '$time', - }, - }, - count: { - $sum: '$count', - }, - sessions: { - $sum: '$sessions', - }, - time: { - $sum: '$time', - }, - }, - }, - { - $project: { - _id: 0, - count: 1, - sessions: 1, - time: 1, - roles: 1, - }, - }, - ]) - .toArray(); - }, - - async getUniqueUsersOfLastMonthOrWeek( - collection: Collection, - { year, month, day, type = 'month' }: DestructuredDateWithType, - ): Promise { - return collection - .aggregate( - [ - { - $match: { - type: 'user_daily', - ...aggregates.getMatchOfLastMonthOrWeek({ year, month, day, type }), - }, - }, - { - $group: { - _id: { - userId: '$userId', - }, - mostImportantRole: { $first: '$mostImportantRole' }, - sessions: { - $sum: '$sessions', - }, - time: { - $sum: '$time', - }, - }, - }, - { - $group: { - _id: { - mostImportantRole: '$mostImportantRole', - }, - count: { - $sum: 1, - }, - sessions: { - $sum: '$sessions', - }, - time: { - $sum: '$time', - }, - }, - }, - { - $sort: { - time: -1, - }, - }, - { - $group: { - _id: 1, - roles: { - $push: { - role: '$_id.mostImportantRole', - count: '$count', - sessions: '$sessions', - time: '$time', - }, - }, - count: { - $sum: '$count', - }, - sessions: { - $sum: '$sessions', - }, - time: { - $sum: '$time', - }, - }, - }, - { - $project: { - _id: 0, - count: 1, - roles: 1, - sessions: 1, - time: 1, - }, - }, - ], - { allowDiskUse: true }, - ) - .toArray(); - }, - - getMatchOfLastMonthOrWeek({ year, month, day, type = 'month' }: DestructuredDateWithType): FilterQuery { - let startOfPeriod; - - if (type === 'month') { - const pastMonthLastDay = new Date(year, month - 1, 0).getDate(); - const currMonthLastDay = new Date(year, month, 0).getDate(); - - startOfPeriod = new Date(year, month - 1, day); - startOfPeriod.setMonth( - startOfPeriod.getMonth() - 1, - (currMonthLastDay === day ? pastMonthLastDay : Math.min(pastMonthLastDay, day)) + 1, - ); - } else { - startOfPeriod = new Date(year, month - 1, day - 6); - } - - const startOfPeriodObject = { - year: startOfPeriod.getFullYear(), - month: startOfPeriod.getMonth() + 1, - day: startOfPeriod.getDate(), - }; - - if (year === startOfPeriodObject.year && month === startOfPeriodObject.month) { - return { - year, - month, - day: { $gte: startOfPeriodObject.day, $lte: day }, - }; - } - - if (year === startOfPeriodObject.year) { - return { - year, - $and: [ - { - $or: [ - { - month: { $gt: startOfPeriodObject.month }, - }, - { - month: startOfPeriodObject.month, - day: { $gte: startOfPeriodObject.day }, - }, - ], - }, - { - $or: [ - { - month: { $lt: month }, - }, - { - month, - day: { $lte: day }, - }, - ], - }, - ], - }; - } - - return { - $and: [ - { - $or: [ - { - year: { $gt: startOfPeriodObject.year }, - }, - { - year: startOfPeriodObject.year, - month: { $gt: startOfPeriodObject.month }, - }, - { - year: startOfPeriodObject.year, - month: startOfPeriodObject.month, - day: { $gte: startOfPeriodObject.day }, - }, - ], - }, - { - $or: [ - { - year: { $lt: year }, - }, - { - year, - month: { $lt: month }, - }, - { - year, - month, - day: { $lte: day }, - }, - ], - }, - ], - }; - }, - - async getUniqueDevicesOfLastMonthOrWeek( - collection: Collection, - { year, month, day, type = 'month' }: DestructuredDateWithType, - ): Promise { - return collection - .aggregate( - [ - { - $match: { - type: 'user_daily', - ...aggregates.getMatchOfLastMonthOrWeek({ year, month, day, type }), - }, - }, - { - $unwind: '$devices', - }, - { - $group: { - _id: { - type: '$devices.device.type', - name: '$devices.device.name', - version: '$devices.device.version', - }, - count: { - $sum: '$devices.sessions', - }, - time: { - $sum: '$devices.time', - }, - }, - }, - { - $sort: { - time: -1, - }, - }, - { - $project: { - _id: 0, - type: '$_id.type', - name: '$_id.name', - version: '$_id.version', - count: 1, - time: 1, - }, - }, - ], - { allowDiskUse: true }, - ) - .toArray(); - }, - - getUniqueDevicesOfYesterday( - collection: Collection, - { year, month, day }: DestructuredDate, - ): Promise { - return collection - .aggregate([ - { - $match: { - year, - month, - day, - type: 'user_daily', - }, - }, - { - $unwind: '$devices', - }, - { - $group: { - _id: { - type: '$devices.device.type', - name: '$devices.device.name', - version: '$devices.device.version', - }, - count: { - $sum: '$devices.sessions', - }, - time: { - $sum: '$devices.time', - }, - }, - }, - { - $sort: { - time: -1, - }, - }, - { - $project: { - _id: 0, - type: '$_id.type', - name: '$_id.name', - version: '$_id.version', - count: 1, - time: 1, - }, - }, - ]) - .toArray(); - }, - - getUniqueOSOfLastMonthOrWeek( - collection: Collection, - { year, month, day, type = 'month' }: DestructuredDateWithType, - ): Promise { - return collection - .aggregate( - [ - { - $match: { - 'type': 'user_daily', - 'devices.device.os.name': { - $exists: true, - }, - ...aggregates.getMatchOfLastMonthOrWeek({ year, month, day, type }), - }, - }, - { - $unwind: '$devices', - }, - { - $group: { - _id: { - name: '$devices.device.os.name', - version: '$devices.device.os.version', - }, - count: { - $sum: '$devices.sessions', - }, - time: { - $sum: '$devices.time', - }, - }, - }, - { - $sort: { - time: -1, - }, - }, - { - $project: { - _id: 0, - name: '$_id.name', - version: '$_id.version', - count: 1, - time: 1, - }, - }, - ], - { allowDiskUse: true }, - ) - .toArray(); - }, - - getUniqueOSOfYesterday(collection: Collection, { year, month, day }: DestructuredDate): Promise { - return collection - .aggregate([ - { - $match: { - year, - month, - day, - 'type': 'user_daily', - 'devices.device.os.name': { - $exists: true, - }, - }, - }, - { - $unwind: '$devices', - }, - { - $group: { - _id: { - name: '$devices.device.os.name', - version: '$devices.device.os.version', - }, - count: { - $sum: '$devices.sessions', - }, - time: { - $sum: '$devices.time', - }, - }, - }, - { - $sort: { - time: -1, - }, - }, - { - $project: { - _id: 0, - name: '$_id.name', - version: '$_id.version', - count: 1, - time: 1, - }, - }, - ]) - .toArray(); - }, -}; - -export class SessionsRaw extends BaseRaw { - private secondaryCollection: Collection; - - constructor(public readonly col: Collection, public readonly colSecondary: Collection, trash?: Collection) { - super(col, trash); - - this.secondaryCollection = colSecondary; - } - - protected modelIndexes(): IndexSpecification[] { - return [ - { key: { instanceId: 1, sessionId: 1, year: 1, month: 1, day: 1 } }, - { key: { instanceId: 1, sessionId: 1, userId: 1 } }, - { key: { instanceId: 1, sessionId: 1 } }, - { key: { sessionId: 1 } }, - { key: { userId: 1 } }, - { key: { year: 1, month: 1, day: 1, type: 1 } }, - { key: { type: 1 } }, - { key: { ip: 1, loginAt: 1 } }, - { key: { _computedAt: 1 }, expireAfterSeconds: 60 * 60 * 24 * 45 }, - ]; - } - - async getActiveUsersBetweenDates({ start, end }: DestructuredRange): Promise { - return this.col - .aggregate([ - { - $match: { - ...matchBasedOnDate(start, end), - type: 'user_daily', - }, - }, - { - $group: { - _id: '$userId', - }, - }, - ]) - .toArray(); - } - - async findLastLoginByIp(ip: string): Promise { - return this.findOne( - { - ip, - }, - { - sort: { loginAt: -1 }, - limit: 1, - }, - ); - } - - findOneBySessionId(sessionId: string): Promise { - return this.findOne({ sessionId }); - } - - findSessionsNotClosedByDateWithoutLastActivity({ year, month, day }: DestructuredDate): Cursor { - const query = { - year, - month, - day, - type: 'session', - closedAt: { $exists: false }, - lastActivityAt: { $exists: false }, - }; - - return this.find(query); - } - - async getActiveUsersOfPeriodByDayBetweenDates({ start, end }: DestructuredRange): Promise< - { - day: number; - month: number; - year: number; - usersList: IUser['_id'][]; - users: number; - }[] - > { - return this.col - .aggregate<{ - day: number; - month: number; - year: number; - usersList: IUser['_id'][]; - users: number; - }>([ - { - $match: { - ...matchBasedOnDate(start, end), - type: 'user_daily', - mostImportantRole: { $ne: 'anonymous' }, - }, - }, - { - $group: { - _id: { - day: '$day', - month: '$month', - year: '$year', - userId: '$userId', - }, - }, - }, - { - $group: { - _id: { - day: '$_id.day', - month: '$_id.month', - year: '$_id.year', - }, - usersList: { - $addToSet: '$_id.userId', - }, - users: { $sum: 1 }, - }, - }, - { - $project: { - _id: 0, - ...getProjectionByFullDate(), - usersList: 1, - users: 1, - }, - }, - { - $sort: { - ...getSortByFullDate(), - }, - }, - ]) - .toArray(); - } - - async getBusiestTimeWithinHoursPeriod({ start, end, groupSize }: DateRange & { groupSize: number }): Promise< - { - hour: number; - users: number; - }[] - > { - const match = { - $match: { - type: 'computed-session', - loginAt: { $gte: start, $lte: end }, - }, - }; - const rangeProject = { - $project: { - range: { - $range: [0, 24, groupSize], - }, - session: '$$ROOT', - }, - }; - const unwind = { - $unwind: '$range', - }; - const groups = getGroupSessionsByHour('$range'); - const presentationProject = { - $project: { - _id: 0, - hour: '$_id', - users: 1, - }, - }; - const sort = { - $sort: { - hour: -1, - }, - }; - return this.col - .aggregate<{ - hour: number; - users: number; - }>([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort]) - .toArray(); - } - - async getTotalOfSessionsByDayBetweenDates({ start, end }: DestructuredRange): Promise< - { - day: number; - month: number; - year: number; - users: number; - }[] - > { - return this.col - .aggregate<{ - day: number; - month: number; - year: number; - users: number; - }>([ - { - $match: { - ...matchBasedOnDate(start, end), - type: 'user_daily', - mostImportantRole: { $ne: 'anonymous' }, - }, - }, - { - $group: { - _id: { year: '$year', month: '$month', day: '$day' }, - users: { $sum: 1 }, - }, - }, - { - $project: { - _id: 0, - ...getProjectionByFullDate(), - users: 1, - }, - }, - { - $sort: { - ...getSortByFullDate(), - }, - }, - ]) - .toArray(); - } - - async getTotalOfSessionByHourAndDayBetweenDates({ start, end }: DateRange): Promise< - { - hour: number; - day: number; - month: number; - year: number; - users: number; - }[] - > { - const match = { - $match: { - type: 'computed-session', - loginAt: { $gte: start, $lte: end }, - }, - }; - const rangeProject = { - $project: { - range: { - $range: [{ $hour: '$loginAt' }, { $sum: [{ $ifNull: [{ $hour: '$closedAt' }, 23] }, 1] }], - }, - session: '$$ROOT', - }, - }; - const unwind = { - $unwind: '$range', - }; - const groups = getGroupSessionsByHour({ - range: '$range', - day: '$session.day', - month: '$session.month', - year: '$session.year', - }); - const presentationProject = { - $project: { - _id: 0, - hour: '$_id.range', - ...getProjectionByFullDate(), - users: 1, - }, - }; - const sort = { - $sort: { - ...getSortByFullDate(), - hour: -1, - }, - }; - - return this.col - .aggregate<{ - hour: number; - day: number; - month: number; - year: number; - users: number; - }>([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort]) - .toArray(); - } - - async getUniqueUsersOfYesterday(): Promise { - const date = new Date(); - date.setDate(date.getDate() - 1); - - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - - return { - year, - month, - day, - data: await aggregates.getUniqueUsersOfYesterday(this.secondaryCollection, { - year, - month, - day, - }), - }; - } - - async getUniqueUsersOfLastMonth(): Promise { - const date = new Date(); - date.setDate(date.getDate() - 1); - - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - - return { - year, - month, - day, - data: await aggregates.getUniqueUsersOfLastMonthOrWeek(this.secondaryCollection, { - year, - month, - day, - }), - }; - } - - async getUniqueUsersOfLastWeek(): Promise { - const date = new Date(); - date.setDate(date.getDate() - 1); - - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - - return { - year, - month, - day, - data: await aggregates.getUniqueUsersOfLastMonthOrWeek(this.secondaryCollection, { - year, - month, - day, - type: 'week', - }), - }; - } - - async getUniqueDevicesOfYesterday(): Promise { - const date = new Date(); - date.setDate(date.getDate() - 1); - - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - - return { - year, - month, - day, - data: await aggregates.getUniqueDevicesOfYesterday(this.secondaryCollection, { - year, - month, - day, - }), - }; - } - - async getUniqueDevicesOfLastMonth(): Promise { - const date = new Date(); - date.setDate(date.getDate() - 1); - - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - - return { - year, - month, - day, - data: await aggregates.getUniqueDevicesOfLastMonthOrWeek(this.secondaryCollection, { - year, - month, - day, - }), - }; - } - - async getUniqueDevicesOfLastWeek(): Promise { - const date = new Date(); - date.setDate(date.getDate() - 1); - - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - - return { - year, - month, - day, - data: await aggregates.getUniqueDevicesOfLastMonthOrWeek(this.secondaryCollection, { - year, - month, - day, - type: 'week', - }), - }; - } - - async getUniqueOSOfYesterday(): Promise { - const date = new Date(); - date.setDate(date.getDate() - 1); - - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - - return { - year, - month, - day, - data: await aggregates.getUniqueOSOfYesterday(this.secondaryCollection, { year, month, day }), - }; - } - - async getUniqueOSOfLastMonth(): Promise { - const date = new Date(); - date.setDate(date.getDate() - 1); - - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - - return { - year, - month, - day, - data: await aggregates.getUniqueOSOfLastMonthOrWeek(this.secondaryCollection, { - year, - month, - day, - }), - }; - } - - async getUniqueOSOfLastWeek(): Promise { - const date = new Date(); - date.setDate(date.getDate() - 1); - - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - - return { - year, - month, - day, - data: await aggregates.getUniqueOSOfLastMonthOrWeek(this.secondaryCollection, { - year, - month, - day, - type: 'week', - }), - }; - } - - async createOrUpdate(data: Omit): Promise { - const { year, month, day, sessionId, instanceId } = data; - - if (!year || !month || !day || !sessionId || !instanceId) { - return; - } - - const now = new Date(); - - return this.updateOne( - { instanceId, sessionId, year, month, day }, - { - $set: data, - $setOnInsert: { - createdAt: now, - }, - }, - { upsert: true }, - ); - } - - async closeByInstanceIdAndSessionId(instanceId: string, sessionId: string): Promise { - const query = { - instanceId, - sessionId, - closedAt: { $exists: false }, - }; - - const closeTime = new Date(); - const update = { - $set: { - closedAt: closeTime, - lastActivityAt: closeTime, - }, - }; - - return this.updateOne(query, update); - } - - async updateActiveSessionsByDateAndInstanceIdAndIds( - { year, month, day }: Partial = {}, - instanceId: string, - sessions: string[], - data = {}, - ): Promise { - const query = { - instanceId, - year, - month, - day, - sessionId: { $in: sessions }, - closedAt: { $exists: false }, - }; - - const update = { - $set: data, - }; - - return this.updateMany(query, update); - } - - async updateActiveSessionsByDate({ year, month, day }: DestructuredDate, data = {}): Promise { - const query = { - year, - month, - day, - type: 'session', - closedAt: { $exists: false }, - lastActivityAt: { $exists: false }, - }; - - const update = { - $set: data, - }; - - return this.updateMany(query, update); - } - - async logoutByInstanceIdAndSessionIdAndUserId(instanceId: string, sessionId: string, userId: string): Promise { - const query = { - instanceId, - sessionId, - userId, - logoutAt: { $exists: 0 }, - }; - - const logoutAt = new Date(); - const update = { - $set: { - logoutAt, - }, - }; - - return this.updateMany(query, update); - } - - async createBatch(sessions: ModelOptionalId[]): Promise { - if (!sessions || sessions.length === 0) { - return; - } - - const ops: BulkWriteOperation[] = []; - sessions.forEach((doc) => { - const { year, month, day, sessionId, instanceId } = doc; - delete doc._id; - - ops.push({ - updateOne: { - filter: { year, month, day, sessionId, instanceId }, - update: { - $set: doc, - }, - upsert: true, - }, - }); - }); - - return this.col.bulkWrite(ops, { ordered: false }); - } -} diff --git a/apps/meteor/app/models/server/raw/Settings.ts b/apps/meteor/app/models/server/raw/Settings.ts deleted file mode 100644 index 90b047798767..000000000000 --- a/apps/meteor/app/models/server/raw/Settings.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { Cursor, FilterQuery, UpdateQuery, WriteOpResult } from 'mongodb'; -import type { ISetting, ISettingColor, ISettingSelectOption } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -export class SettingsRaw extends BaseRaw { - async getValueById(_id: string): Promise { - const setting = await this.findOne>({ _id }, { projection: { value: 1 } }); - - return setting?.value; - } - - findNotHidden({ updatedAfter }: { updatedAfter?: Date } = {}): Cursor { - const query: FilterQuery = { - hidden: { $ne: true }, - }; - - if (updatedAfter) { - query._updatedAt = { $gt: updatedAfter }; - } - - return this.find(query); - } - - findOneNotHiddenById(_id: string): Promise { - const query = { - _id, - hidden: { $ne: true }, - }; - - return this.findOne(query); - } - - findByIds(_id: string[] | string = []): Cursor { - if (typeof _id === 'string') { - _id = [_id]; - } - - const query = { - _id: { - $in: _id, - }, - }; - - return this.find(query); - } - - updateValueById(_id: string, value: T): Promise { - const query = { - blocked: { $ne: true }, - value: { $ne: value }, - _id, - }; - - const update = { - $set: { - value, - }, - }; - - return this.update(query, update); - } - - updateOptionsById(_id: ISetting['_id'], options: UpdateQuery['$set']): Promise { - const query = { - blocked: { $ne: true }, - _id, - }; - - const update = { $set: options }; - - return this.update(query, update); - } - - updateValueNotHiddenById(_id: ISetting['_id'], value: T): Promise { - const query = { - _id, - hidden: { $ne: true }, - blocked: { $ne: true }, - }; - - const update = { - $set: { - value, - }, - }; - - return this.update(query, update); - } - - updateValueAndEditorById( - _id: ISetting['_id'], - value: T, - editor: ISettingColor['editor'], - ): Promise { - const query = { - blocked: { $ne: true }, - value: { $ne: value }, - _id, - }; - - const update = { - $set: { - value, - editor, - }, - }; - - return this.update(query, update); - } - - findNotHiddenPublic( - ids: ISetting['_id'][] = [], - ): Cursor< - T extends ISettingColor - ? Pick - : Pick - > { - const filter: FilterQuery = { - hidden: { $ne: true }, - public: true, - }; - - if (ids.length > 0) { - filter._id = { $in: ids }; - } - - return this.find(filter, { - projection: { - _id: 1, - value: 1, - editor: 1, - enterprise: 1, - invalidValue: 1, - modules: 1, - requiredOnWizard: 1, - }, - }); - } - - findSetupWizardSettings(): Cursor { - return this.find({ wizard: { $exists: true } }); - } - - addOptionValueById(_id: ISetting['_id'], option: ISettingSelectOption): Promise { - const query = { - blocked: { $ne: true }, - _id, - }; - - const { key, i18nLabel } = option; - const update = { - $addToSet: { - values: { - key, - i18nLabel, - }, - }, - }; - - return this.update(query, update); - } - - findNotHiddenPublicUpdatedAfter(updatedAt: Date): Cursor { - const filter = { - hidden: { $ne: true }, - public: true, - _updatedAt: { - $gt: updatedAt, - }, - }; - - return this.find(filter, { - projection: { - _id: 1, - value: 1, - editor: 1, - enterprise: 1, - invalidValue: 1, - modules: 1, - requiredOnWizard: 1, - }, - }); - } -} diff --git a/apps/meteor/app/models/server/raw/SmarshHistory.ts b/apps/meteor/app/models/server/raw/SmarshHistory.ts deleted file mode 100644 index 8cb860993e96..000000000000 --- a/apps/meteor/app/models/server/raw/SmarshHistory.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { ISmarshHistory } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -type T = ISmarshHistory; - -export class SmarshHistoryRaw extends BaseRaw {} diff --git a/apps/meteor/app/models/server/raw/Statistics.ts b/apps/meteor/app/models/server/raw/Statistics.ts deleted file mode 100644 index 70768a77ce46..000000000000 --- a/apps/meteor/app/models/server/raw/Statistics.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { IStats } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class StatisticsRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { createdAt: -1 } }]; - } - - async findLast(): Promise { - const options = { - sort: { - createdAt: -1, - }, - limit: 1, - }; - const records = await this.find({}, options).toArray(); - return records?.[0]; - } -} diff --git a/apps/meteor/app/models/server/raw/Subscriptions.ts b/apps/meteor/app/models/server/raw/Subscriptions.ts deleted file mode 100644 index be58d90b50dd..000000000000 --- a/apps/meteor/app/models/server/raw/Subscriptions.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { FindOneOptions, Cursor, UpdateQuery, FilterQuery, UpdateWriteOpResult, Collection, WithoutProjection } from 'mongodb'; -import { compact } from 'lodash'; -import type { ISubscription, IRole, IUser, IRoom } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; -import { UsersRaw } from './Users'; - -type T = ISubscription; -export class SubscriptionsRaw extends BaseRaw { - constructor(public readonly col: Collection, private readonly models: { Users: UsersRaw }, trash?: Collection) { - super(col, trash); - } - - async getBadgeCount(uid: string): Promise { - const [result] = await this.col - .aggregate<{ total: number } | undefined>([ - { $match: { 'u._id': uid, 'archived': { $ne: true } } }, - { - $group: { - _id: 'total', - total: { $sum: '$unread' }, - }, - }, - ]) - .toArray(); - - return result?.total || 0; - } - - findOneByRoomIdAndUserId(rid: string, uid: string, options: FindOneOptions = {}): Promise { - const query = { - rid, - 'u._id': uid, - }; - - return this.findOne(query, options); - } - - findByUserIdAndRoomIds(userId: string, roomIds: Array, options: FindOneOptions = {}): Cursor { - const query = { - 'u._id': userId, - 'rid': { - $in: roomIds, - }, - }; - - return this.find(query, options); - } - - findByRoomIdAndNotUserId(roomId: string, userId: string, options: FindOneOptions = {}): Cursor { - const query = { - 'rid': roomId, - 'u._id': { - $ne: userId, - }, - }; - - return this.find(query, options); - } - - findByLivechatRoomIdAndNotUserId(roomId: string, userId: string, options: FindOneOptions = {}): Cursor { - const query = { - 'rid': roomId, - 'servedBy._id': { - $ne: userId, - }, - }; - - return this.find(query, options); - } - - countByRoomIdAndUserId(rid: string, uid: string | undefined): Promise { - const query = { - rid, - 'u._id': uid, - }; - - const cursor = this.find(query, { projection: { _id: 0 } }); - - return cursor.count(); - } - - async isUserInRole(uid: IUser['_id'], roleId: IRole['_id'], rid?: IRoom['_id']): Promise { - if (rid == null) { - return null; - } - - const query = { - 'u._id': uid, - rid, - 'roles': roleId, - }; - - return this.findOne(query, { projection: { roles: 1 } }); - } - - setAsReadByRoomIdAndUserId(rid: string, uid: string, alert = false, options: FindOneOptions = {}): ReturnType['update']> { - const query: FilterQuery = { - rid, - 'u._id': uid, - }; - - const update: UpdateQuery = { - $set: { - open: true, - alert, - unread: 0, - userMentions: 0, - groupMentions: 0, - ls: new Date(), - }, - }; - - return this.update(query, update, options); - } - - removeRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][], rid: IRoom['_id']): Promise { - const query = { - 'u._id': uid, - rid, - }; - - const update = { - $pullAll: { - roles, - }, - }; - - return this.updateOne(query, update); - } - - findUsersInRoles(roles: IRole['_id'][], rid: string | undefined): Promise>; - - findUsersInRoles( - roles: IRole['_id'][], - rid: string | undefined, - options: WithoutProjection>, - ): Promise>; - - findUsersInRoles

( - roles: IRole['_id'][], - rid: string | undefined, - options: FindOneOptions

, - ): Promise>; - - async findUsersInRoles

( - roles: IRole['_id'][], - rid: IRoom['_id'] | undefined, - options?: FindOneOptions

, - ): Promise> { - const query = { - roles: { $in: roles }, - ...(rid && { rid }), - }; - - const subscriptions = await this.find(query).toArray(); - - const users = compact(subscriptions.map((subscription) => subscription.u?._id).filter(Boolean)); - - return !options - ? this.models.Users.find({ _id: { $in: users } }) - : this.models.Users.find({ _id: { $in: users } } as FilterQuery, options); - } - - addRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][], rid?: IRoom['_id']): Promise { - if (!Array.isArray(roles)) { - roles = [roles]; - process.env.NODE_ENV === 'development' && console.warn('[WARN] Subscriptions.addRolesByUserId: roles should be an array'); - } - - const query = { - 'u._id': uid, - rid, - }; - - const update = { - $addToSet: { - roles: { $each: roles }, - }, - }; - - return this.updateOne(query, update); - } - - async isUserInRoleScope(uid: IUser['_id'], rid?: IRoom['_id']): Promise { - const query = { - 'u._id': uid, - rid, - }; - - if (!rid) { - return false; - } - const options = { - fields: { _id: 1 }, - }; - - const found = await this.findOne(query, options); - return !!found; - } -} diff --git a/apps/meteor/app/models/server/raw/Team.ts b/apps/meteor/app/models/server/raw/Team.ts deleted file mode 100644 index 385dcea5deb4..000000000000 --- a/apps/meteor/app/models/server/raw/Team.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { WithoutProjection, FindOneOptions, Cursor, UpdateWriteOpResult, DeleteWriteOpResultObject, FilterQuery } from 'mongodb'; -import { ITeam, TEAM_TYPE } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class TeamRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { name: 1 }, unique: true }]; - } - - findByNames(names: Array): Cursor; - - findByNames(names: Array, options: WithoutProjection>): Cursor; - - findByNames

(names: Array, options: FindOneOptions

): Cursor

; - - findByNames

( - names: Array, - options?: undefined | WithoutProjection> | FindOneOptions

, - ): Cursor

| Cursor { - if (options === undefined) { - return this.col.find({ name: { $in: names } }); - } - return this.col.find({ name: { $in: names } }, options); - } - - findByIds(ids: Array, query?: FilterQuery): Cursor; - - findByIds(ids: Array, options: WithoutProjection>, query?: FilterQuery): Cursor; - - findByIds

(ids: Array, options: FindOneOptions

, query?: FilterQuery): Cursor

; - - findByIds

( - ids: Array, - options?: undefined | WithoutProjection> | FindOneOptions

, - query?: FilterQuery, - ): Cursor

| Cursor { - if (options === undefined) { - return this.col.find({ _id: { $in: ids }, ...query }); - } - - return this.col.find({ _id: { $in: ids }, ...query }, options); - } - - findByIdsAndType(ids: Array, type: TEAM_TYPE): Cursor; - - findByIdsAndType(ids: Array, type: TEAM_TYPE, options: WithoutProjection>): Cursor; - - findByIdsAndType

(ids: Array, type: TEAM_TYPE, options: FindOneOptions

): Cursor

; - - findByIdsAndType

( - ids: Array, - type: TEAM_TYPE, - options?: undefined | WithoutProjection> | FindOneOptions

, - ): Cursor

| Cursor { - if (options === undefined) { - return this.col.find({ _id: { $in: ids }, type }); - } - return this.col.find({ _id: { $in: ids }, type }, options); - } - - findByType(type: number): Cursor; - - findByType(type: number, options: WithoutProjection>): Cursor; - - findByType

(type: number, options: FindOneOptions

): Cursor

; - - findByType

( - type: number, - options?: undefined | WithoutProjection> | FindOneOptions

, - ): Cursor | Cursor

{ - if (options === undefined) { - return this.col.find({ type }, options); - } - return this.col.find({ type }, options); - } - - findByNameAndTeamIds(name: string | RegExp, teamIds: Array): Cursor; - - findByNameAndTeamIds(name: string | RegExp, teamIds: Array, options: WithoutProjection>): Cursor; - - findByNameAndTeamIds

(name: string | RegExp, teamIds: Array, options: FindOneOptions

): Cursor

; - - findByNameAndTeamIds

( - name: string | RegExp, - teamIds: Array, - options?: undefined | WithoutProjection> | FindOneOptions

, - ): Cursor

| Cursor { - if (options === undefined) { - return this.col.find({ - name, - $or: [ - { - type: 0, - }, - { - _id: { - $in: teamIds, - }, - }, - ], - }); - } - return this.col.find( - { - name, - $or: [ - { - type: 0, - }, - { - _id: { - $in: teamIds, - }, - }, - ], - }, - options, - ); - } - - findOneByName(name: string | RegExp): Promise; - - findOneByName(name: string | RegExp, options: WithoutProjection>): Promise; - - findOneByName

(name: string | RegExp, options: FindOneOptions

): Promise

; - - findOneByName

( - name: string | RegExp, - options?: undefined | WithoutProjection> | FindOneOptions

, - ): Promise

| Promise { - if (options === undefined) { - return this.col.findOne({ name }); - } - return this.col.findOne({ name }, options); - } - - findOneByMainRoomId(roomId: string): Promise; - - findOneByMainRoomId(roomId: string, options: WithoutProjection>): Promise; - - findOneByMainRoomId

(roomId: string, options: FindOneOptions

): Promise

; - - findOneByMainRoomId

( - roomId: string, - options?: undefined | WithoutProjection> | FindOneOptions

, - ): Promise

| Promise { - return options ? this.col.findOne({ roomId }, options) : this.col.findOne({ roomId }); - } - - updateMainRoomForTeam(id: string, roomId: string): Promise { - return this.updateOne( - { - _id: id, - }, - { - $set: { - roomId, - }, - }, - ); - } - - deleteOneById(id: string): Promise { - return this.col.deleteOne({ - _id: id, - }); - } - - deleteOneByName(name: string): Promise { - return this.col.deleteOne({ name }); - } - - updateNameAndType(teamId: string, nameAndType: { name?: string; type?: TEAM_TYPE }): Promise { - const query = { - _id: teamId, - }; - - const update = { - $set: {}, - }; - - if (nameAndType.name) { - Object.assign(update.$set, { name: nameAndType.name }); - } - - if (typeof nameAndType.type !== 'undefined') { - Object.assign(update.$set, { type: nameAndType.type }); - } - - return this.updateOne(query, update); - } -} diff --git a/apps/meteor/app/models/server/raw/TeamMember.ts b/apps/meteor/app/models/server/raw/TeamMember.ts deleted file mode 100644 index d2ac14c03c7f..000000000000 --- a/apps/meteor/app/models/server/raw/TeamMember.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { - WithoutProjection, - FindOneOptions, - Cursor, - InsertOneWriteOpResult, - UpdateWriteOpResult, - DeleteWriteOpResultObject, - FilterQuery, -} from 'mongodb'; -import type { ITeamMember, IUser, IRole } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -type T = ITeamMember; -export class TeamMemberRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [ - { - key: { teamId: 1 }, - }, - { - key: { teamId: 1, userId: 1 }, - unique: true, - }, - ]; - } - - findByUserId(userId: string): Cursor; - - findByUserId(userId: string, options: WithoutProjection>): Cursor; - - findByUserId

(userId: string, options: FindOneOptions

): Cursor

; - - findByUserId

( - userId: string, - options?: undefined | WithoutProjection> | FindOneOptions

, - ): Cursor

| Cursor { - return options ? this.col.find({ userId }, options) : this.col.find({ userId }, options); - } - - findOneByUserIdAndTeamId(userId: string, teamId: string): Promise; - - findOneByUserIdAndTeamId( - userId: string, - teamId: string, - options: WithoutProjection>, - ): Promise; - - findOneByUserIdAndTeamId

(userId: string, teamId: string, options: FindOneOptions

): Promise

; - - findOneByUserIdAndTeamId

( - userId: string, - teamId: string, - options?: undefined | WithoutProjection> | FindOneOptions

, - ): Promise

{ - return options ? this.col.findOne({ userId, teamId }, options) : this.col.findOne({ userId, teamId }, options); - } - - findByTeamId(teamId: string): Cursor; - - findByTeamId(teamId: string, options: WithoutProjection>): Cursor; - - findByTeamId

(teamId: string, options: FindOneOptions

): Cursor

; - - findByTeamId

( - teamId: string, - options?: undefined | WithoutProjection> | FindOneOptions

, - ): Cursor

| Cursor { - return options ? this.col.find({ teamId }, options) : this.col.find({ teamId }, options); - } - - findByTeamIds(teamIds: Array): Cursor; - - findByTeamIds(teamIds: Array, options: WithoutProjection>): Cursor; - - findByTeamIds

(teamIds: Array, options: FindOneOptions

): Cursor

; - - findByTeamIds

( - teamIds: Array, - options?: undefined | WithoutProjection> | FindOneOptions

, - ): Cursor

| Cursor { - return options ? this.col.find({ teamId: { $in: teamIds } }, options) : this.col.find({ teamId: { $in: teamIds } }, options); - } - - findByTeamIdAndRole(teamId: string, role: IRole['_id']): Cursor; - - findByTeamIdAndRole(teamId: string, role: IRole['_id'], options: WithoutProjection>): Cursor; - - findByTeamIdAndRole

(teamId: string, role: IRole['_id'], options: FindOneOptions

): Cursor

; - - findByTeamIdAndRole

( - teamId: string, - role: IRole['_id'], - options?: undefined | WithoutProjection> | FindOneOptions

, - ): Cursor

| Cursor { - return options ? this.col.find({ teamId, roles: role }, options) : this.col.find({ teamId, roles: role }); - } - - findByUserIdAndTeamIds(userId: string, teamIds: Array, options: FindOneOptions = {}): Cursor { - const query = { - userId, - teamId: { - $in: teamIds, - }, - }; - - return this.col.find(query, options); - } - - findMembersInfoByTeamId(teamId: string, limit: number, skip: number, query?: FilterQuery): Cursor { - return this.col.find({ ...query, teamId }, { - limit, - skip, - projection: { - userId: 1, - roles: 1, - createdBy: 1, - createdAt: 1, - }, - } as FindOneOptions); - } - - updateOneByUserIdAndTeamId(userId: string, teamId: string, update: Partial): Promise { - return this.updateOne({ userId, teamId }, { $set: update }); - } - - createOneByTeamIdAndUserId( - teamId: string, - userId: string, - createdBy: Pick, - ): Promise> { - return this.insertOne({ - teamId, - userId, - createdAt: new Date(), - createdBy, - }); - } - - updateRolesByTeamIdAndUserId(teamId: string, userId: string, roles: Array): Promise { - return this.updateOne( - { - teamId, - userId, - }, - { - $addToSet: { - roles: { $each: roles }, - }, - }, - ); - } - - removeRolesByTeamIdAndUserId(teamId: string, userId: string, roles: Array): Promise { - return this.updateOne( - { - teamId, - userId, - }, - { - $pull: { - roles: { $in: roles }, - }, - }, - ); - } - - deleteByUserIdAndTeamId(userId: string, teamId: string): Promise { - return this.col.deleteOne({ - teamId, - userId, - }); - } - - deleteByTeamId(teamId: string): Promise { - return this.col.deleteMany({ - teamId, - }); - } -} diff --git a/apps/meteor/app/models/server/raw/Uploads.ts b/apps/meteor/app/models/server/raw/Uploads.ts deleted file mode 100644 index f9f227277240..000000000000 --- a/apps/meteor/app/models/server/raw/Uploads.ts +++ /dev/null @@ -1,129 +0,0 @@ -// TODO: Lib imports should not exists inside the raw models -import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { - CollectionInsertOneOptions, - Cursor, - DeleteWriteOpResultObject, - FilterQuery, - InsertOneWriteOpResult, - UpdateOneOptions, - UpdateQuery, - UpdateWriteOpResult, - WithId, - WriteOpResult, -} from 'mongodb'; -import { IUpload as T } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification, InsertionModel } from './BaseRaw'; - -const fillTypeGroup = (fileData: Partial): void => { - if (!fileData.type) { - return; - } - - fileData.typeGroup = fileData.type.split('/').shift(); -}; - -export class UploadsRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { rid: 1 } }, { key: { uploadedAt: 1 } }, { key: { typeGroup: 1 } }]; - } - - findNotHiddenFilesOfRoom(roomId: string, searchText: string, fileType: string, limit: number): Cursor { - const fileQuery = { - rid: roomId, - complete: true, - uploading: false, - _hidden: { - $ne: true, - }, - - ...(searchText && { name: { $regex: new RegExp(escapeRegExp(searchText), 'i') } }), - ...(fileType && fileType !== 'all' && { typeGroup: fileType }), - }; - - const fileOptions = { - limit, - sort: { - uploadedAt: -1, - }, - projection: { - _id: 1, - userId: 1, - rid: 1, - name: 1, - description: 1, - type: 1, - url: 1, - uploadedAt: 1, - typeGroup: 1, - }, - }; - - return this.find(fileQuery, fileOptions); - } - - insert(fileData: InsertionModel, options?: CollectionInsertOneOptions): Promise>> { - fillTypeGroup(fileData); - return super.insertOne(fileData, options); - } - - update( - filter: FilterQuery, - update: UpdateQuery | Partial, - options?: UpdateOneOptions & { multi?: boolean }, - ): Promise { - if ('$set' in update && update.$set) { - fillTypeGroup(update.$set); - } else if ('type' in update && update.type) { - fillTypeGroup(update); - } - - return super.update(filter, update, options); - } - - async insertFileInit(userId: string, store: string, file: { name: string }, extra: object): Promise>> { - const fileData = { - userId, - store, - complete: false, - uploading: true, - progress: 0, - extension: file.name.split('.').pop(), - uploadedAt: new Date(), - ...file, - ...extra, - }; - - fillTypeGroup(fileData); - return this.insert(fileData); - } - - async updateFileComplete(fileId: string, userId: string, file: object): Promise { - if (!fileId) { - return; - } - - const filter = { - _id: fileId, - userId, - }; - - const update = { - $set: { - complete: true, - uploading: false, - progress: 1, - }, - }; - - update.$set = Object.assign(file, update.$set); - - fillTypeGroup(update.$set); - return this.updateOne(filter, update); - } - - async deleteFile(fileId: string): Promise { - return this.deleteOne({ _id: fileId }); - } -} diff --git a/apps/meteor/app/models/server/raw/UserDataFiles.ts b/apps/meteor/app/models/server/raw/UserDataFiles.ts deleted file mode 100644 index 3e441346ca0f..000000000000 --- a/apps/meteor/app/models/server/raw/UserDataFiles.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { FindOneOptions, InsertOneWriteOpResult, WithId, WithoutProjection } from 'mongodb'; -import { IUserDataFile as T } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -export class UserDataFilesRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { userId: 1 } }]; - } - - findLastFileByUser(userId: string, options: WithoutProjection> = {}): Promise { - const query = { - userId, - }; - - options.sort = { _updatedAt: -1 }; - return this.findOne(query, options); - } - - // INSERT - create(data: T): Promise>> { - const userDataFile = { - createdAt: new Date(), - ...data, - }; - - return this.insertOne(userDataFile); - } -} diff --git a/apps/meteor/app/models/server/raw/UsersSessions.ts b/apps/meteor/app/models/server/raw/UsersSessions.ts deleted file mode 100644 index f91616f83c9f..000000000000 --- a/apps/meteor/app/models/server/raw/UsersSessions.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { IUserSession } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -export class UsersSessionsRaw extends BaseRaw { - clearConnectionsFromInstanceId(instanceId: string[]): ReturnType['updateMany']> { - return this.col.updateMany( - {}, - { - $pull: { - connections: { - instanceId: { - $nin: instanceId, - }, - }, - }, - }, - ); - } -} diff --git a/apps/meteor/app/models/server/raw/VoipRooms.ts b/apps/meteor/app/models/server/raw/VoipRooms.ts deleted file mode 100644 index b552cae20c38..000000000000 --- a/apps/meteor/app/models/server/raw/VoipRooms.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { FilterQuery, WithoutProjection, FindOneOptions, WriteOpResult, Cursor } from 'mongodb'; -import type { IVoipRoom, IRoomClosingInfo } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; -import { Logger } from '../../../../server/lib/logger/Logger'; - -export class VoipRoomsRaw extends BaseRaw { - logger = new Logger('VoipRoomsRaw'); - - async findOneOpenByVisitorToken(visitorToken: string, options: FindOneOptions = {}): Promise { - const query: FilterQuery = { - 't': 'v', - 'open': true, - 'v.token': visitorToken, - }; - return this.findOne(query, options); - } - - findOpenByAgentId(agentId: string): Cursor { - return this.find({ - 't': 'v', - 'open': true, - 'servedBy._id': agentId, - }); - } - - async findOneByAgentId(agentId: string): Promise { - return this.findOne({ - 't': 'v', - 'open': true, - 'servedBy._id': agentId, - }); - } - - async findOneVoipRoomById(id: string, options: WithoutProjection> = {}): Promise { - const query: FilterQuery = { - t: 'v', - _id: id, - }; - return this.findOne(query, options); - } - - async findOneOpenByRoomIdAndVisitorToken( - roomId: string, - visitorToken: string, - options: FindOneOptions = {}, - ): Promise { - const query: FilterQuery = { - 't': 'v', - '_id': roomId, - 'open': true, - 'v.token': visitorToken, - }; - return this.findOne(query, options); - } - - async findOneByVisitorToken(visitorToken: string, options: FindOneOptions = {}): Promise { - const query: FilterQuery = { - 't': 'v', - 'v.token': visitorToken, - }; - return this.findOne(query, options); - } - - async findOneByIdAndVisitorToken( - _id: IVoipRoom['_id'], - visitorToken: string, - options: FindOneOptions = {}, - ): Promise { - const query: FilterQuery = { - 't': 'v', - _id, - 'v.token': visitorToken, - }; - return this.findOne(query, options); - } - - closeByRoomId(roomId: IVoipRoom['_id'], closeInfo: IRoomClosingInfo): Promise { - const { closer, closedBy, closedAt, callDuration, serviceTimeDuration, ...extraData } = closeInfo; - - return this.update( - { - _id: roomId, - t: 'v', - }, - { - $set: { - closer, - closedBy, - closedAt, - callDuration, - 'metrics.serviceTimeDuration': serviceTimeDuration, - 'v.status': 'offline', - ...extraData, - }, - $unset: { - open: 1, - }, - }, - ); - } - - findRoomsWithCriteria({ - agents, - open, - createdAt, - closedAt, - tags, - queue, - visitorId, - options = {}, - }: { - agents?: string[]; - open?: boolean; - createdAt?: { start?: string; end?: string }; - closedAt?: { start?: string; end?: string }; - tags?: string[]; - queue?: string; - visitorId?: string; - options?: { - sort?: Record; - count?: number; - fields?: Record; - offset?: number; - }; - }): Cursor { - const query: FilterQuery = { - t: 'v', - }; - - if (agents) { - query.$or = [{ 'servedBy._id': { $in: agents } }, { 'servedBy.username': { $in: agents } }]; - } - if (open !== undefined) { - query.open = { $exists: open }; - } - if (visitorId && visitorId !== 'undefined') { - query['v._id'] = visitorId; - } - if (createdAt && Object.keys(createdAt).length) { - query.ts = {}; - if (createdAt.start) { - query.ts.$gte = new Date(createdAt.start); - } - if (createdAt.end) { - query.ts.$lte = new Date(createdAt.end); - } - } - if (closedAt && Object.keys(closedAt).length) { - query.closedAt = {}; - if (closedAt.start) { - query.closedAt.$gte = new Date(closedAt.start); - } - if (closedAt.end) { - query.closedAt.$lte = new Date(closedAt.end); - } - } - if (tags) { - query.tags = { $in: tags }; - } - if (queue) { - query.queue = queue; - } - - return this.find(query, { - sort: options.sort || { name: 1 }, - skip: options.offset, - limit: options.count, - }); - } -} diff --git a/apps/meteor/app/models/server/raw/WebdavAccounts.ts b/apps/meteor/app/models/server/raw/WebdavAccounts.ts deleted file mode 100644 index 1e6d635711db..000000000000 --- a/apps/meteor/app/models/server/raw/WebdavAccounts.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Webdav Accounts model - */ -import type { FindOneOptions, Cursor, DeleteWriteOpResultObject } from 'mongodb'; -import type { IWebdavAccount } from '@rocket.chat/core-typings'; - -import { BaseRaw, IndexSpecification } from './BaseRaw'; - -type T = IWebdavAccount; - -export class WebdavAccountsRaw extends BaseRaw { - protected modelIndexes(): IndexSpecification[] { - return [{ key: { userId: 1 } }]; - } - - findOneByIdAndUserId(_id: string, userId: string, options: FindOneOptions): Promise { - return this.findOne({ _id, userId }, options); - } - - findOneByUserIdServerUrlAndUsername( - { - userId, - serverURL, - username, - }: { - userId: string; - serverURL: string; - username: string; - }, - options: FindOneOptions, - ): Promise { - return this.findOne({ userId, serverURL, username }, options); - } - - findWithUserId(userId: string, options: FindOneOptions): Cursor { - const query = { userId }; - return this.find(query, options); - } - - removeByUserAndId(_id: string, userId: string): Promise { - return this.deleteOne({ _id, userId }); - } -} diff --git a/apps/meteor/app/models/server/raw/_Users.d.ts b/apps/meteor/app/models/server/raw/_Users.d.ts deleted file mode 100644 index 573117e868fb..000000000000 --- a/apps/meteor/app/models/server/raw/_Users.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { UpdateWriteOpResult } from 'mongodb'; -import type { IRole, IUser } from '@rocket.chat/core-typings'; - -import { BaseRaw } from './BaseRaw'; - -export interface IUserRaw extends BaseRaw { - isUserInRole(uid: IUser['_id'], roleId: IRole['_id']): Promise; - removeRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][]): Promise; - findUsersInRoles(roles: IRole['_id'][]): Promise; - addRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][]): Promise; - isUserInRoleScope(uid: IUser['_id']): Promise; - new (...args: any): IUser; -} -export const UsersRaw: IUserRaw; diff --git a/apps/meteor/app/models/server/raw/index.ts b/apps/meteor/app/models/server/raw/index.ts deleted file mode 100644 index 5213d78ffcf3..000000000000 --- a/apps/meteor/app/models/server/raw/index.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { MongoInternals } from 'meteor/mongo'; - -import { AvatarsRaw } from './Avatars'; -import { AnalyticsRaw } from './Analytics'; -import { api } from '../../../../server/sdk/api'; -import { BaseDbWatch, trash } from '../models/_BaseDb'; -import { CredentialTokensRaw } from './CredentialTokens'; -import { CustomSoundsRaw } from './CustomSounds'; -import { CustomUserStatusRaw } from './CustomUserStatus'; -import { EmailInboxRaw } from './EmailInbox'; -import { EmailMessageHistoryRaw } from './EmailMessageHistory'; -import { EmojiCustomRaw } from './EmojiCustom'; -import { ExportOperationsRaw } from './ExportOperations'; -import { FederationKeysRaw } from './FederationKeys'; -import { FederationServersRaw } from './FederationServers'; -import { ImportDataRaw } from './ImportData'; -import { initWatchers } from '../../../../server/modules/watchers/watchers.module'; -import { InstanceStatusRaw } from './InstanceStatus'; -import { IntegrationHistoryRaw } from './IntegrationHistory'; -import { IntegrationsRaw } from './Integrations'; -import { InvitesRaw } from './Invites'; -import { LivechatAgentActivityRaw } from './LivechatAgentActivity'; -import { LivechatBusinessHoursRaw } from './LivechatBusinessHours'; -import { LivechatCustomFieldRaw } from './LivechatCustomField'; -import { LivechatDepartmentAgentsRaw } from './LivechatDepartmentAgents'; -import { LivechatDepartmentRaw } from './LivechatDepartment'; -import { LivechatInquiryRaw } from './LivechatInquiry'; -import { LivechatRoomsRaw } from './LivechatRooms'; -import { LivechatTriggerRaw } from './LivechatTrigger'; -import { LivechatVisitorsRaw } from './LivechatVisitors'; -import { LoginServiceConfigurationRaw } from './LoginServiceConfiguration'; -import { MessagesRaw } from './Messages'; -import { NotificationQueueRaw } from './NotificationQueue'; -import { OAuthAppsRaw } from './OAuthApps'; -import { OEmbedCacheRaw } from './OEmbedCache'; -import { OmnichannelQueueRaw } from './OmnichannelQueue'; -import { PermissionsRaw } from './Permissions'; -import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; -import { ReadReceiptsRaw } from './ReadReceipts'; -import { ReportsRaw } from './Reports'; -import { RolesRaw } from './Roles'; -import { RoomsRaw } from './Rooms'; -import { ServerEventsRaw } from './ServerEvents'; -import { SessionsRaw } from './Sessions'; -import { SettingsRaw } from './Settings'; -import { SmarshHistoryRaw } from './SmarshHistory'; -import { StatisticsRaw } from './Statistics'; -import { SubscriptionsRaw } from './Subscriptions'; -import { UsersRaw } from './Users'; -import { UsersSessionsRaw } from './UsersSessions'; -import { UserDataFilesRaw } from './UserDataFiles'; -import { UploadsRaw } from './Uploads'; -import { WebdavAccountsRaw } from './WebdavAccounts'; -import { VoipRoomsRaw } from './VoipRooms'; -import ImportDataModel from '../models/ImportData'; -import LivechatBusinessHoursModel from '../models/LivechatBusinessHours'; -import LivechatCustomFieldModel from '../models/LivechatCustomField'; -import LivechatDepartmentAgentsModel from '../models/LivechatDepartmentAgents'; -import LivechatDepartmentModel from '../models/LivechatDepartment'; -import LivechatInquiryModel from '../models/LivechatInquiry'; -import LivechatRoomsModel from '../models/LivechatRooms'; -import LivechatVisitorsModel from '../models/LivechatVisitors'; -import MessagesModel from '../models/Messages'; -import OmnichannelQueueModel from '../models/OmnichannelQueue'; -import RoomsModel from '../models/Rooms'; -import SettingsModel from '../models/Settings'; -import SubscriptionsModel from '../models/Subscriptions'; -import UsersModel from '../models/Users'; -import { PbxEventsRaw } from './PbxEvents'; -import { isRunningMs } from '../../../../server/lib/isRunningMs'; - -const trashCollection = trash.rawCollection(); - -export const Users = new UsersRaw(UsersModel.model.rawCollection(), trashCollection); -export const Subscriptions = new SubscriptionsRaw(SubscriptionsModel.model.rawCollection(), { Users }, trashCollection); -export const Settings = new SettingsRaw(SettingsModel.model.rawCollection(), trashCollection); -export const Rooms = new RoomsRaw(RoomsModel.model.rawCollection(), trashCollection); -export const LivechatCustomField = new LivechatCustomFieldRaw(LivechatCustomFieldModel.model.rawCollection(), trashCollection); -export const LivechatDepartment = new LivechatDepartmentRaw(LivechatDepartmentModel.model.rawCollection(), trashCollection); -export const LivechatDepartmentAgents = new LivechatDepartmentAgentsRaw( - LivechatDepartmentAgentsModel.model.rawCollection(), - trashCollection, -); -export const LivechatRooms = new LivechatRoomsRaw(LivechatRoomsModel.model.rawCollection(), trashCollection); -export const Messages = new MessagesRaw(MessagesModel.model.rawCollection(), trashCollection); -export const LivechatVisitors = new LivechatVisitorsRaw(LivechatVisitorsModel.model.rawCollection(), trashCollection); -export const LivechatInquiry = new LivechatInquiryRaw(LivechatInquiryModel.model.rawCollection(), trashCollection); -export const LivechatBusinessHours = new LivechatBusinessHoursRaw(LivechatBusinessHoursModel.model.rawCollection(), trashCollection); -// export const Roles = new RolesRaw(RolesModel.model.rawCollection(), { Users, Subscriptions }, trashCollection); -export const OmnichannelQueue = new OmnichannelQueueRaw(OmnichannelQueueModel.model.rawCollection(), trashCollection); -export const ImportData = new ImportDataRaw(ImportDataModel.model.rawCollection(), trashCollection); - -const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; -const prefix = 'rocketchat_'; - -export const Avatars = new AvatarsRaw(db.collection(`${prefix}avatars`), trashCollection); -export const Analytics = new AnalyticsRaw( - db.collection(`${prefix}analytics`, { readPreference: readSecondaryPreferred(db) }), - trashCollection, -); -export const CustomSounds = new CustomSoundsRaw(db.collection(`${prefix}custom_sounds`), trashCollection); -export const CustomUserStatus = new CustomUserStatusRaw(db.collection(`${prefix}custom_user_status`), trashCollection); -export const CredentialTokens = new CredentialTokensRaw(db.collection(`${prefix}credential_tokens`), trashCollection); -export const EmailInbox = new EmailInboxRaw(db.collection(`${prefix}email_inbox`), trashCollection); -export const EmailMessageHistory = new EmailMessageHistoryRaw(db.collection(`${prefix}email_message_history`), trashCollection); -export const EmojiCustom = new EmojiCustomRaw(db.collection(`${prefix}custom_emoji`), trashCollection); -export const ExportOperations = new ExportOperationsRaw(db.collection(`${prefix}export_operations`), trashCollection); -export const FederationKeys = new FederationKeysRaw(db.collection(`${prefix}federation_keys`), trashCollection); -export const FederationServers = new FederationServersRaw(db.collection(`${prefix}federation_servers`), trashCollection); -export const InstanceStatus = new InstanceStatusRaw(db.collection('instances'), trashCollection, { - preventSetUpdatedAt: true, -}); -export const Integrations = new IntegrationsRaw(db.collection(`${prefix}integrations`), trashCollection); -export const IntegrationHistory = new IntegrationHistoryRaw(db.collection(`${prefix}integration_history`), trashCollection); -export const Invites = new InvitesRaw(db.collection(`${prefix}invites`), trashCollection); -export const LivechatTrigger = new LivechatTriggerRaw(db.collection(`${prefix}livechat_trigger`), trashCollection); -export const LoginServiceConfiguration = new LoginServiceConfigurationRaw( - db.collection('meteor_accounts_loginServiceConfiguration'), - trashCollection, - { preventSetUpdatedAt: true }, -); - -export const NotificationQueue = new NotificationQueueRaw(db.collection(`${prefix}notification_queue`), trashCollection); -export const OAuthApps = new OAuthAppsRaw(db.collection(`${prefix}oauth_apps`), trashCollection); -export const OEmbedCache = new OEmbedCacheRaw(db.collection(`${prefix}oembed_cache`), trashCollection); -export const Permissions = new PermissionsRaw(db.collection(`${prefix}permissions`), trashCollection); -export const ReadReceipts = new ReadReceiptsRaw(db.collection(`${prefix}read_receipts`), trashCollection); -export const Reports = new ReportsRaw(db.collection(`${prefix}reports`), trashCollection); -export const ServerEvents = new ServerEventsRaw(db.collection(`${prefix}server_events`), trashCollection); -export const Sessions = new SessionsRaw( - db.collection(`${prefix}sessions`), - db.collection(`${prefix}sessions`, { readPreference: readSecondaryPreferred(db) }), - trashCollection, -); -export const Roles = new RolesRaw(db.collection(`${prefix}roles`), { Users, Subscriptions }, trashCollection); -export const SmarshHistory = new SmarshHistoryRaw(db.collection(`${prefix}smarsh_history`), trashCollection); -export const Statistics = new StatisticsRaw(db.collection(`${prefix}statistics`), trashCollection); -export const UsersSessions = new UsersSessionsRaw(db.collection('usersSessions'), trashCollection, { - preventSetUpdatedAt: true, -}); -export const UserDataFiles = new UserDataFilesRaw(db.collection(`${prefix}user_data_files`), trashCollection); -export const Uploads = new UploadsRaw(db.collection(`${prefix}uploads`), trashCollection); -export const WebdavAccounts = new WebdavAccountsRaw(db.collection(`${prefix}webdav_accounts`), trashCollection); -export const VoipRoom = new VoipRoomsRaw(db.collection(`${prefix}room`), trashCollection); -export const PbxEvent = new PbxEventsRaw(db.collection('pbx_events'), trashCollection); -export const LivechatAgentActivity = new LivechatAgentActivityRaw(db.collection(`${prefix}livechat_agent_activity`), trashCollection); - -const map = { - [Messages.col.collectionName]: MessagesModel, - [Users.col.collectionName]: UsersModel, - [Subscriptions.col.collectionName]: SubscriptionsModel, - [Settings.col.collectionName]: SettingsModel, - [LivechatInquiry.col.collectionName]: LivechatInquiryModel, - [LivechatDepartmentAgents.col.collectionName]: LivechatDepartmentAgentsModel, - [Rooms.col.collectionName]: RoomsModel, -}; - -if (!isRunningMs()) { - const models = { - Messages, - Users, - Subscriptions, - Settings, - LivechatInquiry, - LivechatDepartmentAgents, - UsersSessions, - Permissions, - Roles, - Rooms, - LoginServiceConfiguration, - InstanceStatus, - IntegrationHistory, - Integrations, - EmailInbox, - PbxEvent, - }; - - initWatchers(models, api.broadcastLocal.bind(api), (model, fn) => { - const meteorModel = map[model.col.collectionName] || new BaseDbWatch(model.col.collectionName); - if (!meteorModel) { - return; - } - - meteorModel.on('change', fn); - }); -} diff --git a/apps/meteor/app/notification-queue/server/NotificationQueue.ts b/apps/meteor/app/notification-queue/server/NotificationQueue.ts index b6c17a60c1ab..d46f261f0143 100644 --- a/apps/meteor/app/notification-queue/server/NotificationQueue.ts +++ b/apps/meteor/app/notification-queue/server/NotificationQueue.ts @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { INotification, INotificationItemPush, INotificationItemEmail, NotificationItem } from '@rocket.chat/core-typings'; import type { IUser } from '@rocket.chat/core-typings'; +import { NotificationQueue, Users } from '@rocket.chat/models'; -import { NotificationQueue, Users } from '../../models/server/raw'; import { sendEmailFromData } from '../../lib/server/functions/notifications/email'; import { PushNotification } from '../../push-notifications/server'; import { SystemLogger } from '../../../server/lib/logger/system'; @@ -83,7 +83,7 @@ class NotificationClass { NotificationQueue.removeById(notification._id); } catch (e) { SystemLogger.error(e); - await NotificationQueue.setErrorById(notification._id, e.message); + await NotificationQueue.setErrorById(notification._id, e instanceof Error ? e.message : String(e)); } if (counter >= this.maxBatchSize) { @@ -92,7 +92,7 @@ class NotificationClass { this.worker(counter++); } - getNextNotification(): Promise { + getNextNotification(): Promise { const expired = new Date(); expired.setMinutes(expired.getMinutes() - 5); diff --git a/apps/meteor/app/notifications/server/lib/Notifications.ts b/apps/meteor/app/notifications/server/lib/Notifications.ts index f1f6d46ab13f..67260293cdbe 100644 --- a/apps/meteor/app/notifications/server/lib/Notifications.ts +++ b/apps/meteor/app/notifications/server/lib/Notifications.ts @@ -4,12 +4,6 @@ import { DDPCommon } from 'meteor/ddp-common'; import { NotificationsModule } from '../../../../server/modules/notifications/notifications.module'; import { Streamer } from '../../../../server/modules/streamer/streamer.module'; import { api } from '../../../../server/sdk/api'; -import { - Subscriptions as SubscriptionsRaw, - Rooms as RoomsRaw, - Users as UsersRaw, - Settings as SettingsRaw, -} from '../../../models/server/raw'; import './Presence'; export class Stream extends Streamer { @@ -35,12 +29,7 @@ export class Stream extends Streamer { const notifications = new NotificationsModule(Stream); -notifications.configure({ - Rooms: RoomsRaw, - Subscriptions: SubscriptionsRaw, - Users: UsersRaw, - Settings: SettingsRaw, -}); +notifications.configure(); notifications.streamLocal.on('broadcast', ({ eventName, args }) => { api.broadcastLocal(eventName, ...args); diff --git a/apps/meteor/app/oauth2-server-config/client/oauth/oauth2-client.js b/apps/meteor/app/oauth2-server-config/client/oauth/oauth2-client.js index 47e5f0e958d9..05edccb9972d 100644 --- a/apps/meteor/app/oauth2-server-config/client/oauth/oauth2-client.js +++ b/apps/meteor/app/oauth2-server-config/client/oauth/oauth2-client.js @@ -7,7 +7,7 @@ import { APIClient } from '../../../utils/client'; Template.authorize.onCreated(async function () { this.oauthApp = new ReactiveVar({}); - const { oauthApp } = await APIClient.v1.get(`oauth-apps.get?clientId=${this.data.client_id()}`); + const { oauthApp } = await APIClient.get(`/v1/oauth-apps.get`, { clientId: this.data.client_id() }); this.oauthApp.set(oauthApp); }); diff --git a/apps/meteor/app/oauth2-server-config/server/admin/methods/addOAuthApp.js b/apps/meteor/app/oauth2-server-config/server/admin/methods/addOAuthApp.js index fecb7b41b666..d330da92ae32 100644 --- a/apps/meteor/app/oauth2-server-config/server/admin/methods/addOAuthApp.js +++ b/apps/meteor/app/oauth2-server-config/server/admin/methods/addOAuthApp.js @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import _ from 'underscore'; +import { OAuthApps } from '@rocket.chat/models'; import { hasPermission } from '../../../../authorization'; import { Users } from '../../../../models/server'; -import { OAuthApps } from '../../../../models/server/raw'; import { parseUriList } from '../functions/parseUriList'; Meteor.methods({ diff --git a/apps/meteor/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.js b/apps/meteor/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.js index 4bd24819b12f..7af1a14c3d59 100644 --- a/apps/meteor/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.js +++ b/apps/meteor/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; +import { OAuthApps } from '@rocket.chat/models'; import { hasPermission } from '../../../../authorization'; -import { OAuthApps } from '../../../../models/server/raw'; Meteor.methods({ async deleteOAuthApp(applicationId) { diff --git a/apps/meteor/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js b/apps/meteor/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js index 79838a227ab7..74742d07ad1c 100644 --- a/apps/meteor/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js +++ b/apps/meteor/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; +import { OAuthApps } from '@rocket.chat/models'; import { hasPermission } from '../../../../authorization'; -import { OAuthApps } from '../../../../models/server/raw'; import { Users } from '../../../../models/server'; import { parseUriList } from '../functions/parseUriList'; diff --git a/apps/meteor/app/oauth2-server-config/server/oauth/default-services.ts b/apps/meteor/app/oauth2-server-config/server/oauth/default-services.ts index 77277b7ddc3b..68d05ef7fa72 100644 --- a/apps/meteor/app/oauth2-server-config/server/oauth/default-services.ts +++ b/apps/meteor/app/oauth2-server-config/server/oauth/default-services.ts @@ -1,4 +1,4 @@ -import { OAuthApps } from '../../../models/server/raw'; +import { OAuthApps } from '@rocket.chat/models'; async function run(): Promise { if (!(await OAuthApps.findOneById('zapier'))) { diff --git a/apps/meteor/app/oauth2-server-config/server/oauth/oauth2-server.ts b/apps/meteor/app/oauth2-server-config/server/oauth/oauth2-server.ts index 03b058c29895..23b72cc9dd91 100644 --- a/apps/meteor/app/oauth2-server-config/server/oauth/oauth2-server.ts +++ b/apps/meteor/app/oauth2-server-config/server/oauth/oauth2-server.ts @@ -4,9 +4,10 @@ import { Mongo } from 'meteor/mongo'; import { WebApp } from 'meteor/webapp'; import { OAuth2Server } from 'meteor/rocketchat:oauth2-server'; import { Request, Response } from 'express'; +import { IUser } from '@rocket.chat/core-typings'; +import { OAuthApps } from '@rocket.chat/models'; import { Users } from '../../../models/server'; -import { OAuthApps } from '../../../models/server/raw'; import { API } from '../../../api/server'; const oauth2server = new OAuth2Server({ @@ -26,6 +27,29 @@ function getAccessToken(accessToken: string): any { }); } +export function oAuth2ServerAuth(partialRequest: { + headers: Record; + query: Record; +}): { user: IUser } | undefined { + const headerToken = partialRequest.headers.authorization?.replace('Bearer ', ''); + const queryToken = partialRequest.query.access_token; + + const accessToken = getAccessToken(headerToken || queryToken); + + // If there is no token available or the token has expired, return undefined + if (!accessToken || (accessToken.expires != null && accessToken.expires !== 0 && accessToken.expires < new Date())) { + return; + } + + const user = Users.findOne(accessToken.userId); + + if (user == null) { + return; + } + + return { user }; +} + oauth2server.app.disable('x-powered-by'); oauth2server.routes.disable('x-powered-by'); @@ -58,33 +82,5 @@ oauth2server.routes.get('/oauth/userinfo', function (req: Request, res: Response }); API.v1.addAuthMethod(function () { - const headerToken = this.request.headers.authorization; - const getToken = this.request.query.access_token; - - let token: string | undefined; - if (headerToken != null) { - const matches = headerToken.match(/Bearer\s(\S+)/); - if (matches) { - token = matches[1]; - } else { - token = undefined; - } - } - const bearerToken = token || getToken; - if (bearerToken == null) { - return; - } - - const accessToken = getAccessToken(bearerToken); - if (accessToken == null) { - return; - } - if (accessToken.expires != null && accessToken.expires !== 0 && accessToken.expires < new Date()) { - return; - } - const user = Users.findOne(accessToken.userId); - if (user == null) { - return; - } - return { user }; + return oAuth2ServerAuth(this.request); }); diff --git a/apps/meteor/app/oembed/client/index.js b/apps/meteor/app/oembed/client/index.ts similarity index 100% rename from apps/meteor/app/oembed/client/index.js rename to apps/meteor/app/oembed/client/index.ts diff --git a/apps/meteor/app/oembed/server/index.js b/apps/meteor/app/oembed/server/index.ts similarity index 100% rename from apps/meteor/app/oembed/server/index.js rename to apps/meteor/app/oembed/server/index.ts diff --git a/apps/meteor/app/oembed/server/jumpToMessage.js b/apps/meteor/app/oembed/server/jumpToMessage.js deleted file mode 100644 index 8bded0996ed9..000000000000 --- a/apps/meteor/app/oembed/server/jumpToMessage.js +++ /dev/null @@ -1,89 +0,0 @@ -import URL from 'url'; -import QueryString from 'querystring'; - -import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; - -import { Messages, Rooms, Users } from '../../models/server'; -import { settings } from '../../settings/server'; -import { callbacks } from '../../../lib/callbacks'; -import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; -import { canAccessRoom } from '../../authorization/server/functions/canAccessRoom'; - -const recursiveRemove = (message, deep = 1) => { - if (message) { - if ('attachments' in message && message.attachments !== null && deep < settings.get('Message_QuoteChainLimit')) { - message.attachments.map((msg) => recursiveRemove(msg, deep + 1)); - } else { - delete message.attachments; - } - } - return message; -}; - -callbacks.add( - 'beforeSaveMessage', - (msg) => { - // if no message is present, or the message doesn't have any URL, skip - if (!msg || !msg.urls || !msg.urls.length) { - return msg; - } - - const currentUser = Users.findOneById(msg.u._id); - - msg.urls.forEach((item) => { - // if the URL is not internal, skip - if (!item.url.includes(Meteor.absoluteUrl())) { - return; - } - - const urlObj = URL.parse(item.url); - - // if the URL doesn't have query params (doesn't reference message) skip - if (!urlObj.query) { - return; - } - - const { msg: msgId } = QueryString.parse(urlObj.query); - - if (!_.isString(msgId)) { - return; - } - - const jumpToMessage = recursiveRemove(Messages.findOneById(msgId)); - if (!jumpToMessage) { - return; - } - - // validates if user can see the message - // user has to belong to the room the message was first wrote in - const room = Rooms.findOneById(jumpToMessage.rid); - const isLiveChatRoomVisitor = !!msg.token && !!room.v?.token && msg.token === room.v.token; - const canAccessRoomForUser = isLiveChatRoomVisitor || canAccessRoom(room, currentUser); - if (!canAccessRoomForUser) { - return; - } - - msg.attachments = msg.attachments || []; - const index = msg.attachments.findIndex((a) => a.message_link === item.url); - if (index > -1) { - msg.attachments.splice(index, 1); - } - - msg.attachments.push({ - text: jumpToMessage.msg, - translations: jumpToMessage.translations, - author_name: jumpToMessage.alias || jumpToMessage.u.username, - author_icon: getUserAvatarURL(jumpToMessage.u.username), - message_link: item.url, - attachments: jumpToMessage.attachments || [], - ts: jumpToMessage.ts, - }); - item.ignoreParse = true; - }); - - return msg; - }, - callbacks.priority.LOW, - 'jumpToMessage', -); diff --git a/apps/meteor/app/oembed/server/jumpToMessage.ts b/apps/meteor/app/oembed/server/jumpToMessage.ts new file mode 100644 index 000000000000..29168b70ccb3 --- /dev/null +++ b/apps/meteor/app/oembed/server/jumpToMessage.ts @@ -0,0 +1,104 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import URL from 'url'; +import QueryString from 'querystring'; + +import { Meteor } from 'meteor/meteor'; +import _ from 'underscore'; +import { ITranslatedMessage, MessageAttachment, isQuoteAttachment } from '@rocket.chat/core-typings'; + +import { Messages, Rooms, Users } from '../../models/server'; +import { settings } from '../../settings/server'; +import { callbacks } from '../../../lib/callbacks'; +import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; +import { canAccessRoom } from '../../authorization/server/functions/canAccessRoom'; + +const recursiveRemove = (attachments: MessageAttachment, deep = 1): MessageAttachment => { + if (attachments && isQuoteAttachment(attachments)) { + if (deep < settings.get('Message_QuoteChainLimit')) { + attachments.attachments?.map((msg) => recursiveRemove(msg, deep + 1)); + } else { + delete attachments.attachments; + } + } + + return attachments; +}; + +const validateAttachmentDeepness = (message: ITranslatedMessage): ITranslatedMessage => { + if (!message || !message.attachments) { + return message; + } + + message.attachments = message.attachments?.map((attachment) => recursiveRemove(attachment)); + + return message; +}; + +callbacks.add( + 'beforeSaveMessage', + (msg) => { + // if no message is present, or the message doesn't have any URL, skip + if (!msg || !msg.urls || !msg.urls.length) { + return msg; + } + + const currentUser = Users.findOneById(msg.u._id); + + msg.urls.forEach((item) => { + // if the URL is not internal, skip + if (!item.url.includes(Meteor.absoluteUrl())) { + return; + } + + const urlObj = URL.parse(item.url); + + // if the URL doesn't have query params (doesn't reference message) skip + if (!urlObj.query) { + return; + } + + const { msg: msgId } = QueryString.parse(urlObj.query); + + if (!_.isString(msgId)) { + return; + } + + const jumpToMessage = validateAttachmentDeepness(Messages.findOneById(msgId)); + if (!jumpToMessage) { + return; + } + + // validates if user can see the message + // user has to belong to the room the message was first wrote in + const room = Rooms.findOneById(jumpToMessage.rid); + const isLiveChatRoomVisitor = !!msg.token && !!room.v?.token && msg.token === room.v.token; + const canAccessRoomForUser = isLiveChatRoomVisitor || canAccessRoom(room, currentUser); + if (!canAccessRoomForUser) { + return; + } + + msg.attachments = msg.attachments || []; + // Only QuoteAttachments have "message_link" property + const index = msg.attachments.findIndex((a) => isQuoteAttachment(a) && a.message_link === item.url); + if (index > -1) { + msg.attachments.splice(index, 1); + } + + msg.attachments.push({ + text: jumpToMessage.msg, + translations: jumpToMessage.translations, + author_name: jumpToMessage.alias || jumpToMessage.u.username, + author_icon: getUserAvatarURL(jumpToMessage.u.username), + message_link: item.url, + // @ts-expect-error + attachments: jumpToMessage.attachments || [], + ts: jumpToMessage.ts, + }); + item.ignoreParse = true; + }); + + return msg; + }, + callbacks.priority.LOW, + 'jumpToMessage', +); diff --git a/apps/meteor/app/oembed/server/providers.js b/apps/meteor/app/oembed/server/providers.js deleted file mode 100644 index 1f89975e372e..000000000000 --- a/apps/meteor/app/oembed/server/providers.js +++ /dev/null @@ -1,167 +0,0 @@ -import URL from 'url'; -import QueryString from 'querystring'; - -import { camelCase } from 'change-case'; -import _ from 'underscore'; - -import { callbacks } from '../../../lib/callbacks'; -import { SystemLogger } from '../../../server/lib/logger/system'; - -class Providers { - constructor() { - this.providers = []; - } - - static getConsumerUrl(provider, url) { - const urlObj = URL.parse(provider.endPoint, true); - urlObj.query.url = url; - delete urlObj.search; - return URL.format(urlObj); - } - - registerProvider(provider) { - return this.providers.push(provider); - } - - getProviders() { - return this.providers; - } - - getProviderForUrl(url) { - return _.find(this.providers, function (provider) { - const candidate = _.find(provider.urls, function (re) { - return re.test(url); - }); - return candidate != null; - }); - } -} - -const providers = new Providers(); - -providers.registerProvider({ - urls: [new RegExp('https?://soundcloud\\.com/\\S+')], - endPoint: 'https://soundcloud.com/oembed?format=json&maxheight=150', -}); - -providers.registerProvider({ - urls: [ - new RegExp('https?://vimeo\\.com/[^/]+'), - new RegExp('https?://vimeo\\.com/channels/[^/]+/[^/]+'), - new RegExp('https://vimeo\\.com/groups/[^/]+/videos/[^/]+'), - ], - endPoint: 'https://vimeo.com/api/oembed.json?maxheight=200', -}); - -providers.registerProvider({ - urls: [new RegExp('https?://www\\.youtube\\.com/\\S+'), new RegExp('https?://youtu\\.be/\\S+')], - endPoint: 'https://www.youtube.com/oembed?maxheight=200', -}); - -providers.registerProvider({ - urls: [new RegExp('https?://www\\.rdio\\.com/\\S+'), new RegExp('https?://rd\\.io/\\S+')], - endPoint: 'https://www.rdio.com/api/oembed/?format=json&maxheight=150', -}); - -providers.registerProvider({ - urls: [new RegExp('https?://www\\.slideshare\\.net/[^/]+/[^/]+')], - endPoint: 'https://www.slideshare.net/api/oembed/2?format=json&maxheight=200', -}); - -providers.registerProvider({ - urls: [new RegExp('https?://www\\.dailymotion\\.com/video/\\S+')], - endPoint: 'https://www.dailymotion.com/services/oembed?maxheight=200', -}); - -providers.registerProvider({ - urls: [new RegExp('https?://twitter\\.com/[^/]+/status/\\S+')], - endPoint: 'https://publish.twitter.com/oembed', -}); - -providers.registerProvider({ - urls: [new RegExp('https?://(play|open)\\.spotify\\.com/(track|album|playlist|show)/\\S+')], - endPoint: 'https://open.spotify.com/oembed', -}); - -export const oembed = {}; - -oembed.providers = providers; - -callbacks.add( - 'oembed:beforeGetUrlContent', - function (data) { - if (data.parsedUrl != null) { - const url = URL.format(data.parsedUrl); - const provider = providers.getProviderForUrl(url); - if (provider != null) { - let consumerUrl = Providers.getConsumerUrl(provider, url); - consumerUrl = URL.parse(consumerUrl, true); - _.extend(data.parsedUrl, consumerUrl); - data.urlObj.port = consumerUrl.port; - data.urlObj.hostname = consumerUrl.hostname; - data.urlObj.pathname = consumerUrl.pathname; - data.urlObj.query = consumerUrl.query; - delete data.urlObj.search; - delete data.urlObj.host; - } - } - return data; - }, - callbacks.priority.MEDIUM, - 'oembed-providers-before', -); - -const cleanupOembed = (data) => { - if (!data?.meta) { - return data; - } - - // remove oembedHtml key from original meta - const { oembedHtml, ...meta } = data.meta; - - return { - ...data, - meta, - }; -}; - -callbacks.add( - 'oembed:afterParseContent', - function (data) { - if (!data || !data.url || !data.content?.body || !data.parsedUrl?.query) { - return cleanupOembed(data); - } - - let queryString = data.parsedUrl.query; - if (_.isString(data.parsedUrl.query)) { - queryString = QueryString.parse(data.parsedUrl.query); - } - - if (!queryString.url) { - return cleanupOembed(data); - } - - const { url: originalUrl } = data; - const provider = providers.getProviderForUrl(originalUrl); - if (!provider) { - return cleanupOembed(data); - } - - const { url } = queryString; - data.meta.oembedUrl = url; - - try { - const metas = JSON.parse(data.content.body); - _.each(metas, function (value, key) { - if (_.isString(value)) { - data.meta[camelCase(`oembed_${key}`)] = value; - } - }); - } catch (error) { - SystemLogger.error(error); - } - return data; - }, - callbacks.priority.MEDIUM, - 'oembed-providers-after', -); diff --git a/apps/meteor/app/oembed/server/providers.ts b/apps/meteor/app/oembed/server/providers.ts new file mode 100644 index 000000000000..fae14bfa0a9e --- /dev/null +++ b/apps/meteor/app/oembed/server/providers.ts @@ -0,0 +1,189 @@ +import URL from 'url'; +import QueryString from 'querystring'; + +import { camelCase } from 'change-case'; +import _ from 'underscore'; +import { OEmbedMeta, OEmbedUrlContent, ParsedUrl, OEmbedProvider } from '@rocket.chat/core-typings'; + +import { callbacks } from '../../../lib/callbacks'; +import { SystemLogger } from '../../../server/lib/logger/system'; + +type OEmbedExecutor = { + providers: Providers; +}; + +class Providers { + private providers: OEmbedProvider[]; + + constructor() { + this.providers = []; + } + + static getConsumerUrl(provider: OEmbedProvider, url: string): string { + const urlObj = new URL.URL(provider.endPoint); + urlObj.searchParams.set('url', url); + + return URL.format(urlObj); + } + + registerProvider(provider: OEmbedProvider): number { + return this.providers.push(provider); + } + + getProviders(): OEmbedProvider[] { + return this.providers; + } + + getProviderForUrl(url: string): OEmbedProvider | undefined { + return _.find(this.providers, function (provider) { + const candidate = _.find(provider.urls, function (re) { + return re.test(url); + }); + return candidate != null; + }); + } +} + +const providers = new Providers(); + +providers.registerProvider({ + urls: [new RegExp('https?://soundcloud\\.com/\\S+')], + endPoint: 'https://soundcloud.com/oembed?format=json&maxheight=150', +}); + +providers.registerProvider({ + urls: [ + new RegExp('https?://vimeo\\.com/[^/]+'), + new RegExp('https?://vimeo\\.com/channels/[^/]+/[^/]+'), + new RegExp('https://vimeo\\.com/groups/[^/]+/videos/[^/]+'), + ], + endPoint: 'https://vimeo.com/api/oembed.json?maxheight=200', +}); + +providers.registerProvider({ + urls: [new RegExp('https?://www\\.youtube\\.com/\\S+'), new RegExp('https?://youtu\\.be/\\S+')], + endPoint: 'https://www.youtube.com/oembed?maxheight=200', +}); + +providers.registerProvider({ + urls: [new RegExp('https?://www\\.rdio\\.com/\\S+'), new RegExp('https?://rd\\.io/\\S+')], + endPoint: 'https://www.rdio.com/api/oembed/?format=json&maxheight=150', +}); + +providers.registerProvider({ + urls: [new RegExp('https?://www\\.slideshare\\.net/[^/]+/[^/]+')], + endPoint: 'https://www.slideshare.net/api/oembed/2?format=json&maxheight=200', +}); + +providers.registerProvider({ + urls: [new RegExp('https?://www\\.dailymotion\\.com/video/\\S+')], + endPoint: 'https://www.dailymotion.com/services/oembed?maxheight=200', +}); + +providers.registerProvider({ + urls: [new RegExp('https?://twitter\\.com/[^/]+/status/\\S+')], + endPoint: 'https://publish.twitter.com/oembed', +}); + +providers.registerProvider({ + urls: [new RegExp('https?://(play|open)\\.spotify\\.com/(track|album|playlist|show)/\\S+')], + endPoint: 'https://open.spotify.com/oembed', +}); + +export const oembed: OEmbedExecutor = { + providers, +}; + +callbacks.add( + 'oembed:beforeGetUrlContent', + function (data) { + if (data.parsedUrl != null) { + const url = URL.format(data.parsedUrl); + const provider = providers.getProviderForUrl(url); + if (provider != null) { + const consumerUrl = Providers.getConsumerUrl(provider, url); + + const parsedConsumerUrl = URL.parse(consumerUrl, true); + _.extend(data.parsedUrl, parsedConsumerUrl); + + data.urlObj.port = parsedConsumerUrl.port; + data.urlObj.hostname = parsedConsumerUrl.hostname; + data.urlObj.pathname = parsedConsumerUrl.pathname; + data.urlObj.query = parsedConsumerUrl.query; + + delete data.urlObj.search; + delete data.urlObj.host; + } + } + return data; + }, + callbacks.priority.MEDIUM, + 'oembed-providers-before', +); + +const cleanupOembed = (data: { + url: string; + meta: OEmbedMeta; + headers: { [k: string]: string }; + parsedUrl: ParsedUrl; + content: OEmbedUrlContent; +}): { + url: string; + meta: Omit; + headers: { [k: string]: string }; + parsedUrl: ParsedUrl; + content: OEmbedUrlContent; +} => { + if (!data?.meta) { + return data; + } + + // remove oembedHtml key from original meta + const { oembedHtml, ...meta } = data.meta; + + return { + ...data, + meta, + }; +}; + +callbacks.add( + 'oembed:afterParseContent', + function (data) { + if (!data || !data.url || !data.content?.body || !data.parsedUrl?.query) { + return cleanupOembed(data); + } + + let queryString = data.parsedUrl.query; + if (_.isString(data.parsedUrl.query)) { + queryString = QueryString.parse(data.parsedUrl.query); + } + + if (!queryString.url) { + return cleanupOembed(data); + } + + const { url: originalUrl } = data; + const provider = providers.getProviderForUrl(originalUrl); + if (!provider) { + return cleanupOembed(data); + } + + const { url } = queryString; + data.meta.oembedUrl = url; + + try { + const metas = JSON.parse(data.content.body); + _.each(metas, function (value, key) { + if (_.isString(value)) { + data.meta[camelCase(`oembed_${key}`)] = value; + } + }); + } catch (error) { + SystemLogger.error(error); + } + return data; + }, + callbacks.priority.MEDIUM, + 'oembed-providers-after', +); diff --git a/apps/meteor/app/oembed/server/server.js b/apps/meteor/app/oembed/server/server.js deleted file mode 100644 index 84f25366a558..000000000000 --- a/apps/meteor/app/oembed/server/server.js +++ /dev/null @@ -1,310 +0,0 @@ -import URL from 'url'; -import querystring from 'querystring'; - -import { camelCase } from 'change-case'; -import _ from 'underscore'; -import iconv from 'iconv-lite'; -import ipRangeCheck from 'ip-range-check'; -import he from 'he'; -import jschardet from 'jschardet'; - -import { Messages } from '../../models/server'; -import { OEmbedCache } from '../../models/server/raw'; -import { callbacks } from '../../../lib/callbacks'; -import { settings } from '../../settings/server'; -import { isURL } from '../../utils/lib/isURL'; -import { SystemLogger } from '../../../server/lib/logger/system'; -import { Info } from '../../utils/server'; -import { fetch } from '../../../server/lib/http/fetch'; - -const OEmbed = {}; - -// Detect encoding -// Priority: -// Detected == HTTP Header > Detected == HTML meta > HTTP Header > HTML meta > Detected > Default (utf-8) -// See also: https://www.w3.org/International/questions/qa-html-encoding-declarations.en#quickanswer -const getCharset = function (contentType, body) { - let detectedCharset; - let httpHeaderCharset; - let htmlMetaCharset; - let result; - - contentType = contentType || ''; - - const binary = body.toString('binary'); - const detected = jschardet.detect(binary); - if (detected.confidence > 0.8) { - detectedCharset = detected.encoding.toLowerCase(); - } - const m1 = contentType.match(/charset=([\w\-]+)/i); - if (m1) { - httpHeaderCharset = m1[1].toLowerCase(); - } - const m2 = binary.match(/]*charset=["']?([\w\-]+)/i); - if (m2) { - htmlMetaCharset = m2[1].toLowerCase(); - } - if (detectedCharset) { - if (detectedCharset === httpHeaderCharset) { - result = httpHeaderCharset; - } else if (detectedCharset === htmlMetaCharset) { - result = htmlMetaCharset; - } - } - if (!result) { - result = httpHeaderCharset || htmlMetaCharset || detectedCharset; - } - return result || 'utf-8'; -}; - -const toUtf8 = function (contentType, body) { - return iconv.decode(body, getCharset(contentType, body)); -}; - -const getUrlContent = async function (urlObj, redirectCount = 5) { - if (_.isString(urlObj)) { - urlObj = URL.parse(urlObj); - } - - const portsProtocol = { - 80: 'http:', - 8080: 'http:', - 443: 'https:', - }; - - const parsedUrl = _.pick(urlObj, ['host', 'hash', 'pathname', 'protocol', 'port', 'query', 'search', 'hostname']); - const ignoredHosts = settings.get('API_EmbedIgnoredHosts').replace(/\s/g, '').split(',') || []; - if (ignoredHosts.includes(parsedUrl.hostname) || ipRangeCheck(parsedUrl.hostname, ignoredHosts)) { - throw new Error('invalid host'); - } - - const safePorts = settings.get('API_EmbedSafePorts').replace(/\s/g, '').split(',') || []; - - if (safePorts.length > 0 && parsedUrl.port && !safePorts.includes(parsedUrl.port)) { - throw new Error('invalid/unsafe port'); - } - - if (safePorts.length > 0 && !parsedUrl.port && !safePorts.some((port) => portsProtocol[port] === parsedUrl.protocol)) { - throw new Error('invalid/unsafe port'); - } - - const data = callbacks.run('oembed:beforeGetUrlContent', { - urlObj, - parsedUrl, - }); - if (data.attachments != null) { - return data; - } - - const url = URL.format(data.urlObj); - - const sizeLimit = 250000; - - const response = await fetch( - url, - { - compress: true, - follow: redirectCount, - headers: { - 'User-Agent': `${settings.get('API_Embed_UserAgent')} Rocket.Chat/${Info.version}`, - 'Accept-Language': settings.get('Language') || 'en', - }, - size: sizeLimit, // max size of the response body, this was not working as expected so I'm also manually verifying that on the iterator - }, - settings.get('Allow_Invalid_SelfSigned_Certs'), - ); - - let totalSize = 0; - const chunks = []; - for await (const chunk of response.body) { - totalSize += chunk.length; - chunks.push(chunk); - - if (totalSize > sizeLimit) { - SystemLogger.info({ msg: 'OEmbed request size exceeded', url }); - break; - } - } - - const buffer = Buffer.concat(chunks); - - return { - headers: Object.fromEntries(response.headers), - body: toUtf8(response.headers.get('content-type'), buffer), - parsedUrl, - statusCode: response.status, - }; -}; - -OEmbed.getUrlMeta = function (url, withFragment) { - const urlObj = URL.parse(url); - if (withFragment != null) { - const queryStringObj = querystring.parse(urlObj.query); - queryStringObj._escaped_fragment_ = ''; - urlObj.query = querystring.stringify(queryStringObj); - let path = urlObj.pathname; - if (urlObj.query != null) { - path += `?${urlObj.query}`; - urlObj.search = `?${urlObj.query}`; - } - urlObj.path = path; - } - const content = Promise.await(getUrlContent(urlObj, 5)); - - if (!content) { - return; - } - - if (content.attachments != null) { - return content; - } - - let metas = undefined; - if (content && content.body) { - metas = {}; - const escapeMeta = (name, value) => { - metas[name] = metas[name] || he.unescape(value); - return metas[name]; - }; - content.body.replace(/]*>([^<]*)<\/title>/gim, function (meta, title) { - return escapeMeta('pageTitle', title); - }); - content.body.replace(/]*(?:name|property)=[']([^']*)['][^>]*\scontent=[']([^']*)['][^>]*>/gim, function (meta, name, value) { - return escapeMeta(camelCase(name), value); - }); - content.body.replace(/]*(?:name|property)=["]([^"]*)["][^>]*\scontent=["]([^"]*)["][^>]*>/gim, function (meta, name, value) { - return escapeMeta(camelCase(name), value); - }); - content.body.replace(/]*\scontent=[']([^']*)['][^>]*(?:name|property)=[']([^']*)['][^>]*>/gim, function (meta, value, name) { - return escapeMeta(camelCase(name), value); - }); - content.body.replace(/]*\scontent=["]([^"]*)["][^>]*(?:name|property)=["]([^"]*)["][^>]*>/gim, function (meta, value, name) { - return escapeMeta(camelCase(name), value); - }); - if (metas.fragment === '!' && withFragment == null) { - return OEmbed.getUrlMeta(url, true); - } - delete metas.oembedHtml; - } - let headers = undefined; - - if (content?.headers) { - headers = {}; - const headerObj = content.headers; - Object.keys(headerObj).forEach((header) => { - headers[camelCase(header)] = headerObj[header]; - }); - } - if (content && content.statusCode !== 200) { - return; - } - return callbacks.run('oembed:afterParseContent', { - url, - meta: metas, - headers, - parsedUrl: content.parsedUrl, - content, - }); -}; - -OEmbed.getUrlMetaWithCache = async function (url, withFragment) { - const cache = await OEmbedCache.findOneById(url); - - if (cache != null) { - return cache.data; - } - const data = OEmbed.getUrlMeta(url, withFragment); - if (data != null) { - try { - await OEmbedCache.createWithIdAndData(url, data); - } catch (_error) { - SystemLogger.error({ msg: 'OEmbed duplicated record', url }); - } - return data; - } -}; - -const getRelevantHeaders = function (headersObj) { - const headers = {}; - Object.keys(headersObj).forEach((key) => { - const value = headersObj[key]; - const lowerCaseKey = key.toLowerCase(); - if ((lowerCaseKey === 'contenttype' || lowerCaseKey === 'contentlength') && value && value.trim() !== '') { - headers[key] = value; - } - }); - - if (Object.keys(headers).length > 0) { - return headers; - } -}; - -const getRelevantMetaTags = function (metaObj) { - const tags = {}; - Object.keys(metaObj).forEach((key) => { - const value = metaObj[key]; - if (/^(og|fb|twitter|oembed|msapplication).+|description|title|pageTitle$/.test(key.toLowerCase()) && value && value.trim() !== '') { - tags[key] = value; - } - }); - - if (Object.keys(tags).length > 0) { - return tags; - } -}; - -const insertMaxWidthInOembedHtml = (oembedHtml) => oembedHtml?.replace('iframe', 'iframe style="max-width: 100%;width:400px;height:225px"'); - -OEmbed.rocketUrlParser = async function (message) { - if (Array.isArray(message.urls)) { - const attachments = []; - let changed = false; - for await (const item of message.urls) { - if (item.ignoreParse === true) { - return; - } - if (!isURL(item.url)) { - return; - } - const data = await OEmbed.getUrlMetaWithCache(item.url); - if (data != null) { - if (data.attachments) { - attachments.push(...data.attachments); - return; - } - if (data.meta != null) { - item.meta = getRelevantMetaTags(data.meta); - if (item.meta && item.meta.oembedHtml) { - item.meta.oembedHtml = insertMaxWidthInOembedHtml(item.meta.oembedHtml); - } - } - if (data.headers != null) { - item.headers = getRelevantHeaders(data.headers); - } - item.parsedUrl = data.parsedUrl; - changed = true; - } - } - if (attachments.length) { - Messages.setMessageAttachments(message._id, attachments); - } - if (changed === true) { - Messages.setUrlsById(message._id, message.urls); - } - } - return message; -}; - -settings.watch('API_Embed', function (value) { - if (value) { - return callbacks.add( - 'afterSaveMessage', - (message) => Promise.await(OEmbed.rocketUrlParser(message)), - callbacks.priority.LOW, - 'API_Embed', - ); - } - return callbacks.remove('afterSaveMessage', 'API_Embed'); -}); - -export { OEmbed }; diff --git a/apps/meteor/app/oembed/server/server.ts b/apps/meteor/app/oembed/server/server.ts new file mode 100644 index 000000000000..bbc577445441 --- /dev/null +++ b/apps/meteor/app/oembed/server/server.ts @@ -0,0 +1,366 @@ +import URL from 'url'; +import querystring from 'querystring'; + +import { camelCase } from 'change-case'; +import _ from 'underscore'; +import iconv from 'iconv-lite'; +import ipRangeCheck from 'ip-range-check'; +import he from 'he'; +import jschardet from 'jschardet'; +import { + OEmbedUrlContentResult, + OEmbedUrlWithMetadata, + IMessage, + MessageAttachment, + isOEmbedUrlContentResult, + isOEmbedUrlWithMetadata, + OEmbedMeta, +} from '@rocket.chat/core-typings'; +import { OEmbedCache } from '@rocket.chat/models'; + +import { Logger } from '../../logger/server'; +import { Messages } from '../../models/server'; +import { callbacks } from '../../../lib/callbacks'; +import { settings } from '../../settings/server'; +import { isURL } from '../../../lib/utils/isURL'; +import { Info } from '../../utils/server'; +import { fetch } from '../../../server/lib/http/fetch'; + +const log = new Logger('OEmbed'); +// Detect encoding +// Priority: +// Detected == HTTP Header > Detected == HTML meta > HTTP Header > HTML meta > Detected > Default (utf-8) +// See also: https://www.w3.org/International/questions/qa-html-encoding-declarations.en#quickanswer +const getCharset = function (contentType: string, body: Buffer): string { + let detectedCharset; + let httpHeaderCharset; + let htmlMetaCharset; + let result; + + contentType = contentType || ''; + + const binary = body.toString('binary'); + const detected = jschardet.detect(binary); + if (detected.confidence > 0.8) { + detectedCharset = detected.encoding.toLowerCase(); + } + const m1 = contentType.match(/charset=([\w\-]+)/i); + if (m1) { + httpHeaderCharset = m1[1].toLowerCase(); + } + const m2 = binary.match(/]*charset=["']?([\w\-]+)/i); + if (m2) { + htmlMetaCharset = m2[1].toLowerCase(); + } + if (detectedCharset) { + if (detectedCharset === httpHeaderCharset) { + result = httpHeaderCharset; + } else if (detectedCharset === htmlMetaCharset) { + result = htmlMetaCharset; + } + } + if (!result) { + result = httpHeaderCharset || htmlMetaCharset || detectedCharset; + } + return result || 'utf-8'; +}; + +const toUtf8 = function (contentType: string, body: Buffer): string { + return iconv.decode(body, getCharset(contentType, body)); +}; + +const getUrlContent = async function (urlObjStr: string | URL.UrlWithStringQuery, redirectCount = 5): Promise { + let urlObj: URL.UrlWithStringQuery; + if (typeof urlObjStr === 'string') { + urlObj = URL.parse(urlObjStr); + } else { + urlObj = urlObjStr; + } + + const portsProtocol = new Map( + Object.entries({ + 80: 'http:', + 8080: 'http:', + 443: 'https:', + }), + ); + + const parsedUrl = _.pick(urlObj, ['host', 'hash', 'pathname', 'protocol', 'port', 'query', 'search', 'hostname']); + const ignoredHosts = settings.get('API_EmbedIgnoredHosts').replace(/\s/g, '').split(',') || []; + if (parsedUrl.hostname && (ignoredHosts.includes(parsedUrl.hostname) || ipRangeCheck(parsedUrl.hostname, ignoredHosts))) { + throw new Error('invalid host'); + } + + const safePorts = settings.get('API_EmbedSafePorts').replace(/\s/g, '').split(',') || []; + + if (safePorts.length > 0 && parsedUrl.port && !safePorts.includes(parsedUrl.port)) { + throw new Error('invalid/unsafe port'); + } + + if (safePorts.length > 0 && !parsedUrl.port && !safePorts.some((port) => portsProtocol.get(port) === parsedUrl.protocol)) { + throw new Error('invalid/unsafe port'); + } + + const data = callbacks.run('oembed:beforeGetUrlContent', { + urlObj, + parsedUrl, + }); + + /* This prop is neither passed or returned by the callback, so I'll just comment it for now + if (data.attachments != null) { + return data; + } */ + + const url = URL.format(data.urlObj); + + const sizeLimit = 250000; + + log.debug(`Fetching ${url} following redirects ${redirectCount} times`); + const response = await fetch( + url, + { + compress: true, + follow: redirectCount, + headers: { + 'User-Agent': `${settings.get('API_Embed_UserAgent')} Rocket.Chat/${Info.version}`, + 'Accept-Language': settings.get('Language') || 'en', + }, + size: sizeLimit, // max size of the response body, this was not working as expected so I'm also manually verifying that on the iterator + }, + settings.get('Allow_Invalid_SelfSigned_Certs'), + ); + + let totalSize = 0; + const chunks = []; + // @ts-expect-error from https://github.com/microsoft/TypeScript/issues/39051 + for await (const chunk of response.body) { + totalSize += chunk.length; + chunks.push(chunk); + + if (totalSize > sizeLimit) { + log.warn({ msg: 'OEmbed request size exceeded', url }); + break; + } + } + + log.debug('Obtained response from server with length of', totalSize); + const buffer = Buffer.concat(chunks); + return { + // @ts-expect-error - fetch types are kinda weird + headers: Object.fromEntries(response.headers), + body: toUtf8(response.headers.get('content-type') || 'text/plain', buffer), + parsedUrl, + statusCode: response.status, + }; +}; + +const getUrlMeta = async function ( + url: string, + withFragment?: boolean, +): Promise { + log.debug('Obtaining metadata for URL', url); + const urlObj = URL.parse(url); + if (withFragment != null) { + const queryStringObj = querystring.parse(urlObj.query || ''); + // eslint-disable-next-line @typescript-eslint/camelcase + queryStringObj._escaped_fragment_ = ''; + urlObj.query = querystring.stringify(queryStringObj); + let path = urlObj.pathname; + if (urlObj.query != null) { + path += `?${urlObj.query}`; + urlObj.search = `?${urlObj.query}`; + } + urlObj.path = path; + } + log.debug('Fetching url content', urlObj.path); + let content: OEmbedUrlContentResult | undefined; + try { + content = await getUrlContent(urlObj, 5); + } catch (e) { + log.error('Error fetching url content', e); + } + + if (!content) { + return; + } + + if (content.attachments != null) { + return content; + } + + log.debug('Parsing metadata for URL', url); + const metas: { [k: string]: string } = {}; + + if (content?.body) { + const escapeMeta = (name: string, value: string): string => { + metas[name] = metas[name] || he.unescape(value); + return metas[name]; + }; + content.body.replace(/]*>([^<]*)<\/title>/gim, function (_meta, title) { + return escapeMeta('pageTitle', title); + }); + content.body.replace(/]*(?:name|property)=[']([^']*)['][^>]*\scontent=[']([^']*)['][^>]*>/gim, function (_meta, name, value) { + return escapeMeta(camelCase(name), value); + }); + content.body.replace(/]*(?:name|property)=["]([^"]*)["][^>]*\scontent=["]([^"]*)["][^>]*>/gim, function (_meta, name, value) { + return escapeMeta(camelCase(name), value); + }); + content.body.replace(/]*\scontent=[']([^']*)['][^>]*(?:name|property)=[']([^']*)['][^>]*>/gim, function (_meta, value, name) { + return escapeMeta(camelCase(name), value); + }); + content.body.replace(/]*\scontent=["]([^"]*)["][^>]*(?:name|property)=["]([^"]*)["][^>]*>/gim, function (_meta, value, name) { + return escapeMeta(camelCase(name), value); + }); + if (metas.fragment === '!' && withFragment == null) { + return getUrlMeta(url, true); + } + delete metas.oembedHtml; + } + const headers: { [k: string]: string } = {}; + + if (content?.headers) { + const headerObj = content.headers; + Object.keys(headerObj).forEach((header) => { + headers[camelCase(header)] = headerObj[header]; + }); + } + if (content && content.statusCode !== 200) { + return; + } + return callbacks.run('oembed:afterParseContent', { + url, + meta: metas, + headers, + parsedUrl: content.parsedUrl, + content, + }); +}; + +const getUrlMetaWithCache = async function ( + url: string, + withFragment?: boolean, +): Promise { + log.debug('Getting oembed metadata for', url); + const cache = await OEmbedCache.findOneById(url); + + if (cache != null) { + log.debug('Found oembed metadata in cache for', url); + return cache.data; + } + const data = await getUrlMeta(url, withFragment); + if (data != null) { + try { + log.debug('Saving oembed metadata in cache for', url); + await OEmbedCache.createWithIdAndData(url, data); + } catch (_error) { + log.error({ msg: 'OEmbed duplicated record', url }); + } + return data; + } +}; + +const hasOnlyContentLength = (obj: any): obj is { contentLength: string } => 'contentLength' in obj && Object.keys(obj).length === 1; +const hasOnlyContentType = (obj: any): obj is { contentType: string } => 'contentType' in obj && Object.keys(obj).length === 1; +const hasContentLengthAndContentType = (obj: any): obj is { contentLength: string; contentType: string } => + 'contentLength' in obj && 'contentType' in obj && Object.keys(obj).length === 2; + +const getRelevantHeaders = function (headersObj: { + [key: string]: string; +}): { contentLength: string } | { contentType: string } | { contentLength: string; contentType: string } | void { + const headers = { + ...(headersObj.contentLength && { contentLength: headersObj.contentLength }), + ...(headersObj.contentType && { contentType: headersObj.contentType }), + }; + + if (hasOnlyContentLength(headers) || hasOnlyContentType(headers) || hasContentLengthAndContentType(headers)) { + return headers; + } +}; + +const getRelevantMetaTags = function (metaObj: OEmbedMeta): Record | void { + const tags: Record = {}; + Object.keys(metaObj).forEach((key) => { + const value = metaObj[key]; + if (/^(og|fb|twitter|oembed|msapplication).+|description|title|pageTitle$/.test(key.toLowerCase()) && value && value.trim() !== '') { + tags[key] = value; + } + }); + + if (Object.keys(tags).length > 0) { + return tags; + } +}; + +const insertMaxWidthInOembedHtml = (oembedHtml?: string): string | undefined => + oembedHtml?.replace('iframe', 'iframe style="max-width: 100%;width:400px;height:225px"'); + +const rocketUrlParser = async function (message: IMessage): Promise { + log.debug('Parsing message URLs'); + if (Array.isArray(message.urls)) { + log.debug('URLs found', message.urls.length); + const attachments: MessageAttachment[] = []; + + let changed = false; + for await (const item of message.urls) { + if (item.ignoreParse === true) { + log.debug('URL ignored', item.url); + break; + } + if (!isURL(item.url)) { + break; + } + const data = await getUrlMetaWithCache(item.url); + if (data != null) { + if (isOEmbedUrlContentResult(data) && data.attachments) { + attachments.push(...data.attachments); + break; + } + if (isOEmbedUrlWithMetadata(data) && data.meta != null) { + item.meta = getRelevantMetaTags(data.meta) || {}; + if (item.meta && item.meta.oembedHtml) { + item.meta.oembedHtml = insertMaxWidthInOembedHtml(item.meta.oembedHtml) || ''; + } + } + if (data.headers != null) { + const headers = getRelevantHeaders(data.headers); + if (headers) { + item.headers = headers; + } + } + item.parsedUrl = data.parsedUrl; + changed = true; + } + } + if (attachments.length) { + Messages.setMessageAttachments(message._id, attachments); + } + if (changed === true) { + Messages.setUrlsById(message._id, message.urls); + } + } + return message; +}; + +const OEmbed: { + getUrlMeta: (url: string, withFragment?: boolean) => Promise; + getUrlMetaWithCache: (url: string, withFragment?: boolean) => Promise; + rocketUrlParser: (message: IMessage) => Promise; +} = { + rocketUrlParser, + getUrlMetaWithCache, + getUrlMeta, +}; + +settings.watch('API_Embed', function (value) { + if (value) { + return callbacks.add( + 'afterSaveMessage', + (message) => Promise.await(OEmbed.rocketUrlParser(message)), + callbacks.priority.LOW, + 'API_Embed', + ); + } + return callbacks.remove('afterSaveMessage', 'API_Embed'); +}); + +export { OEmbed }; diff --git a/apps/meteor/app/otr/client/rocketchat.otr.js b/apps/meteor/app/otr/client/rocketchat.otr.js index f8a660d7fcfa..ebaefbe0d7db 100644 --- a/apps/meteor/app/otr/client/rocketchat.otr.js +++ b/apps/meteor/app/otr/client/rocketchat.otr.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { Tracker } from 'meteor/tracker'; -import { Subscriptions } from '../../models'; +import { Subscriptions } from '../../models/client'; import { Notifications } from '../../notifications'; import { t } from '../../utils'; import { onClientMessageReceived } from '../../../client/lib/onClientMessageReceived'; diff --git a/apps/meteor/app/otr/client/rocketchat.otr.room.js b/apps/meteor/app/otr/client/rocketchat.otr.room.js index 95b57ad70b78..9354501a7fcc 100644 --- a/apps/meteor/app/otr/client/rocketchat.otr.room.js +++ b/apps/meteor/app/otr/client/rocketchat.otr.room.js @@ -4,7 +4,6 @@ import { Random } from 'meteor/random'; import { EJSON } from 'meteor/ejson'; import { Tracker } from 'meteor/tracker'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { TimeSync } from 'meteor/mizzao:timesync'; import _ from 'underscore'; import { OTR } from './rocketchat.otr'; @@ -62,7 +61,7 @@ OTR.Room = class { } acknowledge() { - APIClient.v1.post('statistics.telemetry', { params: [{ eventName: 'otrStats', timestamp: Date.now(), rid: this.roomId }] }); + APIClient.post('/v1/statistics.telemetry', { params: [{ eventName: 'otrStats', timestamp: Date.now(), rid: this.roomId }] }); Notifications.notifyUser(this.peerId, 'otr', 'acknowledge', { roomId: this.roomId, @@ -219,12 +218,7 @@ OTR.Room = class { } encrypt(message) { - let ts; - if (isNaN(TimeSync.serverOffset())) { - ts = new Date(); - } else { - ts = new Date(Date.now() + TimeSync.serverOffset()); - } + const ts = new Date(); const data = new TextEncoder('UTF-8').encode( EJSON.stringify({ diff --git a/apps/meteor/app/otr/server/methods/deleteOldOTRMessages.js b/apps/meteor/app/otr/server/methods/deleteOldOTRMessages.js index c7b4dafd9900..3852734e4bb0 100644 --- a/apps/meteor/app/otr/server/methods/deleteOldOTRMessages.js +++ b/apps/meteor/app/otr/server/methods/deleteOldOTRMessages.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { Subscriptions, Messages } from '../../../models'; +import { Subscriptions, Messages } from '../../../models/server'; Meteor.methods({ deleteOldOTRMessages(roomId) { diff --git a/apps/meteor/app/otr/server/methods/updateOTRAck.js b/apps/meteor/app/otr/server/methods/updateOTRAck.js index fc48adca642f..1c822a51d89c 100644 --- a/apps/meteor/app/otr/server/methods/updateOTRAck.js +++ b/apps/meteor/app/otr/server/methods/updateOTRAck.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { Messages } from '../../../models'; +import { Messages } from '../../../models/server'; Meteor.methods({ updateOTRAck(_id, ack) { diff --git a/apps/meteor/app/push/server/methods.js b/apps/meteor/app/push/server/methods.js index 5ab475ab2903..fe549db953d0 100644 --- a/apps/meteor/app/push/server/methods.js +++ b/apps/meteor/app/push/server/methods.js @@ -1,3 +1,4 @@ +import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { Random } from 'meteor/random'; @@ -12,6 +13,7 @@ Meteor.methods({ check(options, { id: Match.Optional(String), token: _matchToken, + authToken: String, appName: String, userId: Match.OneOf(String, null), metadata: Match.Optional(Object), @@ -22,6 +24,9 @@ Meteor.methods({ throw new Meteor.Error(403, 'Forbidden access'); } + // we always store the hashed token to protect users + const hashedToken = Accounts._hashLoginToken(options.authToken); + let doc; // lookup app by id if one was included @@ -48,6 +53,7 @@ Meteor.methods({ // Rig default doc doc = { token: options.token, + authToken: hashedToken, appName: options.appName, userId: options.userId, enabled: true, @@ -71,6 +77,7 @@ Meteor.methods({ $set: { updatedAt: new Date(), token: options.token, + authToken: hashedToken, }, }, ); diff --git a/apps/meteor/app/push/server/push.js b/apps/meteor/app/push/server/push.js index 91781cd3a23d..29b30991155f 100644 --- a/apps/meteor/app/push/server/push.js +++ b/apps/meteor/app/push/server/push.js @@ -12,8 +12,6 @@ import { settings } from '../../settings/server'; export const _matchToken = Match.OneOf({ apn: String }, { gcm: String }); export const appTokensCollection = new Mongo.Collection('_raix_push_app_tokens'); -appTokensCollection._ensureIndex({ userId: 1 }); - export class PushClass { options = {}; diff --git a/apps/meteor/app/reactions/client/init.js b/apps/meteor/app/reactions/client/init.js index 15f77ef642de..b20a36dee39d 100644 --- a/apps/meteor/app/reactions/client/init.js +++ b/apps/meteor/app/reactions/client/init.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { Blaze } from 'meteor/blaze'; -import { Rooms, Subscriptions } from '../../models'; +import { Rooms, Subscriptions } from '../../models/client'; import { MessageAction } from '../../ui-utils'; -import { messageArgs } from '../../ui-utils/client/lib/messageArgs'; +import { messageArgs } from '../../../client/lib/utils/messageArgs'; import { EmojiPicker } from '../../emoji'; import { tooltip } from '../../ui/client/components/tooltip'; import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; diff --git a/apps/meteor/app/reactions/client/methods/setReaction.js b/apps/meteor/app/reactions/client/methods/setReaction.js index 0da4565a49a0..26286e4e1130 100644 --- a/apps/meteor/app/reactions/client/methods/setReaction.js +++ b/apps/meteor/app/reactions/client/methods/setReaction.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { Messages, Rooms, Subscriptions } from '../../../models'; +import { Messages, Rooms, Subscriptions } from '../../../models/client'; import { callbacks } from '../../../../lib/callbacks'; import { emoji } from '../../../emoji'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; diff --git a/apps/meteor/app/reactions/server/setReaction.js b/apps/meteor/app/reactions/server/setReaction.js index f477519e6d03..9cfbd264df88 100644 --- a/apps/meteor/app/reactions/server/setReaction.js +++ b/apps/meteor/app/reactions/server/setReaction.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import _ from 'underscore'; +import { EmojiCustom } from '@rocket.chat/models'; import { Messages, Rooms } from '../../models/server'; -import { EmojiCustom } from '../../models/server/raw'; import { callbacks } from '../../../lib/callbacks'; import { emoji } from '../../emoji/server'; import { isTheLastMessage, msgStream } from '../../lib/server'; diff --git a/apps/meteor/app/search/client/provider/result.js b/apps/meteor/app/search/client/provider/result.js index 5c6eaf62be50..ec0ca30f9784 100644 --- a/apps/meteor/app/search/client/provider/result.js +++ b/apps/meteor/app/search/client/provider/result.js @@ -8,7 +8,7 @@ import _ from 'underscore'; import { messageContext } from '../../../ui-utils/client/lib/messageContext'; import { MessageAction, RoomHistoryManager } from '../../../ui-utils'; -import { messageArgs } from '../../../ui-utils/client/lib/messageArgs'; +import { messageArgs } from '../../../../client/lib/utils/messageArgs'; import { Rooms } from '../../../models/client'; import { getCommonRoomEvents } from '../../../ui/client/views/app/lib/getCommonRoomEvents'; import { goToRoomById } from '../../../../client/lib/utils/goToRoomById'; diff --git a/apps/meteor/app/search/server/model/provider.js b/apps/meteor/app/search/server/model/provider.js index edd8ad805d15..5d236ad6d068 100644 --- a/apps/meteor/app/search/server/model/provider.js +++ b/apps/meteor/app/search/server/model/provider.js @@ -1,6 +1,6 @@ /* eslint no-unused-vars: [2, { "args": "none" }]*/ import SearchLogger from '../logger/logger'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; /** * Setting Object in order to manage settings loading for providers and admin ui display diff --git a/apps/meteor/app/settings/server/functions/validateSetting.ts b/apps/meteor/app/settings/server/functions/validateSetting.ts index 09db6d2cb74a..659bb12225c4 100644 --- a/apps/meteor/app/settings/server/functions/validateSetting.ts +++ b/apps/meteor/app/settings/server/functions/validateSetting.ts @@ -37,8 +37,9 @@ export const validateSetting = (_id: T['_id'], type: T['type } break; case 'select': + case 'lookup': if (typeof value !== 'string' && typeof value !== 'number') { - throw new Error(`Setting ${_id} is of type select but got ${typeof value}`); + throw new Error(`Setting ${_id} is of type ${type} but got ${typeof value}`); } break; case 'date': diff --git a/apps/meteor/app/slackbridge/client/slackbridge_import.client.js b/apps/meteor/app/slackbridge/client/slackbridge_import.client.js index 1441701f92d6..abaf069b8e2f 100644 --- a/apps/meteor/app/slackbridge/client/slackbridge_import.client.js +++ b/apps/meteor/app/slackbridge/client/slackbridge_import.client.js @@ -3,8 +3,11 @@ import { slashCommands } from '../../utils'; settings.onload('SlackBridge_Enabled', (key, value) => { if (value) { - slashCommands.add('slackbridge-import', null, { - description: 'Import_old_messages_from_slackbridge', + slashCommands.add({ + command: 'slackbridge-import', + options: { + description: 'Import_old_messages_from_slackbridge', + }, }); } else { delete slashCommands.commands['slackbridge-import']; diff --git a/apps/meteor/app/slackbridge/server/RocketAdapter.js b/apps/meteor/app/slackbridge/server/RocketAdapter.js index 27f92b4fee45..302e35151ead 100644 --- a/apps/meteor/app/slackbridge/server/RocketAdapter.js +++ b/apps/meteor/app/slackbridge/server/RocketAdapter.js @@ -7,8 +7,8 @@ import { Random } from 'meteor/random'; import { rocketLogger } from './logger'; import { callbacks } from '../../../lib/callbacks'; -import { settings } from '../../settings'; -import { Messages, Rooms, Users } from '../../models'; +import { settings } from '../../settings/server'; +import { Messages, Rooms, Users } from '../../models/server'; import { createRoom, sendMessage, setUserAvatar } from '../../lib'; export default class RocketAdapter { diff --git a/apps/meteor/app/slackbridge/server/SlackAdapter.js b/apps/meteor/app/slackbridge/server/SlackAdapter.js index 4e597f43f1ef..efaa28e18e21 100644 --- a/apps/meteor/app/slackbridge/server/SlackAdapter.js +++ b/apps/meteor/app/slackbridge/server/SlackAdapter.js @@ -8,8 +8,8 @@ import { Meteor } from 'meteor/meteor'; import { slackLogger } from './logger'; import { SlackAPI } from './SlackAPI'; import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; -import { Messages, Rooms, Users } from '../../models'; -import { settings } from '../../settings'; +import { Messages, Rooms, Users } from '../../models/server'; +import { settings } from '../../settings/server'; import { deleteMessage, updateMessage, addUserToRoom, removeUserFromRoom, archiveRoom, unarchiveRoom, sendMessage } from '../../lib'; import { saveRoomName, saveRoomTopic } from '../../channel-settings'; import { FileUpload } from '../../file-upload'; diff --git a/apps/meteor/app/slackbridge/server/slackbridge_import.server.js b/apps/meteor/app/slackbridge/server/slackbridge_import.server.js index e41cb15255cf..094c2124c241 100644 --- a/apps/meteor/app/slackbridge/server/slackbridge_import.server.js +++ b/apps/meteor/app/slackbridge/server/slackbridge_import.server.js @@ -4,7 +4,7 @@ import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { SlackBridge } from './slackbridge'; -import { Rooms } from '../../models'; +import { Rooms } from '../../models/server'; import { msgStream } from '../../lib'; import { slashCommands } from '../../utils'; @@ -87,4 +87,4 @@ function SlackBridgeImport(command, params, item) { } } -slashCommands.add('slackbridge-import', SlackBridgeImport); +slashCommands.add({ command: 'slackbridge-import', callback: SlackBridgeImport }); diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/gimme.ts b/apps/meteor/app/slashcommand-asciiarts/lib/gimme.ts index a057a5faa542..795c10aa3be7 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,13 +7,17 @@ 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); } -slashCommands.add('gimme', Gimme, { - description: 'Slash_Gimme_Description', - params: 'your_message_optional', +slashCommands.add({ + command: 'gimme', + callback: Gimme, + options: { + description: 'Slash_Gimme_Description', + params: 'your_message_optional', + }, }); diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/lenny.ts b/apps/meteor/app/slashcommand-asciiarts/lib/lenny.ts index 135090952227..cba8f3f3857c 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,13 +7,17 @@ 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); } -slashCommands.add('lennyface', LennyFace, { - description: 'Slash_LennyFace_Description', - params: 'your_message_optional', +slashCommands.add({ + command: 'lennyface', + callback: LennyFace, + options: { + description: 'Slash_LennyFace_Description', + params: 'your_message_optional', + }, }); diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/shrug.ts b/apps/meteor/app/slashcommand-asciiarts/lib/shrug.ts index b18e1fd1b039..da55ec19256b 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({ + command: 'shrug', + callback: (_command: 'shrug', params, item): void => { + const msg = item; + msg.msg = `${params} ¯\\_(ツ)_/¯`; + Meteor.call('sendMessage', msg); + }, + options: { + 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..6bd3883a875e 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({ + command: 'tableflip', + callback: (_command, params, item): void => { + const msg = item; + msg.msg = `${params} (╯°□°)╯︵ ┻━┻`; + Meteor.call('sendMessage', msg); + }, + options: { + 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..2fbe8f75c2f5 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({ + command: 'unflip', + callback: (_command: 'unflip', params, item): void => { + const msg = item; + msg.msg = `${params} ┬─┬ ノ( ゜-゜ノ)`; + Meteor.call('sendMessage', msg); + }, + options: { + description: 'Slash_TableUnflip_Description', + params: 'your_message_optional', + }, }); diff --git a/apps/meteor/app/slashcommands-archiveroom/client/client.ts b/apps/meteor/app/slashcommands-archiveroom/client/client.ts index bee69247b2f2..c24763106684 100644 --- a/apps/meteor/app/slashcommands-archiveroom/client/client.ts +++ b/apps/meteor/app/slashcommands-archiveroom/client/client.ts @@ -1,15 +1,11 @@ import { slashCommands } from '../../utils/lib/slashCommand'; -slashCommands.add( - 'archive', - undefined, - { +slashCommands.add({ + command: 'archive', + options: { description: 'Archive', params: '#channel', permission: 'archive-room', }, - undefined, - false, - undefined, - undefined, -); + providesPreview: false, +}); diff --git a/apps/meteor/app/slashcommands-archiveroom/server/server.ts b/apps/meteor/app/slashcommands-archiveroom/server/server.ts index ae5f12ac8506..1fdfea6f5242 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({ + command: 'archive', + callback: 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', + }, + options: { + description: 'Archive', + params: '#channel', + permission: 'archive-room', + }, }); diff --git a/apps/meteor/app/slashcommands-bridge/client/index.ts b/apps/meteor/app/slashcommands-bridge/client/index.ts deleted file mode 100644 index d7beb4be531e..000000000000 --- a/apps/meteor/app/slashcommands-bridge/client/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { slashCommands } from '../../utils/lib/slashCommand'; - -slashCommands.add( - 'bridge', - undefined, - { - description: 'Invites_an_user_to_a_bridged_room', - params: '#command #user', - }, - undefined, - false, - undefined, - undefined, -); diff --git a/apps/meteor/app/slashcommands-bridge/server/index.ts b/apps/meteor/app/slashcommands-bridge/server/index.ts deleted file mode 100644 index c364aca53720..000000000000 --- a/apps/meteor/app/slashcommands-bridge/server/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -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'; - -function Bridge(_command: 'bridge', stringParams: string, item: IMessage): void { - if (_command !== 'bridge' || !Match.test(stringParams, String)) { - return; - } - - const [command, ...params] = stringParams.split(' '); - - const { rid: roomId } = item; - - switch (command) { - case 'invite': - // Invite a user - // Example: /bridge invite rc_helena:b.rc.allskar.com - const [userId] = params; - - const currentUserId = Meteor.userId(); - - if (currentUserId) { - Promise.await(matrixClient.user.invite(currentUserId, roomId, `@${userId.replace('@', '')}`)); - } - - break; - } -} - -slashCommands.add('bridge', Bridge, { - description: 'Invites_an_user_to_a_bridged_room', - params: '#command #user', -}); diff --git a/apps/meteor/app/slashcommands-create/client/client.ts b/apps/meteor/app/slashcommands-create/client/client.ts index f618d472f096..299db606db9c 100644 --- a/apps/meteor/app/slashcommands-create/client/client.ts +++ b/apps/meteor/app/slashcommands-create/client/client.ts @@ -1,15 +1,11 @@ import { slashCommands } from '../../utils/lib/slashCommand'; -slashCommands.add( - 'create', - undefined, - { +slashCommands.add({ + command: 'create', + options: { description: 'Create_A_New_Channel', params: '#channel', permission: ['create-c', 'create-p'], }, - undefined, - false, - undefined, - undefined, -); + providesPreview: false, +}); diff --git a/apps/meteor/app/slashcommands-create/server/server.ts b/apps/meteor/app/slashcommands-create/server/server.ts index 6d342951b0dc..0ff5d17e36b0 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({ + command: 'create', + callback: 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, []); + }, + options: { + 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..815f6df9323b 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({ + command: 'help', + callback: 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', + }, + options: { + description: 'Show_the_keyboard_shortcut_list', + }, }); diff --git a/apps/meteor/app/slashcommands-hide/client/hide.ts b/apps/meteor/app/slashcommands-hide/client/hide.ts index 61612bf1ff8e..99c1eaea7049 100644 --- a/apps/meteor/app/slashcommands-hide/client/hide.ts +++ b/apps/meteor/app/slashcommands-hide/client/hide.ts @@ -1,6 +1,9 @@ import { slashCommands } from '../../utils/lib/slashCommand'; -slashCommands.add('hide', undefined, { - description: 'Hide_room', - params: '#room', +slashCommands.add({ + command: 'hide', + options: { + description: 'Hide_room', + params: '#room', + }, }); diff --git a/apps/meteor/app/slashcommands-hide/server/hide.ts b/apps/meteor/app/slashcommands-hide/server/hide.ts index 203f304c050d..dbe94d0d7d1c 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({ + command: 'hide', + callback: (_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 }), + }); + } + }); + }, + options: { description: 'Hide_room', params: '#room' }, +}); diff --git a/apps/meteor/app/slashcommands-invite/client/client.ts b/apps/meteor/app/slashcommands-invite/client/client.ts index 4a494287d0ae..729073b785d8 100644 --- a/apps/meteor/app/slashcommands-invite/client/client.ts +++ b/apps/meteor/app/slashcommands-invite/client/client.ts @@ -1,15 +1,11 @@ import { slashCommands } from '../../utils/lib/slashCommand'; -slashCommands.add( - 'invite', - undefined, - { +slashCommands.add({ + command: 'invite', + options: { description: 'Invite_user_to_join_channel', params: '@username', permission: 'add-user-to-joined-room', }, - undefined, - false, - undefined, - undefined, -); + providesPreview: false, +}); diff --git a/apps/meteor/app/slashcommands-invite/server/server.ts b/apps/meteor/app/slashcommands-invite/server/server.ts index d54b319646a8..23b60387df27 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({ + command: 'invite', + callback: (_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', + }); + }, + options: { + description: 'Invite_user_to_join_channel', + params: '@username', + permission: 'add-user-to-joined-room', + }, }); diff --git a/apps/meteor/app/slashcommands-inviteall/client/client.ts b/apps/meteor/app/slashcommands-inviteall/client/client.ts index d09c9d9e908d..f8ab40953d27 100644 --- a/apps/meteor/app/slashcommands-inviteall/client/client.ts +++ b/apps/meteor/app/slashcommands-inviteall/client/client.ts @@ -1,12 +1,18 @@ import { slashCommands } from '../../utils/lib/slashCommand'; -slashCommands.add('invite-all-to', undefined, { - description: 'Invite_user_to_join_channel_all_to', - params: '#room', - permission: ['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room'], +slashCommands.add({ + command: 'invite-all-to', + options: { + description: 'Invite_user_to_join_channel_all_to', + params: '#room', + permission: ['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room'], + }, }); -slashCommands.add('invite-all-from', undefined, { - description: 'Invite_user_to_join_channel_all_from', - params: '#room', - permission: 'add-user-to-joined-room', +slashCommands.add({ + command: 'invite-all-from', + options: { + description: 'Invite_user_to_join_channel_all_from', + params: '#room', + 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..3f5225a4a6c3 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; } @@ -93,14 +93,22 @@ function inviteAll(type: string): typeof slashCommands.commands[string]['callbac }; } -slashCommands.add('invite-all-to', inviteAll('to'), { - description: 'Invite_user_to_join_channel_all_to', - params: '#room', - permission: ['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room'], +slashCommands.add({ + command: 'invite-all-to', + callback: inviteAll('to'), + options: { + description: 'Invite_user_to_join_channel_all_to', + params: '#room', + permission: ['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room'], + }, }); -slashCommands.add('invite-all-from', inviteAll('from'), { - description: 'Invite_user_to_join_channel_all_from', - params: '#room', - permission: 'add-user-to-joined-room', +slashCommands.add({ + command: 'invite-all-from', + callback: inviteAll('from'), + options: { + description: 'Invite_user_to_join_channel_all_from', + params: '#room', + permission: 'add-user-to-joined-room', + }, }); module.exports = inviteAll; diff --git a/apps/meteor/app/slashcommands-join/client/client.ts b/apps/meteor/app/slashcommands-join/client/client.ts index 3fb1ac1c949d..71374b69d6c3 100644 --- a/apps/meteor/app/slashcommands-join/client/client.ts +++ b/apps/meteor/app/slashcommands-join/client/client.ts @@ -2,19 +2,18 @@ import { Meteor } from 'meteor/meteor'; import { slashCommands } from '../../utils/lib/slashCommand'; -slashCommands.add( - 'join', - undefined, - { +slashCommands.add({ + command: 'join', + options: { description: 'Join_the_given_channel', params: '#channel', permission: 'view-c-room', }, - function (err: Meteor.Error, _result: unknown, params: Record) { - if (err.error === 'error-user-already-in-room') { + result(err, _result: unknown, params: Record) { + if ((err as Meteor.Error).error === 'error-user-already-in-room') { params.cmd = 'open'; params.msg.msg = params.msg.msg.replace('join', 'open'); return slashCommands.run('open', params.params, params.msg, ''); } }, -); +}); diff --git a/apps/meteor/app/slashcommands-join/server/server.ts b/apps/meteor/app/slashcommands-join/server/server.ts index 25737a969dd4..5ea5c5c0b9bb 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({ + command: 'join', + callback: (_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); + }, + options: { + description: 'Join_the_given_channel', + params: '#channel', + permission: 'view-c-room', + }, }); diff --git a/apps/meteor/app/slashcommands-kick/client/client.ts b/apps/meteor/app/slashcommands-kick/client/client.ts index f3a2b50410aa..23dee27e5d2e 100644 --- a/apps/meteor/app/slashcommands-kick/client/client.ts +++ b/apps/meteor/app/slashcommands-kick/client/client.ts @@ -1,17 +1,17 @@ import { slashCommands } from '../../utils/lib/slashCommand'; -slashCommands.add( - 'kick', - function (_command: 'kick', params: string) { +slashCommands.add({ + command: 'kick', + callback(_command: 'kick', params: string) { const username = params.trim(); if (username === '') { return; } return username.replace('@', ''); }, - { + options: { description: 'Remove_someone_from_room', params: '@username', permission: 'remove-user', }, -); +}); diff --git a/apps/meteor/app/slashcommands-kick/server/server.ts b/apps/meteor/app/slashcommands-kick/server/server.ts index 5a08e53fbacd..f385269cc4d1 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({ + command: 'kick', + callback: (_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 }); + }, + options: { + 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..566de8295b2e 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,13 +24,21 @@ 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', - permission: ['leave-c', 'leave-p'], +slashCommands.add({ + command: 'leave', + callback: Leave, + options: { + description: 'Leave_the_current_channel', + permission: ['leave-c', 'leave-p'], + }, }); -slashCommands.add('part', Leave, { - description: 'Leave_the_current_channel', - permission: ['leave-c', 'leave-p'], +slashCommands.add({ + command: 'part', + callback: Leave, + options: { + description: 'Leave_the_current_channel', + permission: ['leave-c', 'leave-p'], + }, }); diff --git a/apps/meteor/app/slashcommands-me/server/me.ts b/apps/meteor/app/slashcommands-me/server/me.ts index 300be2ad353a..945cd87620b7 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'; @@ -8,17 +7,17 @@ import { slashCommands } from '../../utils/lib/slashCommand'; * Me is a named function that will replace /me commands * @param {Object} message - The message object */ -slashCommands.add( - 'me', - function Me(_command: 'me', params: string, item: IMessage): void { +slashCommands.add({ + command: 'me', + callback: function Me(_command: 'me', params, item): void { if (s.trim(params)) { const msg = item; msg.msg = `_${params}_`; Meteor.call('sendMessage', msg); } }, - { + options: { description: 'Displays_action_text', params: 'your_message', }, -); +}); diff --git a/apps/meteor/app/slashcommands-msg/server/server.ts b/apps/meteor/app/slashcommands-msg/server/server.ts index bea98e24e2ca..1dc5313bfa10 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({ + command: 'msg', + callback: 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); + }, + options: { + 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..5f9ab47c6117 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({ + command: 'mute', + callback: 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', + }, + options: { + 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..602268da65f4 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({ + command: 'unmute', + callback: 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, + }); + }, + options: { + 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..c2622f00dfbe 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({ + command: 'open', + callback: 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); + }); + }, + options: { + 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..188707a9c809 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({ + command: 'status', + callback: 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', + }, + options: { + 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..d72687ceb333 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({ + command: 'status', + callback: 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', + }); + }, + options: { + 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..e7093e94e314 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({ + command: 'topic', + callback: 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)); + } + }); + } + }, + options: { + 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..b4b2565ed54f 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({ + command: 'topic', + callback: 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; + } + }); + } + }, + options: { + description: 'Slash_Topic_Description', + params: 'Slash_Topic_Params', + permission: 'edit-room', + }, }); diff --git a/apps/meteor/app/slashcommands-unarchiveroom/client/client.ts b/apps/meteor/app/slashcommands-unarchiveroom/client/client.ts index aab7690912a0..2fed1e1c7802 100644 --- a/apps/meteor/app/slashcommands-unarchiveroom/client/client.ts +++ b/apps/meteor/app/slashcommands-unarchiveroom/client/client.ts @@ -1,15 +1,11 @@ import { slashCommands } from '../../utils/lib/slashCommand'; -slashCommands.add( - 'unarchive', - undefined, - { +slashCommands.add({ + command: 'unarchive', + options: { description: 'Unarchive', params: '#channel', permission: 'unarchive-room', }, - undefined, - false, - undefined, - undefined, -); + providesPreview: false, +}); diff --git a/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts b/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts index ad3c999f7b38..fa74c5fd3502 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({ + command: 'unarchive', + callback: 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', + }, + options: { + description: 'Unarchive', + params: '#channel', + permission: 'unarchive-room', + }, }); diff --git a/apps/meteor/app/smarsh-connector/server/functions/generateEml.js b/apps/meteor/app/smarsh-connector/server/functions/generateEml.js index a4244e3f9501..fea8bc9d4d23 100644 --- a/apps/meteor/app/smarsh-connector/server/functions/generateEml.js +++ b/apps/meteor/app/smarsh-connector/server/functions/generateEml.js @@ -2,10 +2,10 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import _ from 'underscore'; import moment from 'moment'; +import { SmarshHistory } from '@rocket.chat/models'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { Rooms, Messages, Users } from '../../../models/server'; -import { SmarshHistory } from '../../../models/server/raw'; import { MessageTypes } from '../../../ui-utils'; import { smarsh } from '../lib/rocketchat'; import 'moment-timezone'; diff --git a/apps/meteor/app/smarsh-connector/server/functions/sendEmail.js b/apps/meteor/app/smarsh-connector/server/functions/sendEmail.js index 67fcfde02e67..ed062693ad57 100644 --- a/apps/meteor/app/smarsh-connector/server/functions/sendEmail.js +++ b/apps/meteor/app/smarsh-connector/server/functions/sendEmail.js @@ -5,10 +5,10 @@ // files: ['i3nc9l3mn'] // } import { UploadFS } from 'meteor/jalik:ufs'; +import { Uploads } from '@rocket.chat/models'; import * as Mailer from '../../../mailer'; -import { Uploads } from '../../../models/server/raw'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { smarsh } from '../lib/rocketchat'; smarsh.sendEmail = async (data) => { diff --git a/apps/meteor/app/smarsh-connector/server/settings.js b/apps/meteor/app/smarsh-connector/server/settings.js index 77c8f4e3b3de..f5fe44cca1b5 100644 --- a/apps/meteor/app/smarsh-connector/server/settings.js +++ b/apps/meteor/app/smarsh-connector/server/settings.js @@ -1,7 +1,6 @@ -import moment from 'moment'; +import moment from 'moment-timezone'; import { settingsRegistry } from '../../settings/server'; -import 'moment-timezone'; settingsRegistry.addGroup('Smarsh', function addSettings() { this.add('Smarsh_Enabled', false, { diff --git a/apps/meteor/app/sms/server/services/mobex.js b/apps/meteor/app/sms/server/services/mobex.js index 5f92c2920305..ad0ceb7ffec0 100644 --- a/apps/meteor/app/sms/server/services/mobex.js +++ b/apps/meteor/app/sms/server/services/mobex.js @@ -1,7 +1,7 @@ import { HTTP } from 'meteor/http'; import { Base64 } from 'meteor/base64'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { SMS } from '../SMS'; import { SystemLogger } from '../../../../server/lib/logger/system'; diff --git a/apps/meteor/app/sms/server/services/twilio.js b/apps/meteor/app/sms/server/services/twilio.js index 3fab2b7520a8..c0213051b91b 100644 --- a/apps/meteor/app/sms/server/services/twilio.js +++ b/apps/meteor/app/sms/server/services/twilio.js @@ -3,7 +3,7 @@ import twilio from 'twilio'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import filesize from 'filesize'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { SMS } from '../SMS'; import { fileUploadIsValidContentType } from '../../../utils/lib/fileUploadRestrictions'; import { api } from '../../../../server/sdk/api'; diff --git a/apps/meteor/app/sms/server/services/voxtelesys.js b/apps/meteor/app/sms/server/services/voxtelesys.js index bc8347caa10b..c5b7a2e88a3e 100644 --- a/apps/meteor/app/sms/server/services/voxtelesys.js +++ b/apps/meteor/app/sms/server/services/voxtelesys.js @@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import filesize from 'filesize'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { SMS } from '../SMS'; import { fileUploadIsValidContentType } from '../../../utils/lib/fileUploadRestrictions'; import { mime } from '../../../utils/lib/mimeTypes'; diff --git a/apps/meteor/app/statistics/server/functions/getLastStatistics.js b/apps/meteor/app/statistics/server/functions/getLastStatistics.js index c405109d6dfd..5fc6bbbb1620 100644 --- a/apps/meteor/app/statistics/server/functions/getLastStatistics.js +++ b/apps/meteor/app/statistics/server/functions/getLastStatistics.js @@ -1,6 +1,7 @@ +import { Statistics } from '@rocket.chat/models'; + import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { statistics } from '../lib/statistics'; -import { Statistics } from '../../../models/server/raw'; export async function getLastStatistics({ userId, refresh }) { if (!(await hasPermissionAsync(userId, 'view-statistics'))) { diff --git a/apps/meteor/app/statistics/server/functions/getStatistics.ts b/apps/meteor/app/statistics/server/functions/getStatistics.ts index da536355c494..4f4cefc64a9f 100644 --- a/apps/meteor/app/statistics/server/functions/getStatistics.ts +++ b/apps/meteor/app/statistics/server/functions/getStatistics.ts @@ -1,8 +1,8 @@ -import type { SortOptionObject, SchemaMember } from 'mongodb'; +import type { FindOptions, SchemaMember } from 'mongodb'; import type { IStats } from '@rocket.chat/core-typings'; +import { Statistics } from '@rocket.chat/models'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { Statistics } from '../../../models/server/raw'; type GetStatisticsParams = { userId: string; @@ -10,7 +10,7 @@ type GetStatisticsParams = { pagination: { offset: number; count?: number; - sort?: SortOptionObject; + sort?: FindOptions['sort']; fields?: SchemaMember; }; }; @@ -26,16 +26,14 @@ export async function getStatistics({ throw new Error('error-not-allowed'); } - const cursor = Statistics.find(query, { + const { cursor, totalCount } = Statistics.findPaginated(query, { sort: sort || { name: 1 }, skip: offset, limit: count, projection: fields, }); - const total = await cursor.count(); - - const statistics = await cursor.toArray(); + const [statistics, total] = await Promise.all([cursor.toArray(), totalCount]); return { statistics, diff --git a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts index f52bce1ec94b..dec524ca3705 100644 --- a/apps/meteor/app/statistics/server/lib/SAUMonitor.ts +++ b/apps/meteor/app/statistics/server/lib/SAUMonitor.ts @@ -2,14 +2,15 @@ import { Meteor } from 'meteor/meteor'; import { SyncedCron } from 'meteor/littledata:synced-cron'; import UAParser from 'ua-parser-js'; import mem from 'mem'; -import type { ISession, ISessionDevice, ISocketConnection, IUser } from '@rocket.chat/core-typings'; +import type { ISession, ISessionDevice, ISocketConnectionLogged, IUser } from '@rocket.chat/core-typings'; +import { Sessions, Users } from '@rocket.chat/models'; import { UAParserMobile, UAParserDesktop } from './UAParserCustom'; -import { Sessions, Users } from '../../../models/server/raw'; -import { aggregates } from '../../../models/server/raw/Sessions'; +import { aggregates } from '../../../../server/models/raw/Sessions'; import { Logger } from '../../../../server/lib/logger/Logger'; import { getMostImportantRole } from '../../../../lib/roles/getMostImportantRole'; import { sauEvents } from '../../../../server/services/sauMonitor/events'; +import { getClientAddress } from '../../../../server/lib/getClientAddress'; type DateObj = { day: number; month: number; year: number }; @@ -23,7 +24,7 @@ const logger = new Logger('SAUMonitor'); const getUserRoles = mem( async (userId: string): Promise => { - const user = await Users.findOneById(userId, { projection: { roles: 1 } }); + const user = await Users.findOneById>(userId, { projection: { roles: 1 } }); return user?.roles || []; }, @@ -121,20 +122,25 @@ export class SAUMonitorClass { if (!this.isRunning()) { return; } + const { id: sessionId } = connection; - await Sessions.logoutByInstanceIdAndSessionIdAndUserId(connection.instanceId, connection.id, userId); + await Sessions.logoutBySessionIdAndUserId({ sessionId, userId }); }); } private async _handleSession( - connection: ISocketConnection, + connection: ISocketConnectionLogged, params: Pick, ): Promise { const data = this._getConnectionInfo(connection, params); + if (!data) { return; } - await Sessions.createOrUpdate(data); + + const searchTerm = this._getSearchTerm(data); + + await Sessions.insertOne({ ...data, searchTerm, createdAt: new Date() }); } private async _finishSessionsFromDate(yesterday: Date, today: Date): Promise { @@ -181,30 +187,37 @@ export class SAUMonitorClass { // TODO missing an action to perform on dangling sessions (for example remove sessions not closed one month ago) } + private _getSearchTerm(session: Omit): string { + return [session.device?.name, session.device?.type, session.device?.os.name, session.sessionId, session.userId] + .filter(Boolean) + .join(''); + } + private _getConnectionInfo( - connection: ISocketConnection, + connection: ISocketConnectionLogged, params: Pick, - ): Omit | undefined { + ): Omit | undefined { if (!connection) { return; } - const ip = connection.clientAddress || connection.httpHeaders?.['x-real-ip'] || connection.httpHeaders?.['x-forwarded-for']; + const ip = getClientAddress(connection); - const host = connection.httpHeaders?.host || ''; + const host = connection.httpHeaders?.host ?? ''; return { type: 'session', sessionId: connection.id, instanceId: connection.instanceId, - ip: (Array.isArray(ip) ? ip[0] : ip) || '', + ...(connection.loginToken && { loginToken: connection.loginToken }), + ip, host, ...this._getUserAgentInfo(connection), ...params, }; } - private _getUserAgentInfo(connection: ISocketConnection): { device: ISessionDevice } | undefined { + private _getUserAgentInfo(connection: ISocketConnectionLogged): { device: ISessionDevice } | undefined { if (!connection?.httpHeaders?.['user-agent']) { return; } @@ -315,13 +328,6 @@ export class SAUMonitorClass { date.setDate(date.getDate() - 0); // yesterday const yesterday = getDateObj(date); - const match = { - type: 'session', - year: { $lte: yesterday.year }, - month: { $lte: yesterday.month }, - day: { $lte: yesterday.day }, - }; - for await (const record of aggregates.dailySessionsOfYesterday(Sessions.col, yesterday)) { await Sessions.updateOne( { _id: `${record.userId}-${record.year}-${record.month}-${record.day}` }, @@ -330,11 +336,19 @@ export class SAUMonitorClass { ); } - await Sessions.updateMany(match, { - $set: { - type: 'computed-session', - _computedAt: new Date(), + await Sessions.updateMany( + { + type: 'session', + year: { $lte: yesterday.year }, + month: { $lte: yesterday.month }, + day: { $lte: yesterday.day }, }, - }); + { + $set: { + type: 'computed-session', + _computedAt: new Date(), + }, + }, + ); } } diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 051456c55ae7..5f1bf083f87b 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -5,12 +5,6 @@ import _ from 'underscore'; import { Meteor } from 'meteor/meteor'; import { MongoInternals } from 'meteor/mongo'; import type { IRoom, IStats } from '@rocket.chat/core-typings'; - -import { Settings, Users, Rooms, Subscriptions, Messages, LivechatVisitors } from '../../../models/server'; -import { settings } from '../../../settings/server'; -import { Info, getMongoInfo } from '../../../utils/server'; -import { getControl } from '../../../../server/lib/migrations'; -import { getStatistics as federationGetStatistics } from '../../../federation/server/functions/dashboard'; import { NotificationQueue, Users as UsersRaw, @@ -21,17 +15,25 @@ import { Invites, Uploads, LivechatDepartment, + LivechatVisitors, EmailInbox, LivechatBusinessHours, Messages as MessagesRaw, + Roles as RolesRaw, InstanceStatus, -} from '../../../models/server/raw'; +} from '@rocket.chat/models'; + +import { Settings, Users, Rooms, Subscriptions, Messages } from '../../../models/server'; +import { settings } from '../../../settings/server'; +import { Info, getMongoInfo } from '../../../utils/server'; +import { getControl } from '../../../../server/lib/migrations'; +import { getStatistics as federationGetStatistics } from '../../../federation/server/functions/dashboard'; import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { getAppsStatistics } from './getAppsStatistics'; import { getImporterStatistics } from './getImporterStatistics'; import { getServicesStatistics } from './getServicesStatistics'; import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server'; -import { Analytics, Team } from '../../../../server/sdk'; +import { Analytics, Team, VideoConf } from '../../../../server/sdk'; import { getSettingsStatistics } from '../../../../server/lib/statistics/getSettingsStatistics'; const wizardFields = ['Organization_Type', 'Industry', 'Size', 'Country', 'Language', 'Server_Type', 'Register_Server']; @@ -112,7 +114,7 @@ export const statistics = { statistics.totalThreads = Messages.countThreads(); // livechat visitors - statistics.totalLivechatVisitors = LivechatVisitors.find().count(); + statistics.totalLivechatVisitors = await LivechatVisitors.find().count(); // livechat agents statistics.totalLivechatAgents = Users.findAgents().count(); @@ -182,8 +184,8 @@ export const statistics = { // Amount of chats placed on hold statsPms.push( - MessagesRaw.col.distinct('rid', { t: 'omnichannel_placed_chat_on_hold' }).then((msgs) => { - statistics.chatsOnHold = msgs.length; + MessagesRaw.countRoomsWithMessageType('omnichannel_placed_chat_on_hold', { readPreference }).then((total) => { + statistics.chatsOnHold = total; }), ); @@ -192,12 +194,9 @@ export const statistics = { // Amount of VoIP Calls statsPms.push( - RoomsRaw.col - .find({ t: 'v' }) - .count() - .then((count) => { - statistics.voipCalls = count; - }), + RoomsRaw.countByType('v').then((count) => { + statistics.voipCalls = count; + }), ); // Amount of VoIP Extensions connected @@ -212,27 +211,21 @@ export const statistics = { // Amount of Calls that ended properly statsPms.push( - MessagesRaw.col - .find({ t: 'voip-call-wrapup' }) - .count() - .then((count) => { - statistics.voipSuccessfulCalls = count; - }), + MessagesRaw.countByType('voip-call-wrapup', { readPreference }).then((count) => { + statistics.voipSuccessfulCalls = count; + }), ); // Amount of Calls that ended with an error statsPms.push( - MessagesRaw.col - .find({ t: 'voip-call-ended-unexpectedly' }) - .count() - .then((count) => { - statistics.voipErrorCalls = count; - }), + MessagesRaw.countByType('voip-call-ended-unexpectedly', { readPreference }).then((count) => { + statistics.voipErrorCalls = count; + }), ); // Amount of Calls that were put on hold statsPms.push( - MessagesRaw.col.distinct('rid', { t: 'voip-call-on-hold' }).then((msgs) => { - statistics.voipOnHoldCalls = msgs.length; + MessagesRaw.countRoomsWithMessageType('voip-call-on-hold', { readPreference }).then((count) => { + statistics.voipOnHoldCalls = count; }), ); @@ -398,6 +391,7 @@ export const statistics = { statistics.apps = getAppsStatistics(); statistics.services = getServicesStatistics(); statistics.importer = getImporterStatistics(); + statistics.videoConf = await VideoConf.getStatistics(); // If getSettingsStatistics() returns an error, save as empty object. statsPms.push( @@ -466,6 +460,9 @@ export const statistics = { statistics.slashCommandsJitsi = settings.get('Jitsi_Start_SlashCommands_Count'); statistics.totalOTRRooms = Rooms.findByCreatedOTR().count(); statistics.totalOTR = settings.get('OTR_Count'); + statistics.totalBroadcastRooms = await RoomsRaw.findByBroadcast().count(); + statistics.totalRoomsWithActiveLivestream = await RoomsRaw.findByActiveLivestream().count(); + statistics.totalTriggeredEmails = settings.get('Triggered_Emails_Count'); statistics.totalRoomsWithStarred = await MessagesRaw.countRoomsWithStarredMessages({ readPreference }); statistics.totalRoomsWithPinned = await MessagesRaw.countRoomsWithPinnedMessages({ readPreference }); statistics.totalUserTOTP = await UsersRaw.findActiveUsersTOTPEnable({ readPreference }).count(); @@ -477,10 +474,32 @@ export const statistics = { statistics.totalEmailInvitation = settings.get('Invitation_Email_Count'); statistics.totalE2ERooms = await RoomsRaw.findByE2E({ readPreference }).count(); statistics.logoChange = Object.keys(settings.get('Assets_logo')).includes('url'); - statistics.homeTitleChanged = settings.get('Layout_Home_Title') !== 'Home'; statistics.showHomeButton = settings.get('Layout_Show_Home_Button'); - statistics.totalEncryptedMessages = await MessagesRaw.countE2EEMessages({ readPreference }); + statistics.totalEncryptedMessages = await MessagesRaw.countByType('e2e', { readPreference }); statistics.totalManuallyAddedUsers = settings.get('Manual_Entry_User_Count'); + statistics.totalSubscriptionRoles = await RolesRaw.findByScope('Subscriptions').count(); + statistics.totalUserRoles = await RolesRaw.findByScope('Users').count(); + statistics.totalWebRTCCalls = settings.get('WebRTC_Calls_Count'); + statistics.matrixBridgeEnabled = settings.get('Federation_Matrix_enabled'); + statistics.uncaughtExceptionsCount = settings.get('Uncaught_Exceptions_Count'); + + const defaultHomeTitle = Settings.findOneById('Layout_Home_Title').packageValue; + statistics.homeTitleChanged = settings.get('Layout_Home_Title') !== defaultHomeTitle; + + const defaultHomeBody = Settings.findOneById('Layout_Home_Body').packageValue; + statistics.homeBodyChanged = settings.get('Layout_Home_Body') !== defaultHomeBody; + + const defaultCustomCSS = Settings.findOneById('theme-custom-css').packageValue; + statistics.customCSSChanged = settings.get('theme-custom-css') !== defaultCustomCSS; + + const defaultOnLogoutCustomScript = Settings.findOneById('Custom_Script_On_Logout').packageValue; + statistics.onLogoutCustomScriptChanged = settings.get('Custom_Script_On_Logout') !== defaultOnLogoutCustomScript; + + const defaultLoggedOutCustomScript = Settings.findOneById('Custom_Script_Logged_Out').packageValue; + statistics.loggedOutCustomScriptChanged = settings.get('Custom_Script_Logged_Out') !== defaultLoggedOutCustomScript; + + const defaultLoggedInCustomScript = Settings.findOneById('Custom_Script_Logged_In').packageValue; + statistics.loggedInCustomScriptChanged = settings.get('Custom_Script_Logged_In') !== defaultLoggedInCustomScript; await Promise.all(statsPms).catch(log); diff --git a/apps/meteor/app/theme/client/imports/components/message-box.css b/apps/meteor/app/theme/client/imports/components/message-box.css index 92c801d2be82..c775af9fa865 100644 --- a/apps/meteor/app/theme/client/imports/components/message-box.css +++ b/apps/meteor/app/theme/client/imports/components/message-box.css @@ -146,6 +146,10 @@ } } + &__federation_icon { + width: 20px; + } + &__action-menu { position: relative; diff --git a/apps/meteor/app/theme/client/imports/general/base_old.css b/apps/meteor/app/theme/client/imports/general/base_old.css index 5e198fed7f87..f6ed236827ea 100644 --- a/apps/meteor/app/theme/client/imports/general/base_old.css +++ b/apps/meteor/app/theme/client/imports/general/base_old.css @@ -36,7 +36,7 @@ padding: 0.05rem 0.2rem; - line-height: 1rem; + line-height: 1.25rem; } } @@ -120,7 +120,7 @@ } .copyonly { - display: inline-block; + display: none; width: 0; height: 0; @@ -2934,6 +2934,8 @@ .rc-old .highlight-text { padding: 2px; + color: inherit; + border-radius: 15px; background-color: var(--selection-background); } diff --git a/apps/meteor/app/theme/client/imports/general/rtl.css b/apps/meteor/app/theme/client/imports/general/rtl.css index e4a16e61846f..3e260ee22e56 100644 --- a/apps/meteor/app/theme/client/imports/general/rtl.css +++ b/apps/meteor/app/theme/client/imports/general/rtl.css @@ -411,12 +411,6 @@ } } - /* Override toastr messages to show on the left side */ - & .toast-top-right { - right: auto; - left: 12px; - } - @media (width <= 1100px) { & #rocket-chat .flex-opened { left: 0; diff --git a/apps/meteor/app/theme/client/index.js b/apps/meteor/app/theme/client/index.js index 304c6de1954f..2dc7ee1fe082 100644 --- a/apps/meteor/app/theme/client/index.js +++ b/apps/meteor/app/theme/client/index.js @@ -1,4 +1,3 @@ -import 'toastr/build/toastr.min.css'; import './main.css'; import './vendor/photoswipe.css'; import './vendor/fontello/css/fontello.css'; diff --git a/apps/meteor/app/threads/client/flextab/thread.js b/apps/meteor/app/threads/client/flextab/thread.js index c1dfa725b5e6..6c71447980be 100644 --- a/apps/meteor/app/threads/client/flextab/thread.js +++ b/apps/meteor/app/threads/client/flextab/thread.js @@ -11,7 +11,7 @@ import { chatMessages, ChatMessages } from '../../../ui'; import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; import { messageContext } from '../../../ui-utils/client/lib/messageContext'; import { upsertMessageBulk } from '../../../ui-utils/client/lib/RoomHistoryManager'; -import { Messages } from '../../../models'; +import { Messages } from '../../../models/client'; import { fileUpload } from '../../../ui/client/lib/fileUpload'; import { dropzoneEvents, dropzoneHelpers } from '../../../ui/client/views/app/room'; import './thread.html'; @@ -77,6 +77,8 @@ Template.thread.helpers({ } = Template.currentData(); const showFormattingTips = settings.get('Message_ShowFormattingTips'); + const alsoSendPreferenceState = getUserPreference(Meteor.userId(), 'alsoSendThreadToChannel'); + return { showFormattingTips, tshow: instance.state.get('sendToChannel'), @@ -85,7 +87,9 @@ Template.thread.helpers({ tmid, onSend: (...args) => { instance.sendToBottom(); - instance.state.set('sendToChannel', false); + if (alsoSendPreferenceState === 'default') { + instance.state.set('sendToChannel', false); + } return instance.chatMessages && instance.chatMessages.send.apply(instance.chatMessages, args); }, onKeyUp: (...args) => instance.chatMessages && instance.chatMessages.keyup.apply(instance.chatMessages, args), @@ -243,8 +247,22 @@ Template.thread.onRendered(function () { Template.thread.onCreated(async function () { this.Threads = new Mongo.Collection(null); + const preferenceState = getUserPreference(Meteor.userId(), 'alsoSendThreadToChannel'); + + let sendToChannel; + switch (preferenceState) { + case 'always': + sendToChannel = true; + break; + case 'never': + sendToChannel = false; + break; + default: + sendToChannel = !this.data.mainMessage.tcount; + } + this.state = new ReactiveDict({ - sendToChannel: !this.data.mainMessage.tcount, + sendToChannel, }); this.loadMore = async () => { diff --git a/apps/meteor/app/threads/client/flextab/threadlist.tsx b/apps/meteor/app/threads/client/flextab/threadlist.tsx index 52e8930b159c..d5003a429aa7 100644 --- a/apps/meteor/app/threads/client/flextab/threadlist.tsx +++ b/apps/meteor/app/threads/client/flextab/threadlist.tsx @@ -1,6 +1,6 @@ import React, { useMemo, lazy, LazyExoticComponent, FC, ReactNode } from 'react'; import { BadgeProps } from '@rocket.chat/fuselage'; -import type { ISubscription } from '@rocket.chat/core-typings'; +import { IRoom, isRoomFederated, ISubscription } from '@rocket.chat/core-typings'; import { useSetting } from '@rocket.chat/ui-contexts'; import { addAction } from '../../../../client/views/room/lib/Toolbox'; @@ -19,7 +19,8 @@ const getVariant = (tunreadUser: number, tunreadGroup: number): BadgeProps['vari const template = lazy(() => import('../../../../client/views/room/contextualBar/Threads')) as LazyExoticComponent; addAction('thread', (options) => { - const room = options.room as unknown as ISubscription; + const room = options.room as unknown as ISubscription & IRoom; + const federated = isRoomFederated(room); const threadsEnabled = useSetting('Threads_enabled'); return useMemo( () => @@ -31,6 +32,10 @@ addAction('thread', (options) => { title: 'Threads', icon: 'thread', template, + ...(federated && { + 'data-tooltip': 'Threads_unavailable_for_federation', + 'disabled': true, + }), renderAction: (props): ReactNode => { const tunread = room.tunread?.length || 0; const tunreadUser = room.tunreadUser?.length || 0; @@ -46,6 +51,6 @@ addAction('thread', (options) => { order: 2, } : null, - [threadsEnabled, room.tunread?.length, room.tunreadUser?.length, room.tunreadGroup?.length], + [threadsEnabled, room.tunread?.length, room.tunreadUser?.length, room.tunreadGroup?.length, federated], ); }); diff --git a/apps/meteor/app/threads/client/messageAction/follow.ts b/apps/meteor/app/threads/client/messageAction/follow.ts index b6194f751afa..b1fe4da2eef4 100644 --- a/apps/meteor/app/threads/client/messageAction/follow.ts +++ b/apps/meteor/app/threads/client/messageAction/follow.ts @@ -18,7 +18,7 @@ Meteor.startup(function () { id: 'follow-message', icon: 'bell', label: 'Follow_message', - context: ['message', 'message-mobile', 'threads'], + context: ['message', 'message-mobile', 'threads', 'federated'], async action(_, { message }) { callWithErrorHandling('followMessage', { mid: message._id }).then(() => dispatchToastMessage({ diff --git a/apps/meteor/app/threads/client/messageAction/replyInThread.ts b/apps/meteor/app/threads/client/messageAction/replyInThread.ts index 1c939110bbd3..82b166310c27 100644 --- a/apps/meteor/app/threads/client/messageAction/replyInThread.ts +++ b/apps/meteor/app/threads/client/messageAction/replyInThread.ts @@ -4,7 +4,7 @@ import { FlowRouter } from 'meteor/kadira:flow-router'; import { settings } from '../../../settings/client'; import { MessageAction } from '../../../ui-utils/client'; -import { messageArgs } from '../../../ui-utils/client/lib/messageArgs'; +import { messageArgs } from '../../../../client/lib/utils/messageArgs'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; Meteor.startup(function () { diff --git a/apps/meteor/app/threads/client/messageAction/unfollow.ts b/apps/meteor/app/threads/client/messageAction/unfollow.ts index f476d3fab767..5b42a499b8e1 100644 --- a/apps/meteor/app/threads/client/messageAction/unfollow.ts +++ b/apps/meteor/app/threads/client/messageAction/unfollow.ts @@ -17,7 +17,7 @@ Meteor.startup(function () { id: 'unfollow-message', icon: 'bell-off', label: 'Unfollow_message', - context: ['message', 'message-mobile', 'threads'], + context: ['message', 'message-mobile', 'threads', 'federated'], async action(_, { message }) { callWithErrorHandling('unfollowMessage', { mid: message._id }).then(() => dispatchToastMessage({ diff --git a/apps/meteor/app/tokenpass/README.md b/apps/meteor/app/tokenpass/README.md deleted file mode 100644 index 8eb3a0c7a37a..000000000000 --- a/apps/meteor/app/tokenpass/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Tokenpass OAuth Flow - -An implementation of the Tokenpass OAuth flow with Tokenpass using RocketChat CustomOAuth. See the [Tokenpass API Reference](http://apidocs.tokenly.com/tokenpass/#oauth-integration) for more details. diff --git a/apps/meteor/app/tokenpass/client/channelSettings.css b/apps/meteor/app/tokenpass/client/channelSettings.css deleted file mode 100644 index 08c0275b14c4..000000000000 --- a/apps/meteor/app/tokenpass/client/channelSettings.css +++ /dev/null @@ -1,47 +0,0 @@ -.channelSettings-tokenpass { - flex-direction: column; - - &.tokenpass__editing { - & .button.edit { - visibility: hidden !important; - } - } - - & .details { - padding-left: 1rem; - } - - & button { - margin: 0 5px 0 0 !important; - } - - & button[disabled] { - opacity: 0.5; - } - - & button[disabled]:hover { - opacity: 0.5; - } - - & .js-edit { - float: right; - } - - & .chip-container { - margin: 0; - - & > li { - padding: 2px 8px; - } - } - - &__form .button { - visibility: visible !important; - } - - &__input { - height: auto !important; - margin: 0 0 10px !important; - padding: 10px !important; - } -} diff --git a/apps/meteor/app/tokenpass/client/index.js b/apps/meteor/app/tokenpass/client/index.js index e51844feeee8..e44dbe195eff 100644 --- a/apps/meteor/app/tokenpass/client/index.js +++ b/apps/meteor/app/tokenpass/client/index.js @@ -1,9 +1 @@ import '../lib/common'; -import './startup'; -import './tokenChannelsList.html'; -import './tokenChannelsList'; -import './tokenpassChannelSettings.html'; -import './tokenpassChannelSettings'; -import './login-button.css'; -import './channelSettings.css'; -import './styles.css'; diff --git a/apps/meteor/app/tokenpass/client/login-button.css b/apps/meteor/app/tokenpass/client/login-button.css deleted file mode 100644 index 1171df2633cc..000000000000 --- a/apps/meteor/app/tokenpass/client/login-button.css +++ /dev/null @@ -1,14 +0,0 @@ -.icon-tokenpass.service-icon { - display: inline-block; - - width: 25px; - height: 1em; - - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAQCAYAAAAbBi9cAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAAddEVYdFRpdGxlAFRva2VubHlfTG9nb19JY29uX1doaXRldF224QAAAapJREFUOI2lk89OE1EUxn9nrumS2GZIhiaFBFekQsILsME/JC7AF2DVpT4BBI3GNRvZoG58Ad0SdCEvwKJW3ehCQmfajB1YAr33uJiZOg2hbeK3u1/O/d1zvnuvUFCr1SqVK/4h6EruqdAAVJR3/yrlKOnF9+v1+mXueEVQpeI/LUJulq6Uy/6TojMAhWE4raLb4yF5U/qs2+0G10Aq8hLl9sQgmOo793wIFEXRIkoj834C8QhAnNWA0jjpdJYAbgGosgsY4EqdfYQxDwTuAmDlBwBG99O9fNW+OfCMbQIlz+kusCrtdmcD0Q/ZaSeoe2iMuRg1k7W2hHgHwGzamTyWdhh9AxbGRzJS37202/+WymkUrYvyMTN+o25t3GiXIsZYdwjMpRQ2BCAMo08Kq6RhLxbDVivvAcToJlwPW+DzzExwTyC9fqcck97cL2AK8LPTGgCivM0aioFz4A5gPWE5CIKmBxAEQRP0TVY4n0NukJ9BAN1P9xZetu33t4DeqGyGJJyhupMvB6BardZD5dXEIMeLarU6+AFDvz9J4tcgRxNgviTJn72i8RdLE6KbF5O00AAAAABJRU5ErkJggg==); - background-repeat: no-repeat; - background-position: center center; -} - -.button.external-login.tokenpass { - background-color: #4170a0; -} diff --git a/apps/meteor/app/tokenpass/client/startup.js b/apps/meteor/app/tokenpass/client/startup.js deleted file mode 100644 index 164e2cc50b87..000000000000 --- a/apps/meteor/app/tokenpass/client/startup.js +++ /dev/null @@ -1,21 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { ChannelSettings } from '../../channel-settings'; -import { Rooms } from '../../models'; - -Meteor.startup(function () { - ChannelSettings.addOption({ - group: ['room'], - id: 'tokenpass', - template: 'channelSettings__tokenpass', - validation(data) { - if (data && data.rid) { - const room = Rooms.findOne(data.rid, { fields: { tokenpass: 1 } }); - - return room && room.tokenpass; - } - - return false; - }, - }); -}); diff --git a/apps/meteor/app/tokenpass/client/styles.css b/apps/meteor/app/tokenpass/client/styles.css deleted file mode 100644 index 126731991bd7..000000000000 --- a/apps/meteor/app/tokenpass/client/styles.css +++ /dev/null @@ -1,12 +0,0 @@ -.icon-tokenpass { - display: inline-block; - - width: 24px; - height: 0.8em; - - margin-bottom: -1px; - - background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MzIgMzgxLjUiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDojNDE3MGEwO308L3N0eWxlPjwvZGVmcz48dGl0bGU+VG9rZW5seV9JY29uPC90aXRsZT48ZyBpZD0iTGF5ZXJfMiIgZGF0YS1uYW1lPSJMYXllciAyIj48ZyBpZD0iTGF5ZXJfMS0yIiBkYXRhLW5hbWU9IkxheWVyIDEiPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTQyOC4zMSwxNzcsMzM0LjA3LDEzLjc2QTI3LjUyLDI3LjUyLDAsMCwwLDMxMC4yNCwwaC03OS40VjEzMy44OWg3NFYxMTIuODRoMTcuNzJsMTguMTMsMzEuNGExMi41MiwxMi41MiwwLDAsMSwwLDEyLjcxLDEyLjc1LDEyLjc1LDAsMCwxLTExLjIyLDYuNTRIMTAyLjU2YTEyLjc1LDEyLjc1LDAsMCwxLTExLjIyLTYuNTQsMTIuNTIsMTIuNTIsMCwwLDEsMC0xMi43MWwxOC4xMy0zMS40aDE3LjcydjIxLjA1aDc0VjBoLTc5LjRBMjcuNTIsMjcuNTIsMCwwLDAsOTcuOTMsMTMuNzZMMy42OSwxNzdhMjcuNTIsMjcuNTIsMCwwLDAsMCwyNy41Mkw5Ny45MywzNjcuNzRhMjcuNTIsMjcuNTIsMCwwLDAsMjMuODQsMTMuNzZoNzkuNFYyNDcuNjFoLTc0djIxLjA2SDEwOS40N2wtMTguMTMtMzEuNGExMi41MiwxMi41MiwwLDAsMSwwLTEyLjcxQTEyLjc1LDEyLjc1LDAsMCwxLDEwMi41NiwyMThIMzI5LjQ0YTEyLjc1LDEyLjc1LDAsMCwxLDExLjIyLDYuNTQsMTIuNTIsMTIuNTIsMCwwLDEsMCwxMi43MWwtMTguMTMsMzEuNEgzMDQuODFWMjQ3LjYxaC03NFYzODEuNWg3OS40YTI3LjUyLDI3LjUyLDAsMCwwLDIzLjg0LTEzLjc2bDk0LjI0LTE2My4yM0EyNy41MiwyNy41MiwwLDAsMCw0MjguMzEsMTc3WiIvPjwvZz48L2c+PC9zdmc+); - background-repeat: no-repeat; - background-position: center center; -} diff --git a/apps/meteor/app/tokenpass/client/tokenChannelsList.html b/apps/meteor/app/tokenpass/client/tokenChannelsList.html deleted file mode 100644 index ae892bfd614a..000000000000 --- a/apps/meteor/app/tokenpass/client/tokenChannelsList.html +++ /dev/null @@ -1,14 +0,0 @@ - diff --git a/apps/meteor/app/tokenpass/client/tokenChannelsList.js b/apps/meteor/app/tokenpass/client/tokenChannelsList.js deleted file mode 100644 index 032ec605985d..000000000000 --- a/apps/meteor/app/tokenpass/client/tokenChannelsList.js +++ /dev/null @@ -1,33 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Tracker } from 'meteor/tracker'; -import { Template } from 'meteor/templating'; - -import { Subscriptions } from '../../models'; - -Template.tokenChannelsList.helpers({ - rooms() { - return Template.instance() - .tokenpassRooms.get() - .filter((room) => Subscriptions.find({ rid: room._id }).count() === 0); - }, -}); - -Template.tokenChannelsList.onRendered(function () { - Tracker.autorun((c) => { - const user = Meteor.user(); - if (user && user.services && user.services.tokenpass) { - c.stop(); - - Meteor.call('findTokenChannels', (error, result) => { - if (!error) { - this.tokenpassRooms.set(result); - } - }); - } - }); -}); - -Template.tokenChannelsList.onCreated(function () { - this.tokenpassRooms = new ReactiveVar([]); -}); diff --git a/apps/meteor/app/tokenpass/client/tokenpassChannelSettings.html b/apps/meteor/app/tokenpass/client/tokenpassChannelSettings.html deleted file mode 100644 index 2ecda3b6d626..000000000000 --- a/apps/meteor/app/tokenpass/client/tokenpassChannelSettings.html +++ /dev/null @@ -1,39 +0,0 @@ - diff --git a/apps/meteor/app/tokenpass/client/tokenpassChannelSettings.js b/apps/meteor/app/tokenpass/client/tokenpassChannelSettings.js deleted file mode 100644 index c92260c88a04..000000000000 --- a/apps/meteor/app/tokenpass/client/tokenpassChannelSettings.js +++ /dev/null @@ -1,119 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Template } from 'meteor/templating'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; - -import { t } from '../../utils'; -import { ChatRoom } from '../../models'; -import { handleError } from '../../../client/lib/utils/handleError'; -import { dispatchToastMessage } from '../../../client/lib/toast'; - -Template.channelSettings__tokenpass.helpers({ - addDisabled() { - const { balance, token } = Template.instance(); - return balance.get() && token.get() ? '' : 'disabled'; - }, - list() { - return Template.instance().list.get(); - }, - save() { - const { list, initial } = Template.instance(); - return JSON.stringify(list.get()) !== JSON.stringify(initial); - }, - editing() { - return Template.instance().editing.get() ? 'tokenpass__editing' : ''; - }, - requiredChecked() { - return Template.instance().requireAll.get() ? 'checked' : ''; - }, - requiredLabel() { - return Template.instance().requireAll.get() ? t('Require_all_tokens') : t('Require_any_token'); - }, - requiredDisabled() { - return !Template.instance().editing.get() ? 'disabled' : ''; - }, - editDisabled() { - return Template.instance().editing.get() ? 'disabled' : ''; - }, -}); - -Template.channelSettings__tokenpass.onCreated(function () { - const room = ChatRoom.findOne(this.data.rid, { fields: { tokenpass: 1 } }); - - this.editing = new ReactiveVar(false); - this.initial = room.tokenpass; - this.requireAll = new ReactiveVar(room.tokenpass.require === 'all'); - this.list = new ReactiveVar(this.initial.tokens); - this.token = new ReactiveVar(''); - this.balance = new ReactiveVar(''); -}); - -Template.channelSettings__tokenpass.events({ - 'click .js-edit'(e, i) { - i.editing.set(true); - }, - 'input [name=token]'(e, i) { - i.token.set(e.target.value); - }, - 'input [name=balance]'(e, i) { - i.balance.set(e.target.value); - }, - 'click .js-add'(e, i) { - e.preventDefault(); - const instance = Template.instance(); - const { balance, token, list } = instance; - list.set([...list.get().filter((t) => t.token !== token), { token: token.get(), balance: balance.get() }]); - - [...i.findAll('input')].forEach((el) => { - el.value = ''; - }); - return balance.set('') && token.set(''); - }, - 'click .js-remove'(e, instance) { - e.preventDefault(); - const { list, editing } = instance; - - if (!editing.get()) { - return; - } - list.set(list.get().filter((t) => t.token !== this.token)); - }, - 'click .js-save'(e, i) { - e.preventDefault(); - - const tokenpass = { - require: i.find('[name=requireAllTokens]').checked ? 'all' : 'any', - tokens: i.list.get(), - }; - - Meteor.call('saveRoomSettings', this.rid, 'tokenpass', tokenpass, function (err) { - if (err) { - return handleError(err); - } - i.editing.set(false); - i.token.set(''); - i.balance.set(''); - i.initial = tokenpass; - [...i.findAll('input')].forEach((el) => { - el.value = ''; - }); - return dispatchToastMessage({ - type: 'success', - message: TAPi18n.__('Room_tokenpass_config_changed_successfully'), - }); - }); - }, - 'click .js-cancel'(e, i) { - e.preventDefault(); - i.editing.set(false); - i.list.set(i.initial.tokens); - i.token.set(''); - i.balance.set(''); - [...i.findAll('input')].forEach((el) => { - el.value = ''; - }); - }, - 'change [name=requireAllTokens]'(e, instance) { - instance.requireAll.set(e.currentTarget.checked); - }, -}); diff --git a/apps/meteor/app/tokenpass/lib/common.js b/apps/meteor/app/tokenpass/lib/common.js index 0cd6868a3672..46e9f0151e7d 100644 --- a/apps/meteor/app/tokenpass/lib/common.js +++ b/apps/meteor/app/tokenpass/lib/common.js @@ -9,7 +9,7 @@ const config = { identityPath: '/oauth/user', authorizePath: '/oauth/authorize', tokenPath: '/oauth/access-token', - scope: 'user,tca,private-balances', + scope: 'user', tokenSentVia: 'payload', usernameField: 'username', mergeUsers: true, diff --git a/apps/meteor/app/tokenpass/server/Tokenpass.js b/apps/meteor/app/tokenpass/server/Tokenpass.js deleted file mode 100644 index 53707063167a..000000000000 --- a/apps/meteor/app/tokenpass/server/Tokenpass.js +++ /dev/null @@ -1,8 +0,0 @@ -export const Tokenpass = { - validateAccess(tokenpass, balances) { - const compFunc = tokenpass.require === 'any' ? 'some' : 'every'; - return tokenpass.tokens[compFunc]((config) => - balances.some((userToken) => config.token === userToken.asset && parseFloat(config.balance) <= parseFloat(userToken.balance)), - ); - }, -}; diff --git a/apps/meteor/app/tokenpass/server/cronRemoveUsers.js b/apps/meteor/app/tokenpass/server/cronRemoveUsers.js deleted file mode 100644 index 2376fc782434..000000000000 --- a/apps/meteor/app/tokenpass/server/cronRemoveUsers.js +++ /dev/null @@ -1,52 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { SyncedCron } from 'meteor/littledata:synced-cron'; - -import { updateUserTokenpassBalances } from './functions/updateUserTokenpassBalances'; -import { Tokenpass } from './Tokenpass'; -import { Rooms, Subscriptions, Users } from '../../models'; -import { removeUserFromRoom } from '../../lib/server/functions/removeUserFromRoom'; - -function removeUsersFromTokenChannels() { - const rooms = {}; - - Rooms.findAllTokenChannels().forEach((room) => { - rooms[room._id] = room.tokenpass; - }); - - const users = {}; - - Subscriptions.findByRoomIds(Object.keys(rooms)).forEach((sub) => { - if (!users[sub.u._id]) { - users[sub.u._id] = []; - } - users[sub.u._id].push(sub.rid); - }); - - Object.keys(users).forEach((user) => { - const userInfo = Users.findOneById(user); - - if (userInfo && userInfo.services && userInfo.services.tokenpass) { - const balances = updateUserTokenpassBalances(userInfo); - - users[user].forEach((roomId) => { - const valid = Tokenpass.validateAccess(rooms[roomId], balances); - - if (!valid) { - Promise.await(removeUserFromRoom(roomId, userInfo)); - } - }); - } - }); -} - -Meteor.startup(function () { - Meteor.defer(function () { - removeUsersFromTokenChannels(); - - SyncedCron.add({ - name: 'Remove users from Token Channels', - schedule: (parser) => parser.cron('0 * * * *'), - job: removeUsersFromTokenChannels, - }); - }); -}); diff --git a/apps/meteor/app/tokenpass/server/functions/getProtectedTokenpassBalances.js b/apps/meteor/app/tokenpass/server/functions/getProtectedTokenpassBalances.js deleted file mode 100644 index c6059608697b..000000000000 --- a/apps/meteor/app/tokenpass/server/functions/getProtectedTokenpassBalances.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { HTTP } from 'meteor/http'; - -import { settings } from '../../../settings'; - -let userAgent = 'Meteor'; -if (Meteor.release) { - userAgent += `/${Meteor.release}`; -} - -export const getProtectedTokenpassBalances = function (accessToken) { - try { - return HTTP.get(`${settings.get('API_Tokenpass_URL')}/api/v1/tca/protected/balances`, { - headers: { - 'Accept': 'application/json', - 'User-Agent': userAgent, - }, - params: { - oauth_token: accessToken, - }, - }).data; - } catch (error) { - throw new Error(`Failed to fetch protected tokenpass balances from Tokenpass. ${error.message}`); - } -}; diff --git a/apps/meteor/app/tokenpass/server/functions/getPublicTokenpassBalances.js b/apps/meteor/app/tokenpass/server/functions/getPublicTokenpassBalances.js deleted file mode 100644 index ead13ad26f83..000000000000 --- a/apps/meteor/app/tokenpass/server/functions/getPublicTokenpassBalances.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { HTTP } from 'meteor/http'; - -import { settings } from '../../../settings'; - -let userAgent = 'Meteor'; -if (Meteor.release) { - userAgent += `/${Meteor.release}`; -} - -export const getPublicTokenpassBalances = function (accessToken) { - try { - return HTTP.get(`${settings.get('API_Tokenpass_URL')}/api/v1/tca/public/balances`, { - headers: { - 'Accept': 'application/json', - 'User-Agent': userAgent, - }, - params: { - oauth_token: accessToken, - }, - }).data; - } catch (error) { - throw new Error(`Failed to fetch public tokenpass balances from Tokenpass. ${error.message}`); - } -}; diff --git a/apps/meteor/app/tokenpass/server/functions/saveRoomTokensMinimumBalance.js b/apps/meteor/app/tokenpass/server/functions/saveRoomTokensMinimumBalance.js deleted file mode 100644 index 5361c58856ba..000000000000 --- a/apps/meteor/app/tokenpass/server/functions/saveRoomTokensMinimumBalance.js +++ /dev/null @@ -1,17 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Match } from 'meteor/check'; -import { escapeHTML } from '@rocket.chat/string-helpers'; - -import { Rooms } from '../../../models'; - -export const saveRoomTokensMinimumBalance = function (rid, roomTokensMinimumBalance) { - if (!Match.test(rid, String)) { - throw new Meteor.Error('invalid-room', 'Invalid room', { - function: 'RocketChat.saveRoomTokensMinimumBalance', - }); - } - - const minimumTokenBalance = parseFloat(escapeHTML(roomTokensMinimumBalance)); - - return Rooms.setMinimumTokenBalanceById(rid, minimumTokenBalance); -}; diff --git a/apps/meteor/app/tokenpass/server/functions/updateUserTokenpassBalances.js b/apps/meteor/app/tokenpass/server/functions/updateUserTokenpassBalances.js deleted file mode 100644 index e85992a55dd6..000000000000 --- a/apps/meteor/app/tokenpass/server/functions/updateUserTokenpassBalances.js +++ /dev/null @@ -1,18 +0,0 @@ -import _ from 'underscore'; - -import { getPublicTokenpassBalances } from './getPublicTokenpassBalances'; -import { getProtectedTokenpassBalances } from './getProtectedTokenpassBalances'; -import { Users } from '../../../models'; - -export const updateUserTokenpassBalances = function (user) { - if (user && user.services && user.services.tokenpass) { - const tcaPublicBalances = getPublicTokenpassBalances(user.services.tokenpass.accessToken); - const tcaProtectedBalances = getProtectedTokenpassBalances(user.services.tokenpass.accessToken); - - const balances = _.uniq(_.union(tcaPublicBalances, tcaProtectedBalances), false, (item) => item.asset); - - Users.setTokenpassTcaBalances(user._id, balances); - - return balances; - } -}; diff --git a/apps/meteor/app/tokenpass/server/index.js b/apps/meteor/app/tokenpass/server/index.js index ba1cf78e0939..086f6cc5d704 100644 --- a/apps/meteor/app/tokenpass/server/index.js +++ b/apps/meteor/app/tokenpass/server/index.js @@ -1,11 +1,2 @@ import '../lib/common'; import './startup'; -import './functions/getProtectedTokenpassBalances'; -import './functions/getPublicTokenpassBalances'; -import './functions/saveRoomTokensMinimumBalance'; -import './methods/findTokenChannels'; -import './methods/getChannelTokenpass'; -import './cronRemoveUsers'; - -export { updateUserTokenpassBalances } from './functions/updateUserTokenpassBalances'; -export { Tokenpass } from './Tokenpass'; diff --git a/apps/meteor/app/tokenpass/server/methods/findTokenChannels.js b/apps/meteor/app/tokenpass/server/methods/findTokenChannels.js deleted file mode 100644 index c4003aeb12fc..000000000000 --- a/apps/meteor/app/tokenpass/server/methods/findTokenChannels.js +++ /dev/null @@ -1,27 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Rooms } from '../../../models'; -import { Tokenpass } from '../Tokenpass'; - -Meteor.methods({ - findTokenChannels() { - if (!Meteor.userId()) { - return []; - } - - const user = Meteor.user(); - - if (user.services && user.services.tokenpass && user.services.tokenpass.tcaBalances) { - const tokens = {}; - user.services.tokenpass.tcaBalances.forEach((token) => { - tokens[token.asset] = 1; - }); - - return Rooms.findByTokenpass(Object.keys(tokens)).filter((room) => - Tokenpass.validateAccess(room.tokenpass, user.services.tokenpass.tcaBalances), - ); - } - - return []; - }, -}); diff --git a/apps/meteor/app/tokenpass/server/methods/getChannelTokenpass.js b/apps/meteor/app/tokenpass/server/methods/getChannelTokenpass.js deleted file mode 100644 index 89a449798781..000000000000 --- a/apps/meteor/app/tokenpass/server/methods/getChannelTokenpass.js +++ /dev/null @@ -1,26 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; - -import { Rooms } from '../../../models'; - -Meteor.methods({ - getChannelTokenpass(rid) { - check(rid, String); - - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'getChannelTokenpass', - }); - } - - const room = Rooms.findOneById(rid); - - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { - method: 'getChannelTokenpass', - }); - } - - return room.tokenpass; - }, -}); diff --git a/apps/meteor/app/tokenpass/server/roomAccessValidator.compatibility.js b/apps/meteor/app/tokenpass/server/roomAccessValidator.compatibility.js deleted file mode 100644 index 637a58317532..000000000000 --- a/apps/meteor/app/tokenpass/server/roomAccessValidator.compatibility.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Tokenpass } from './Tokenpass'; -import { Users } from '../../models'; - -export function validateTokenAccess(userData, roomData) { - if (!userData || !userData.services || !userData.services.tokenpass || !userData.services.tokenpass.tcaBalances) { - return false; - } - - return Tokenpass.validateAccess(roomData.tokenpass, userData.services.tokenpass.tcaBalances); -} - -export const validators = [ - function (room, user) { - const userData = Users.getTokenBalancesByUserId(user._id); - - return validateTokenAccess(userData, room); - }, -]; diff --git a/apps/meteor/app/tokenpass/server/roomAccessValidator.internalService.ts b/apps/meteor/app/tokenpass/server/roomAccessValidator.internalService.ts deleted file mode 100644 index 4d5bd027ba68..000000000000 --- a/apps/meteor/app/tokenpass/server/roomAccessValidator.internalService.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { IRoom, IUser } from '@rocket.chat/core-typings'; - -import { ServiceClassInternal } from '../../../server/sdk/types/ServiceClass'; -import { validators } from './roomAccessValidator.compatibility'; -import { IAuthorizationTokenpass } from '../../../server/sdk/types/IAuthorizationTokenpass'; - -export class AuthorizationTokenpass extends ServiceClassInternal implements IAuthorizationTokenpass { - protected name = 'authorization-tokenpass'; - - protected internal = true; - - async canAccessRoom(room: Pick, user: Pick): Promise { - for (const validator of validators) { - if (validator(room, user)) { - return true; - } - } - - return false; - } -} diff --git a/apps/meteor/app/tokenpass/server/startup.js b/apps/meteor/app/tokenpass/server/startup.js index 0d39a0867ec6..d9f52f6dabd1 100644 --- a/apps/meteor/app/tokenpass/server/startup.js +++ b/apps/meteor/app/tokenpass/server/startup.js @@ -1,11 +1,4 @@ -import { Meteor } from 'meteor/meteor'; -import { Accounts } from 'meteor/accounts-base'; - -import { updateUserTokenpassBalances } from './functions/updateUserTokenpassBalances'; import { settingsRegistry } from '../../settings/server'; -import { callbacks } from '../../../lib/callbacks'; -import { validateTokenAccess } from './roomAccessValidator.compatibility'; -import './roomAccessValidator.internalService'; settingsRegistry.addGroup('OAuth', function () { this.section('Tokenpass', function () { @@ -31,19 +24,3 @@ settingsRegistry.addGroup('OAuth', function () { }); }); }); - -Meteor.startup(function () { - callbacks.add('beforeJoinRoom', function (user, room) { - if (room.tokenpass && !validateTokenAccess(user, room)) { - throw new Meteor.Error('error-not-allowed', 'Token required', { method: 'joinRoom' }); - } - - return user; - }); -}); - -Accounts.onLogin(function ({ user }) { - if (user && user.services && user.services.tokenpass) { - updateUserTokenpassBalances(user); - } -}); diff --git a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.js b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.js index e0d9de196656..3da924f71675 100644 --- a/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.js +++ b/apps/meteor/app/ui-cached-collection/client/models/CachedCollection.js @@ -129,7 +129,7 @@ export class CachedCollection extends Emitter { userRelated = true, listenChangesForLoggedUsersOnly = false, useSync = true, - version = 16, + version = 17, maxCacheTime = 60 * 60 * 24 * 30, onSyncData = (/* action, record */) => {}, }) { diff --git a/apps/meteor/app/ui-clean-history/client/lib/tabBar.ts b/apps/meteor/app/ui-clean-history/client/lib/tabBar.ts index fdbe64940ff3..72fcbf22f598 100644 --- a/apps/meteor/app/ui-clean-history/client/lib/tabBar.ts +++ b/apps/meteor/app/ui-clean-history/client/lib/tabBar.ts @@ -1,5 +1,6 @@ import { useMemo, lazy } from 'react'; import { usePermission } from '@rocket.chat/ui-contexts'; +import { isRoomFederated } from '@rocket.chat/core-typings'; import { addAction } from '../../../../client/views/room/lib/Toolbox'; @@ -7,6 +8,8 @@ const template = lazy(() => import('../../../../client/views/room/contextualBar/ addAction('clean-history', ({ room }) => { const hasPermission = usePermission('clean-channel-history', room._id); + const federated = isRoomFederated(room); + return useMemo( () => hasPermission @@ -16,10 +19,14 @@ addAction('clean-history', ({ room }) => { full: true, title: 'Prune_Messages', icon: 'eraser', + ...(federated && { + 'data-tooltip': 'Clean_History_unavailable_for_federation', + 'disabled': true, + }), template, order: 250, } : null, - [hasPermission], + [hasPermission, federated], ); }); diff --git a/apps/meteor/app/ui-login/client/index.js b/apps/meteor/app/ui-login/client/index.js index 5772939cf607..f78815730e02 100644 --- a/apps/meteor/app/ui-login/client/index.js +++ b/apps/meteor/app/ui-login/client/index.js @@ -6,3 +6,4 @@ import './username/username.html'; import './login/form'; import './login/services'; import './username/username'; +import './login/startup'; diff --git a/apps/meteor/app/ui-login/client/login/layout.html b/apps/meteor/app/ui-login/client/login/layout.html index 4333479c42f9..42ad59b33a82 100644 --- a/apps/meteor/app/ui-login/client/login/layout.html +++ b/apps/meteor/app/ui-login/client/login/layout.html @@ -2,6 +2,9 @@

{{> loginLayoutHeader}} + {{#if showForcedLogoutBanner}} + {{> loggedOutBanner}} + {{/if}} {{> loginForm}} {{> loginLayoutFooter}}
diff --git a/apps/meteor/app/ui-login/client/login/layout.js b/apps/meteor/app/ui-login/client/login/layout.js index a0db1560bf84..d9386ae42a1a 100644 --- a/apps/meteor/app/ui-login/client/login/layout.js +++ b/apps/meteor/app/ui-login/client/login/layout.js @@ -1,3 +1,4 @@ +import { Session } from 'meteor/session'; import { Template } from 'meteor/templating'; import { settings } from '../../../settings'; @@ -10,4 +11,7 @@ Template.loginLayout.helpers({ return `${prefix}/${asset.url || asset.defaultUrl}`; } }, + showForcedLogoutBanner() { + return Session.get('force_logout'); + }, }); diff --git a/apps/meteor/app/ui-login/client/login/startup.ts b/apps/meteor/app/ui-login/client/login/startup.ts new file mode 100644 index 000000000000..60a2cb80cf7c --- /dev/null +++ b/apps/meteor/app/ui-login/client/login/startup.ts @@ -0,0 +1,19 @@ +import { Meteor } from 'meteor/meteor'; +import { Session } from 'meteor/session'; +import { Tracker } from 'meteor/tracker'; + +import { Notifications } from '../../../notifications/client'; + +Meteor.startup(() => { + Tracker.autorun(() => { + const userId = Meteor.userId(); + + if (!userId) { + return; + } + + Notifications.onUser('force_logout', () => { + Session.set('force_logout', true); + }); + }); +}); diff --git a/apps/meteor/app/ui-master/client/body.js b/apps/meteor/app/ui-master/client/body.js index 9fc2bbb4065d..af1bf3c5557a 100644 --- a/apps/meteor/app/ui-master/client/body.js +++ b/apps/meteor/app/ui-master/client/body.js @@ -9,7 +9,7 @@ import { t } from '../../utils/client'; import { chatMessages } from '../../ui'; import { popover, RoomManager } from '../../ui-utils'; import { settings } from '../../settings'; -import { ChatSubscription } from '../../models'; +import { ChatSubscription } from '../../models/client'; import './body.html'; import { imperativeModal } from '../../../client/lib/imperativeModal'; import GenericModal from '../../../client/components/GenericModal'; diff --git a/apps/meteor/app/ui-master/server/index.js b/apps/meteor/app/ui-master/server/index.js index c93c8d73e3c2..cf837de93795 100644 --- a/apps/meteor/app/ui-master/server/index.js +++ b/apps/meteor/app/ui-master/server/index.js @@ -4,7 +4,7 @@ import { Tracker } from 'meteor/tracker'; import _ from 'underscore'; import { escapeHTML } from '@rocket.chat/string-helpers'; -import { Settings } from '../../models'; +import { Settings } from '../../models/server'; import { settings } from '../../settings/server'; import { applyHeadInjections, headInjections, injectIntoBody, injectIntoHead } from './inject'; import './scripts'; diff --git a/apps/meteor/app/ui-message/client/ActionButtonSyncer.ts b/apps/meteor/app/ui-message/client/ActionButtonSyncer.ts index 7410fa2deeca..0ec6f94d7fb0 100644 --- a/apps/meteor/app/ui-message/client/ActionButtonSyncer.ts +++ b/apps/meteor/app/ui-message/client/ActionButtonSyncer.ts @@ -46,7 +46,7 @@ export const removeButton = (button: IUIActionButton): void => { }; export const loadButtons = (): Promise => - APIClient.get('apps/actionButtons').then((value: Array) => { + APIClient.get('/apps/actionButtons').then((value) => { registeredButtons.forEach((button) => removeButton(button)); registeredButtons = []; value.map(addButton); diff --git a/apps/meteor/app/ui-message/client/ActionManager.js b/apps/meteor/app/ui-message/client/ActionManager.js index 1d422855dd73..f172a412dacd 100644 --- a/apps/meteor/app/ui-message/client/ActionManager.js +++ b/apps/meteor/app/ui-message/client/ActionManager.js @@ -8,10 +8,9 @@ import { UIKitInteractionTypes } from '@rocket.chat/core-typings'; import Notifications from '../../notifications/client/lib/Notifications'; import { CachedCollectionManager } from '../../ui-cached-collection'; import { modal } from '../../ui-utils/client/lib/modal'; -import { APIClient } from '../../utils'; +import { APIClient, t } from '../../utils/client'; import * as banners from '../../../client/lib/banners'; import { dispatchToastMessage } from '../../../client/lib/toast'; -import { t } from '../../utils/client'; const events = new Emitter(); @@ -169,7 +168,7 @@ export const triggerAction = async ({ type, actionId, appId, rid, mid, viewId, c setTimeout(reject, TRIGGER_TIMEOUT, [TRIGGER_TIMEOUT_ERROR, { triggerId, appId }]); - const { type: interactionType, ...data } = await APIClient.post(`apps/ui.interaction/${appId}`, { + const { type: interactionType, ...data } = await APIClient.post(`/apps/ui.interaction/${appId}`, { type, actionId, payload, diff --git a/apps/meteor/app/ui-message/client/actionButtons/messageAction.ts b/apps/meteor/app/ui-message/client/actionButtons/messageAction.ts index 2237ce94ebde..992a47b34893 100644 --- a/apps/meteor/app/ui-message/client/actionButtons/messageAction.ts +++ b/apps/meteor/app/ui-message/client/actionButtons/messageAction.ts @@ -1,7 +1,8 @@ import { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; import { Utilities } from '../../../apps/lib/misc/Utilities'; -import { MessageAction, messageArgs } from '../../../ui-utils/client'; +import { MessageAction } from '../../../ui-utils/client'; +import { messageArgs } from '../../../../client/lib/utils/messageArgs'; import { t } from '../../../utils/client'; import { triggerActionButtonAction } from '../ActionManager'; import { applyButtonFilters } from './lib/applyButtonFilters'; diff --git a/apps/meteor/app/ui-message/client/message.html b/apps/meteor/app/ui-message/client/message.html index 4837a2bb0df5..fb29dbfbd5dd 100644 --- a/apps/meteor/app/ui-message/client/message.html +++ b/apps/meteor/app/ui-message/client/message.html @@ -1,5 +1,5 @@