diff --git a/.babelrc b/.babelrc index 867a790a279a..b9359fe771b4 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,6 @@ { "presets": [ - "@babel/preset-env" + "@babel/preset-env", + "@babel/preset-react" ] } diff --git a/.docker/Dockerfile.rhel b/.docker/Dockerfile.rhel index f8607432f2fd..1e0f562724d7 100644 --- a/.docker/Dockerfile.rhel +++ b/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/ubi8/nodejs-12 -ENV RC_VERSION 4.1.2 +ENV RC_VERSION 4.2.0 MAINTAINER buildmaster@rocket.chat diff --git a/.eslintignore b/.eslintignore index 24f6298dbc9d..38a10ea159b1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -11,12 +11,13 @@ public/packages/rocketchat_videobridge/client/public/external_api.js packages/tap-i18n/lib/tap_i18next/tap_i18next-1.7.3.js private/moment-locales/ public/livechat/ -!.scripts public/pdf.worker.min.js public/workers/**/* imports/client/**/* -!/.storybook/ ee/server/services/dist/** !/.mocharc.js +!/.mocharc.*.js +!/.scripts/ +!/.storybook/ !/client/.eslintrc.js !/ee/client/.eslintrc.js diff --git a/.eslintrc b/.eslintrc index 8833cddb4eec..0d96bb0a34f8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -72,7 +72,8 @@ }, "plugins": [ "react", - "@typescript-eslint" + "@typescript-eslint", + "anti-trojan-source" ], "rules": { "func-call-spacing": "off", @@ -122,7 +123,8 @@ "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "ignoreRestSiblings": true - }] + }], + "anti-trojan-source/no-bidi": "error" }, "env": { "browser": true, @@ -144,6 +146,16 @@ "version": "detect" } } + }, + { + "files": [ + "**/*.tests.js", + "**/*.tests.ts", + "**/*.spec.ts" + ], + "env": { + "mocha": true + } } ] } diff --git a/.github/history.json b/.github/history.json index 5f3b0c6a3950..b441b4f94ab1 100644 --- a/.github/history.json +++ b/.github/history.json @@ -67142,6 +67142,725 @@ ] } ] + }, + "4.2.0-rc.0": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.1", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23769", + "title": "Chore: Update settings.ts", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "23565", + "title": "[FIX] Registration not possible when any user is blocked for multiple failed logins", + "userLogin": "ostjen", + "contributors": [ + "ostjen" + ] + }, + { + "pr": "23770", + "title": "Regression: Fix sendMessagesToAdmins not in Fiber", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23771", + "title": "Chore: Remove duplicated 'name' key from rate limiter logs", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23761", + "title": "[NEW] Enable LDAP manual sync to deployments without EE license", + "userLogin": "rodrigok", + "description": "Open the Enterprise LDAP API that executes background sync to be used without any Enterprise License and enforce 2FA requirements.", + "milestone": "4.2.0", + "contributors": [ + "rodrigok", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "23732", + "title": "[NEW] Rate limiting for user registering", + "userLogin": "ostjen", + "milestone": "4.2.0", + "contributors": [ + "ostjen" + ] + }, + { + "pr": "23675", + "title": "Chore: add index on appId + associations for apps_persistence collection", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23768", + "title": "Chore: Bump Rocket.Chat@livechat to 1.10", + "userLogin": "KevLehman", + "milestone": "4.2.0", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23766", + "title": "[IMPROVE] Improve the add user drop down for add a user in create channel modal for UserAutoCompleteMultiple", + "userLogin": "dougfabris", + "description": "Seeing only the name of the person you are not adding is not practical in my opinion because two people can have the same name. Moreover, you can't see the username of the person you want to add in the dropdown. So I changed that and created another selection of users to show the username as well. I made this change so that it would appear in the key place for creating a room and adding a user.\r\n\r\nBefore:\r\n\r\nhttps://user-images.githubusercontent.com/45966964/115287805-faac8d00-a150-11eb-871f-147ab011ced0.mp4\r\n\r\n\r\nAfter:\r\n\r\nhttps://user-images.githubusercontent.com/45966964/115287664-d2249300-a150-11eb-8cf6-0e04730b425d.mp4", + "milestone": "4.2.0", + "contributors": [ + "Jeanstaquet", + "web-flow", + "dougfabris" + ] + }, + { + "pr": "23533", + "title": "[FIX] New specific endpoint for contactChatHistoryMessages with right permissions", + "userLogin": "tiagoevanp", + "description": "Anyone with 'View Omnichannel Rooms' permission can see the History Messages.", + "milestone": "4.2.0", + "contributors": [ + "tiagoevanp", + "web-flow", + "KevLehman", + "ggazzo" + ] + }, + { + "pr": "23588", + "title": "[FIX][ENTERPRISE] OAuth \"Merge Roles\" removes roles from users", + "userLogin": "matheusbsilva137", + "description": "- Fix OAuth \"Merge Roles\": the \"Merge Roles\" option now synchronize only the roles described in the \"**Roles to Sync**\" setting available in each Custom OAuth settings' group (instead of replacing users' roles by their OAuth roles);\r\n- Fix \"Merge Roles\" and \"Channel Mapping\" not being performed/updated on OAuth login.", + "contributors": [ + "matheusbsilva137", + "web-flow" + ] + }, + { + "pr": "23547", + "title": "[IMPROVE] Engagement Dashboard", + "userLogin": "tassoevan", + "description": "- Adds helpers `onToggledFeature` for server and client code to handle license activation/deactivation without server restart;\r\n- Replaces usage of `useEndpointData` with `useQuery` (from [React Query](https://react-query.tanstack.com/));\r\n- Introduces `view-engagement-dashboard` permission.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23004", + "title": "[NEW] Audio and Video calling in Livechat", + "userLogin": "murtaza98", + "contributors": [ + "dhruvjain99", + "murtaza98", + "Deepak-learner" + ] + }, + { + "pr": "23758", + "title": "Chore: Type omnichannel models", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman", + "ggazzo" + ] + }, + { + "pr": "23737", + "title": "[NEW] Allow registering by REG_TOKEN environment variable", + "userLogin": "geekgonecrazy", + "description": "You can provide the REG_TOKEN environment variable containing a registration token and it will automatically register to your cloud account. This simplifies the registration flow", + "contributors": [ + "geekgonecrazy" + ] + }, + { + "pr": "23686", + "title": "[NEW] Permission for download/uploading files on mobile", + "userLogin": "ostjen", + "contributors": [ + "ostjen" + ] + }, + { + "pr": "23735", + "title": "[IMPROVE] Stricter API types", + "userLogin": "tassoevan", + "description": "It:\r\n- Adds stricter types for `API`;\r\n- Enables types for `urlParams`;\r\n- Removes mandatory passage of `undefined` payload on client;\r\n- Corrects some regressions;\r\n- Reassures my belief in TypeScript supremacy.", + "contributors": [ + "tassoevan", + "ggazzo" + ] + }, + { + "pr": "23757", + "title": "Regression: Units endpoint to TS", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "23750", + "title": "[NEW] REST endpoints to manage Omnichannel Business Units", + "userLogin": "KevLehman", + "description": "Basic documentation about endpoints can be found at https://www.postman.com/kaleman960/workspace/rocketchat-public-api/request/3865466-71502450-8c8f-42b4-8954-1cd3d01fcb0c", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23738", + "title": "[FIX] Autofocus on search input in admin", + "userLogin": "gabriellsh", + "description": "Removed \"generic\" autofocus on sidenav template.", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "23745", + "title": "Chore: Generic Table ", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "23739", + "title": "[FIX] Await promise to handle error when attempting to transfer a room", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23673", + "title": "[FIX][ENTERPRISE] Private rooms and discussions can't be audited", + "userLogin": "matheusbsilva137", + "description": "- Add Private rooms (groups) and Discussions to the Message Auditing (Channels) autocomplete;\r\n- Update \"Channels\" tab name to \"Rooms\".", + "contributors": [ + "matheusbsilva137", + "gabriellsh" + ] + }, + { + "pr": "23734", + "title": "[FIX] Missing user roles in edit user tab", + "userLogin": "dougfabris", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "23733", + "title": "[FIX] Discussions created inside discussions", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "23694", + "title": "[NEW] Allow Omnichannel statistics to be collected.", + "userLogin": "cauefcr", + "description": "This PR adds the possibility for business stakeholders to see what is actually being used of the Omnichannel integrations.", + "contributors": [ + null, + "cauefcr", + "web-flow" + ] + }, + { + "pr": "23725", + "title": "[IMPROVE] Re-naming department query param for Twilio", + "userLogin": "murtaza98", + "description": "Since the endpoint supports both, department ID and department Name, so we're renaming it to reflect the same. `departmentName` -> `department`", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "23468", + "title": "[FIX] Fixed E2E default room settings not being honoured", + "userLogin": "ostjen", + "contributors": [ + "ostjen", + "TheDigitalEagle", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "23659", + "title": "[FIX] broken avatar preview when changing avatar", + "userLogin": "Aman-Maheshwari", + "contributors": [ + "Aman-Maheshwari" + ] + }, + { + "pr": "23705", + "title": "[FIX] Prevent UserAction.addStream without Subscription", + "userLogin": "tiagoevanp", + "description": "When you take an Omnichannel chat from queue, the guest's typing information will appear.", + "contributors": [ + "ggazzo", + "tiagoevanp" + ] + }, + { + "pr": "23499", + "title": "[FIX] PhotoSwipe crashing on show", + "userLogin": "tassoevan", + "description": "Waits for initial content to load before showing it.", + "contributors": [ + "tassoevan", + "dougfabris" + ] + }, + { + "pr": "23695", + "title": "Chore: add `no-bidi` rule", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23711", + "title": "[FIX] Fix typo in FR translation", + "userLogin": "Cormoran96", + "contributors": [ + "Cormoran96" + ] + }, + { + "pr": "23706", + "title": "Chore: Mocha testing configuration", + "userLogin": "tassoevan", + "description": "We've been writing integration tests for the REST API quite regularly, but we can't say the same for UI-related modules. This PR is based on the assumption that _improving the developer experience on writing tests_ would increase our coverage and promote the adoption even for newcomers.\r\n\r\nHere as summary of the proposal:\r\n\r\n- Change Mocha configuration files:\r\n - Add a base configuration (`.mocharc.base.json`);\r\n - Rename the configuration for REST API tests (`mocha_end_to_end.opts.js -> .mocharc.api.js`);\r\n - Add a configuration for client modules (`.mocharc.client.js`);\r\n - Enable ESLint for them.\r\n- Add a Mocha test command exclusive for client modules (`npm run testunit-client`);\r\n- Enable fast watch mode:\r\n - Configure `ts-node` to only transpile code (skip type checking);\r\n - Define a list of files to be watched.\r\n- Configure `mocha` environment on ESLint only for test files (required when using Mocha's globals);\r\n- Adopt Chai as our assertion library:\r\n - Unify the setup of Chai plugins (`chai-spies`, `chai-datetime`, `chai-dom`);\r\n - Replace `assert` with `chai`;\r\n - Replace `chai.expect` with `expect`.\r\n- Enable integration tests with React components:\r\n - Enable JSX support on our default Babel configuration;\r\n - Adopt [testing library](https://testing-library.com/).", + "contributors": [ + "tassoevan", + "KevLehman", + "ggazzo" + ] + }, + { + "pr": "23701", + "title": "Chore: Api definitions", + "userLogin": "ggazzo", + "contributors": [ + "tassoevan", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "23703", + "title": "[FIX][ENTERPRISE] Replace all occurrences of a placeholder on string instead of just first one", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23641", + "title": "[FIX] Omnichannel webhooks can't be saved", + "userLogin": "Aman-Maheshwari", + "contributors": [ + "Aman-Maheshwari" + ] + }, + { + "pr": "23595", + "title": "[FIX] Omnichannel business hours page breaking navigation", + "userLogin": "Aman-Maheshwari", + "contributors": [ + "Aman-Maheshwari", + "tiagoevanp", + "web-flow" + ] + }, + { + "pr": "23626", + "title": "[IMPROVE] Allow override of default department for SMS Livechat sessions", + "userLogin": "bhardwajaditya", + "contributors": [ + "bhardwajaditya" + ] + }, + { + "pr": "23691", + "title": "[FIX] Omnichannel contact center navigation", + "userLogin": "tiagoevanp", + "description": "Derives from: https://github.com/RocketChat/Rocket.Chat/pull/23656\r\n\r\nThis PR includes a different approach to solving navigation problems following the same code structure and UI definitions of other \"ActionButtons\" components in Sidebar.", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "23692", + "title": "Regression: Improve AggregationCursor types", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "23696", + "title": "Chore: Remove useCallbacks", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23387", + "title": "[IMPROVE] Reduce complexity in some functions", + "userLogin": "tassoevan", + "description": "Overhauls all places where eslint's `complexity` rule is disabled.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23633", + "title": "Chore: Convert Fiber models to async Step 1", + "userLogin": "rodrigok", + "contributors": [ + "rodrigok", + "sampaiodiego" + ] + }, + { + "pr": "23389", + "title": "[NEW] Permissions for interacting with Omnichannel Contact Center", + "userLogin": "cauefcr", + "description": "Adds a new permission, one that allows for control over user access to Omnichannel Contact Center,", + "contributors": [ + null, + "cauefcr", + "web-flow" + ] + }, + { + "pr": "23587", + "title": "[FIX] Omnichannel status being changed on page refresh", + "userLogin": "KevLehman", + "milestone": "4.1.2", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23661", + "title": "[FIX] Performance issues when running Omnichannel job queue dispatcher", + "userLogin": "renatobecker", + "milestone": "4.1.2", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "23608", + "title": "[FIX] Advanced LDAP Sync Features", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.1.1", + "contributors": [ + "pierre-lehnen-rc", + "web-flow" + ] + }, + { + "pr": "23627", + "title": "[FIX] LDAP users not being re-activated on login", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.1.1", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "23576", + "title": "[FIX] \"to users\" not working in export message", + "userLogin": "ostjen", + "contributors": [ + "ostjen", + "web-flow" + ] + }, + { + "pr": "23607", + "title": "[FIX] App update flow failing in HA setups", + "userLogin": "d-gubert", + "description": "The flow for app updates is broken in specific scenarios with HA setups. Here we change the method calls in the Apps-Engine to avoid race conditions", + "milestone": "4.1.1", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "23566", + "title": "[FIX] Apps scheduler \"losing\" jobs after server restart", + "userLogin": "d-gubert", + "description": "If a job is scheduled and the server restarted, said job won't be executed, giving the impression it's been lost.\r\n\r\nWhat happens is that the scheduler is only started when some app tries to schedule an app - if that happens, all jobs that are \"late\" will be executed; if that doesn't happen, no job will run.\r\n\r\nThis PR starts the apps scheduler right after all apps have been loaded", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "23603", + "title": "i18n: Language update from LingoHub 🤖 on 2021-11-01Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "sampaiodiego" + ] + }, + { + "pr": "23498", + "title": "[NEW] Show on-hold metrics on analytics pages and current chats", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23452", + "title": "Chore: Rearrange module typings", + "userLogin": "tassoevan", + "description": "- Move all external module declarations (definitions and augmentations) to `/definition/externals`;\r\n- ~Symlink some modules on `/definition/externals` to `/ee/server/services/definition/externals`~ Share types with `/ee/server/services`;\r\n- Use TypeScript as server code entrypoint.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23487", + "title": "[FIX] Notifications are not being filtered", + "userLogin": "matheusbsilva137", + "description": "- Add a migration to update the `Accounts_Default_User_Preferences_pushNotifications` setting's value to the `Accounts_Default_User_Preferences_mobileNotifications` setting's value;\r\n - Remove the `Accounts_Default_User_Preferences_mobileNotifications` setting (replaced by `Accounts_Default_User_Preferences_pushNotifications`);\r\n - Rename 'mobileNotifications' user's preference to 'pushNotifications'.", + "milestone": "4.1.2", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "23542", + "title": "[IMPROVE] MKP12 - New UI - Merge Apps and Marketplace Tabs and Content", + "userLogin": "rique223", + "description": "Merged the Marketplace and Apps page into a single page with a tabs component that changes between Markeplace and installed apps.\r\n![page merging](https://user-images.githubusercontent.com/43561537/138516558-f86d62e6-1a5c-4817-a229-a1b876323960.gif)", + "contributors": [ + "ggazzo", + "dougfabris" + ] + }, + { + "pr": "23586", + "title": "Merge master into develop & Set version to 4.2.0-develop", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + } + ] + }, + "4.2.0-rc.1": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.1", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23778", + "title": "Regression: Fix incorrect API path for livechat calls", + "userLogin": "murtaza98", + "milestone": "4.2.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "23775", + "title": "Regression: Fix LDAP sync route", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + } + ] + }, + "4.2.0-rc.2": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.1", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23793", + "title": "Regression: Include files on EE services build", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23789", + "title": "Regression: Fix sort param on omnichannel endpoints", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + } + ] + }, + "4.2.0-rc.3": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.1", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23802", + "title": "Regression: Add @rocket.chat/emitter to EE services", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "4.2.0-rc.4": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.1", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23774", + "title": "Regression: Add trash to raw models", + "userLogin": "sampaiodiego", + "milestone": "4.2.0", + "contributors": [ + "sampaiodiego", + "ggazzo" + ] + }, + { + "pr": "23820", + "title": "[FIX] LDAP users being disabled when an AD security policy is enabled", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.2.0", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "23815", + "title": "Regression: \"When is the chat busier\" and \"Users by time of day\" charts are not working", + "userLogin": "matheusbsilva137", + "description": "- Fix \"When is the chat busier\" (Hours) and \"Users by time of day\" charts, which weren't displaying any data;", + "milestone": "4.2.0", + "contributors": [ + "murtaza98", + "matheusbsilva137", + "web-flow" + ] + }, + { + "pr": "23812", + "title": "i18n: Language update from LingoHub 🤖 on 2021-11-29Z", + "userLogin": "lingohub[bot]", + "milestone": "4.2.0", + "contributors": [ + null, + "sampaiodiego" + ] + }, + { + "pr": "23813", + "title": "Regression: Mark Livechat WebRTC video calling as alpha", + "userLogin": "murtaza98", + "description": "![image](https://user-images.githubusercontent.com/34130764/143832378-82b99a72-23e8-4115-8b28-a0d210de598b.png)", + "milestone": "4.2.0", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "23803", + "title": "Regression: Current Chats not Filtering", + "userLogin": "MartinSchoeler", + "milestone": "4.2.0", + "contributors": [ + "MartinSchoeler" + ] + } + ] + }, + "4.2.0": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.1", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [] } } } \ No newline at end of file diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 57fe8317489a..be8b2b394109 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -127,11 +127,11 @@ jobs: # - name: Build a Meteor cache # run: | # # to do this we can clear the main files and it build the rest - # echo "" > server/main.js - # echo "" > client/main.js + # 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.js client/main.js .meteor/packages + # git checkout -- server/main.ts client/main.ts .meteor/packages - name: Reset Meteor if: startsWith(github.ref, 'refs/tags/') == 'true' || github.ref == 'refs/heads/develop' @@ -142,6 +142,8 @@ jobs: run: | cd ./ee/server/services npm run build + # check if build succeeded + [ ! -d ./dist/ee/server/services ] && exit 1 rm -rf dist/ - name: Build Rocket.Chat From Pull Request @@ -242,9 +244,15 @@ jobs: run: | npm install + - name: Unit Test (definitions) + run: npm run testunit-definition + - name: Unit Test run: npm run testunit + - name: Unit Test (client) + run: npm run testunit-client + - name: E2E Test env: TEST_MODE: "true" @@ -358,11 +366,11 @@ jobs: # - name: Build a Meteor cache # run: | # # to do this we can clear the main files and it build the rest - # echo "" > server/main.js - # echo "" > client/main.js + # 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.js client/main.js .meteor/packages + # git checkout -- server/main.ts client/main.ts .meteor/packages - name: Build Rocket.Chat run: | diff --git a/.husky/pre-push b/.husky/pre-push index 8f8e7a09a9aa..3c9fedc8460a 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,2 +1,3 @@ meteor npm run lint && \ -meteor npm run testunit +meteor npm run testunit && \ +meteor npm run testunit-client diff --git a/.mocharc.api.js b/.mocharc.api.js new file mode 100644 index 000000000000..cef49fb74933 --- /dev/null +++ b/.mocharc.api.js @@ -0,0 +1,17 @@ +'use strict'; + +/** + * Mocha configuration for REST API integration tests. + */ + +module.exports = { + ...require('./.mocharc.base.json'), // see https://github.com/mochajs/mocha/issues/3916 + timeout: 10000, + bail: true, + file: 'tests/end-to-end/teardown.js', + spec: [ + 'tests/end-to-end/api/*.js', + 'tests/end-to-end/api/*.ts', + 'tests/end-to-end/apps/*.js', + ], +}; diff --git a/.mocharc.base.json b/.mocharc.base.json new file mode 100644 index 000000000000..ac8a2bcce8b7 --- /dev/null +++ b/.mocharc.base.json @@ -0,0 +1,16 @@ +{ + "ui": "bdd", + "reporter": "spec", + "extension": ["js", "ts", "tsx"], + "require": [ + "@babel/register", + "regenerator-runtime/runtime", + "ts-node/register", + "./tests/setup/chaiPlugins.ts" + ], + "watch-files": [ + "./**/*.js", + "./**/*.ts", + "./**/*.tsx" + ] +} diff --git a/.mocharc.client.js b/.mocharc.client.js new file mode 100644 index 000000000000..e4279a9a6356 --- /dev/null +++ b/.mocharc.client.js @@ -0,0 +1,32 @@ +'use strict'; + +/** + * Mocha configuration for client-side unit and integration tests. + */ + +const base = require('./.mocharc.base.json'); + +/** + * Mocha will run `ts-node` without doing type checking to speed-up the tests. It should be fine as `npm run typecheck` + * covers test files too. + */ + +Object.assign(process.env, { + TS_NODE_FILES: true, + TS_NODE_TRANSPILE_ONLY: true, +}, process.env); + +module.exports = { + ...base, // see https://github.com/mochajs/mocha/issues/3916 + require: [ + ...base.require, + './tests/setup/registerWebApiMocks.ts', + './tests/setup/cleanupTestingLibrary.ts', + ], + exit: false, + slow: 200, + spec: [ + 'client/**/*.spec.ts', + 'client/**/*.spec.tsx', + ], +}; diff --git a/.mocharc.definition.js b/.mocharc.definition.js new file mode 100644 index 000000000000..efffe16964d5 --- /dev/null +++ b/.mocharc.definition.js @@ -0,0 +1,29 @@ +'use strict'; + +/** + * Mocha configuration for unit tests for type guards. + */ + +const base = require('./.mocharc.base.json'); + +/** + * Mocha will run `ts-node` without doing type checking to speed-up the tests. It should be fine as `npm run typecheck` + * covers test files too. + */ + +Object.assign(process.env, { + TS_NODE_FILES: true, + TS_NODE_TRANSPILE_ONLY: true, +}, process.env); + +module.exports = { + ...base, // see https://github.com/mochajs/mocha/issues/3916 + require: [ + ...base.require, + ], + exit: false, + slow: 200, + spec: [ + 'definition/**/*.spec.ts', + ], +}; diff --git a/.mocharc.js b/.mocharc.js index c939a10c0cc5..a71a3020cf4b 100644 --- a/.mocharc.js +++ b/.mocharc.js @@ -1,18 +1,28 @@ 'use strict'; +/** + * Mocha configuration for general unit tests. + */ + +const base = require('./.mocharc.base.json'); + +/** + * Mocha will run `ts-node` without doing type checking to speed-up the tests. It should be fine as `npm run typecheck` + * covers test files too. + */ + +Object.assign(process.env, { + TS_NODE_FILES: true, + TS_NODE_TRANSPILE_ONLY: true, +}, process.env); + module.exports = { - require: [ - 'ts-node/register', - '@babel/register', - ], - reporter: 'spec', - ui: 'bdd', - extension: ['js', 'ts'], + ...base, // see https://github.com/mochajs/mocha/issues/3916 + exit: true, spec: [ + 'app/**/*.spec.ts', 'app/**/*.tests.js', 'app/**/*.tests.ts', - 'app/**/*.spec.ts', 'server/**/*.tests.ts', - 'client/**/*.spec.ts', ], }; diff --git a/.snapcraft/resources/prepareRocketChat b/.snapcraft/resources/prepareRocketChat index a5968214af14..c27dc0cb3b56 100755 --- a/.snapcraft/resources/prepareRocketChat +++ b/.snapcraft/resources/prepareRocketChat @@ -1,6 +1,6 @@ #!/bin/bash -curl -SLf "https://releases.rocket.chat/4.1.2/download/" -o rocket.chat.tgz +curl -SLf "https://releases.rocket.chat/4.2.0/download/" -o rocket.chat.tgz tar xf rocket.chat.tgz --strip 1 diff --git a/.snapcraft/snap/snapcraft.yaml b/.snapcraft/snap/snapcraft.yaml index 4bb19f3461dd..226a11c4a793 100644 --- a/.snapcraft/snap/snapcraft.yaml +++ b/.snapcraft/snap/snapcraft.yaml @@ -7,7 +7,7 @@ # 5. `snapcraft snap` name: rocketchat-server -version: 4.1.2 +version: 4.2.0 summary: Rocket.Chat server description: Have your own Slack like online chat, built with Meteor. https://rocket.chat/ confinement: strict diff --git a/.storybook/.eslintrc.js b/.storybook/.eslintrc.js new file mode 120000 index 000000000000..8589dc8c5324 --- /dev/null +++ b/.storybook/.eslintrc.js @@ -0,0 +1 @@ +../client/.eslintrc.js \ No newline at end of file diff --git a/.storybook/.prettierrc b/.storybook/.prettierrc new file mode 120000 index 000000000000..4031483e531f --- /dev/null +++ b/.storybook/.prettierrc @@ -0,0 +1 @@ +../client/.prettierrc \ No newline at end of file diff --git a/.storybook/decorators.tsx b/.storybook/decorators.tsx index 01b7b4f93cb7..9314684c128f 100644 --- a/.storybook/decorators.tsx +++ b/.storybook/decorators.tsx @@ -1,6 +1,8 @@ import React, { ReactElement } from 'react'; import { MeteorProviderMock } from './mocks/providers'; +import QueryClientProviderMock from './mocks/providers/QueryClientProviderMock'; +import ServerProviderMock from './mocks/providers/ServerProviderMock'; export const rocketChatDecorator = (storyFn: () => ReactElement): ReactElement => { const linkElement = document.getElementById('theme-styles') || document.createElement('link'); @@ -18,34 +20,44 @@ export const rocketChatDecorator = (storyFn: () => ReactElement): ReactElement = /* eslint-disable-next-line */ const { default: icons } = require('!!raw-loader!../private/public/icons.svg'); - return - -
-
- {storyFn()} -
- ; + return ( + + + + +
+
{storyFn()}
+ + + + ); }; -export const fullHeightDecorator = (storyFn: () => ReactElement): ReactElement => -
+export const fullHeightDecorator = (storyFn: () => ReactElement): ReactElement => ( +
{storyFn()} -
; +
+); -export const centeredDecorator = (storyFn: () => ReactElement): ReactElement => -
+export const centeredDecorator = (storyFn: () => ReactElement): ReactElement => ( +
{storyFn()} -
; +
+); diff --git a/.storybook/hooks/index.ts b/.storybook/hooks/index.ts new file mode 100644 index 000000000000..ca0d1db71f7f --- /dev/null +++ b/.storybook/hooks/index.ts @@ -0,0 +1 @@ +export * from './useAutoToggle'; diff --git a/.storybook/hooks.ts b/.storybook/hooks/useAutoToggle.ts similarity index 100% rename from .storybook/hooks.ts rename to .storybook/hooks/useAutoToggle.ts diff --git a/.storybook/main.js b/.storybook/main.js index 7ac1da4c927f..58531e40b22b 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -3,18 +3,15 @@ const { resolve, relative, join } = require('path'); const webpack = require('webpack'); module.exports = { - typescript: { - reactDocgen: 'none', - }, stories: [ '../app/**/*.stories.{js,tsx}', '../client/**/*.stories.{js,tsx}', - '../ee/**/*.stories.{js,tsx}', - ], - addons: [ - '@storybook/addon-essentials', - '@storybook/addon-postcss', + ...(process.env.EE === 'true' ? ['../ee/**/*.stories.{js,tsx}'] : []), ], + addons: ['@storybook/addon-essentials', '@storybook/addon-postcss'], + typescript: { + reactDocgen: 'none', + }, webpackFinal: async (config) => { const cssRule = config.module.rules.find(({ test }) => test.test('index.css')); @@ -22,16 +19,21 @@ module.exports = { ...cssRule.use[2].options, postcssOptions: { plugins: [ - require('postcss-custom-properties')({ preserve: true }), - require('postcss-media-minmax')(), - require('postcss-nested')(), - require('autoprefixer')(), - require('postcss-url')({ url: ({ absolutePath, relativePath, url }) => { - const absoluteDir = absolutePath.slice(0, -relativePath.length); - const relativeDir = relative(absoluteDir, resolve(__dirname, '../public')); - const newPath = join(relativeDir, url); - return newPath; - } }), + ['postcss-custom-properties', { preserve: true }], + 'postcss-media-minmax', + 'postcss-nested', + 'autoprefixer', + [ + 'postcss-url', + { + url: ({ absolutePath, relativePath, url }) => { + const absoluteDir = absolutePath.slice(0, -relativePath.length); + const relativeDir = relative(absoluteDir, resolve(__dirname, '../public')); + const newPath = join(relativeDir, url); + return newPath; + }, + }, + ], ], }, }; @@ -59,10 +61,7 @@ module.exports = { }); config.plugins.push( - new webpack.NormalModuleReplacementPlugin( - /^meteor/, - require.resolve('./mocks/meteor.js'), - ), + new webpack.NormalModuleReplacementPlugin(/^meteor/, require.resolve('./mocks/meteor.js')), new webpack.NormalModuleReplacementPlugin( /(app)\/*.*\/(server)\/*/, require.resolve('./mocks/empty.ts'), diff --git a/.storybook/mocks/meteor.js b/.storybook/mocks/meteor.js index e4746cb59f0e..ef22c95f67a7 100644 --- a/.storybook/mocks/meteor.js +++ b/.storybook/mocks/meteor.js @@ -13,6 +13,10 @@ export const Meteor = { on: () => {}, removeListener: () => {}, }), + StreamerCentral: { + on: () => {}, + removeListener: () => {}, + }, startup: () => {}, methods: () => {}, call: () => {}, @@ -41,7 +45,9 @@ export const ReactiveVar = (val) => { let currentVal = val; return { get: () => currentVal, - set: (val) => { currentVal = val; }, + set: (val) => { + currentVal = val; + }, }; }; @@ -51,16 +57,19 @@ export const ReactiveDict = () => ({ all: () => {}, }); -export const Template = Object.assign(() => ({ - onCreated: () => {}, - onRendered: () => {}, - onDestroyed: () => {}, - helpers: () => {}, - events: () => {}, -}), { - registerHelper: () => {}, - __checkName: () => {}, -}); +export const Template = Object.assign( + () => ({ + onCreated: () => {}, + onRendered: () => {}, + onDestroyed: () => {}, + helpers: () => {}, + events: () => {}, + }), + { + registerHelper: () => {}, + __checkName: () => {}, + }, +); export const Blaze = { Template, diff --git a/.storybook/mocks/providers/QueryClientProviderMock.tsx b/.storybook/mocks/providers/QueryClientProviderMock.tsx new file mode 100644 index 000000000000..d44ea0e9d079 --- /dev/null +++ b/.storybook/mocks/providers/QueryClientProviderMock.tsx @@ -0,0 +1,20 @@ +import React, { FC } from 'react'; +import { QueryCache, QueryClient, QueryClientProvider } from 'react-query'; + +const queryCache = new QueryCache(); + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + cacheTime: Infinity, + }, + }, + queryCache, +}); + +const QueryClientProviderMock: FC = ({ children }) => ( + {children} +); + +export default QueryClientProviderMock; diff --git a/.storybook/mocks/providers/ServerProviderMock.tsx b/.storybook/mocks/providers/ServerProviderMock.tsx new file mode 100644 index 000000000000..7cc9a273b065 --- /dev/null +++ b/.storybook/mocks/providers/ServerProviderMock.tsx @@ -0,0 +1,96 @@ +import { action } from '@storybook/addon-actions'; +import React, { ContextType, FC } from 'react'; + +import { + ServerContext, + ServerMethodName, + ServerMethodParameters, + ServerMethodReturn, +} from '../../../client/contexts/ServerContext'; +import { Serialized } from '../../../definition/Serialized'; +import { + MatchPathPattern, + Method, + OperationParams, + OperationResult, + PathFor, +} from '../../../definition/rest'; + +const logAction = action('ServerProvider'); + +const randomDelay = (): Promise => + new Promise((resolve) => setTimeout(resolve, Math.random() * 1000)); + +const absoluteUrl = (path: string): string => new URL(path, '/').toString(); + +const callMethod = ( + methodName: MethodName, + ...args: ServerMethodParameters +): Promise> => + Promise.resolve(logAction('callMethod', methodName, ...args)) + .then(randomDelay) + .then(() => undefined as any); + +const callEndpoint = >( + method: TMethod, + path: TPath, + params: Serialized>>, +): Promise>>> => + Promise.resolve(logAction('callEndpoint', method, path, params)) + .then(randomDelay) + .then(() => undefined as any); + +const uploadToEndpoint = (endpoint: string, params: any, formData: any): Promise => + Promise.resolve(logAction('uploadToEndpoint', endpoint, params, formData)).then(randomDelay); + +const getStream = ( + streamName: string, + options: {} = {}, +): ((eventName: string, callback: (data: T) => void) => () => void) => { + logAction('getStream', streamName, options); + + return (eventName, callback): (() => void) => { + const subId = Math.random().toString(16).slice(2); + logAction('getStream.subscribe', streamName, eventName, subId); + + randomDelay().then(() => callback(undefined as any)); + + return (): void => { + logAction('getStream.unsubscribe', streamName, eventName, subId); + }; + }; +}; + +const ServerProviderMock: FC>> = ({ + children, + ...overrides +}) => ( + +); + +export default ServerProviderMock; diff --git a/.storybook/mocks/providers.tsx b/.storybook/mocks/providers/index.tsx similarity index 74% rename from .storybook/mocks/providers.tsx rename to .storybook/mocks/providers/index.tsx index 31a66433c753..9cecb32ff8b9 100644 --- a/.storybook/mocks/providers.tsx +++ b/.storybook/mocks/providers/index.tsx @@ -1,8 +1,10 @@ import i18next from 'i18next'; import React, { PropsWithChildren, ReactElement } from 'react'; -import { TranslationContext, TranslationContextValue } from '../../client/contexts/TranslationContext'; -import ServerProvider from '../../client/providers/ServerProvider'; +import { + TranslationContext, + TranslationContextValue, +} from '../../../client/contexts/TranslationContext'; let contextValue: TranslationContextValue; @@ -16,7 +18,7 @@ const getContextValue = (): TranslationContextValue => { defaultNS: 'project', resources: { en: { - project: require('../../packages/rocketchat-i18n/i18n/en.i18n.json'), + project: require('../../../packages/rocketchat-i18n/i18n/en.i18n.json'), }, }, interpolation: { @@ -45,11 +47,13 @@ const getContextValue = (): TranslationContextValue => { translate.has = (key: string): boolean => !!key && i18next.exists(key); contextValue = { - languages: [{ - name: 'English', - en: 'English', - key: 'en', - }], + languages: [ + { + name: 'English', + en: 'English', + key: 'en', + }, + ], language: 'en', translate, loadLanguage: async (): Promise => undefined, @@ -62,10 +66,7 @@ function TranslationProviderMock({ children }: PropsWithChildren<{}>): ReactElem return ; } +// eslint-disable-next-line react/no-multi-comp export function MeteorProviderMock({ children }: PropsWithChildren<{}>): ReactElement { - return - - {children} - - ; + return {children}; } diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 2839f538c25e..ab9bd5a3220d 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,4 +1,4 @@ -import { DocsPage, DocsContainer } from '@storybook/addon-docs/blocks'; +import { DocsPage, DocsContainer } from '@storybook/addon-docs'; import { addDecorator, addParameters } from '@storybook/react'; import { rocketChatDecorator } from './decorators'; @@ -18,7 +18,6 @@ addParameters({ page: DocsPage, }, options: { - storySort: ([, a], [, b]): number => - a.kind.localeCompare(b.kind), + storySort: ([, a], [, b]): number => a.kind.localeCompare(b.kind), }, }); diff --git a/HISTORY.md b/HISTORY.md index 7ee96dd1faa2..453efbf632d3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,4 +1,295 @@ +# 4.2.0 +`2021-11-30 · 9 🎉 · 7 🚀 · 26 🐛 · 27 🔍 · 24 👩‍💻👨‍💻` + +### Engine versions +- Node: `12.22.1` +- NPM: `6.14.1` +- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- Apps-Engine: `1.28.1` + +### 🎉 New features + + +- Allow Omnichannel statistics to be collected. ([#23694](https://github.com/RocketChat/Rocket.Chat/pull/23694)) + + This PR adds the possibility for business stakeholders to see what is actually being used of the Omnichannel integrations. + +- Allow registering by REG_TOKEN environment variable ([#23737](https://github.com/RocketChat/Rocket.Chat/pull/23737)) + + You can provide the REG_TOKEN environment variable containing a registration token and it will automatically register to your cloud account. This simplifies the registration flow + +- Audio and Video calling in Livechat ([#23004](https://github.com/RocketChat/Rocket.Chat/pull/23004) by [@Deepak-learner](https://github.com/Deepak-learner) & [@dhruvjain99](https://github.com/dhruvjain99)) + +- Enable LDAP manual sync to deployments without EE license ([#23761](https://github.com/RocketChat/Rocket.Chat/pull/23761)) + + Open the Enterprise LDAP API that executes background sync to be used without any Enterprise License and enforce 2FA requirements. + +- Permission for download/uploading files on mobile ([#23686](https://github.com/RocketChat/Rocket.Chat/pull/23686)) + +- Permissions for interacting with Omnichannel Contact Center ([#23389](https://github.com/RocketChat/Rocket.Chat/pull/23389)) + + Adds a new permission, one that allows for control over user access to Omnichannel Contact Center, + +- Rate limiting for user registering ([#23732](https://github.com/RocketChat/Rocket.Chat/pull/23732)) + +- REST endpoints to manage Omnichannel Business Units ([#23750](https://github.com/RocketChat/Rocket.Chat/pull/23750)) + + Basic documentation about endpoints can be found at https://www.postman.com/kaleman960/workspace/rocketchat-public-api/request/3865466-71502450-8c8f-42b4-8954-1cd3d01fcb0c + +- Show on-hold metrics on analytics pages and current chats ([#23498](https://github.com/RocketChat/Rocket.Chat/pull/23498)) + +### 🚀 Improvements + + +- Allow override of default department for SMS Livechat sessions ([#23626](https://github.com/RocketChat/Rocket.Chat/pull/23626) by [@bhardwajaditya](https://github.com/bhardwajaditya)) + +- Engagement Dashboard ([#23547](https://github.com/RocketChat/Rocket.Chat/pull/23547)) + + - Adds helpers `onToggledFeature` for server and client code to handle license activation/deactivation without server restart; + - Replaces usage of `useEndpointData` with `useQuery` (from [React Query](https://react-query.tanstack.com/)); + - Introduces `view-engagement-dashboard` permission. + +- Improve the add user drop down for add a user in create channel modal for UserAutoCompleteMultiple ([#23766](https://github.com/RocketChat/Rocket.Chat/pull/23766) by [@Jeanstaquet](https://github.com/Jeanstaquet)) + + Seeing only the name of the person you are not adding is not practical in my opinion because two people can have the same name. Moreover, you can't see the username of the person you want to add in the dropdown. So I changed that and created another selection of users to show the username as well. I made this change so that it would appear in the key place for creating a room and adding a user. + + Before: + + https://user-images.githubusercontent.com/45966964/115287805-faac8d00-a150-11eb-871f-147ab011ced0.mp4 + + + After: + + https://user-images.githubusercontent.com/45966964/115287664-d2249300-a150-11eb-8cf6-0e04730b425d.mp4 + +- MKP12 - New UI - Merge Apps and Marketplace Tabs and Content ([#23542](https://github.com/RocketChat/Rocket.Chat/pull/23542)) + + Merged the Marketplace and Apps page into a single page with a tabs component that changes between Markeplace and installed apps. + ![page merging](https://user-images.githubusercontent.com/43561537/138516558-f86d62e6-1a5c-4817-a229-a1b876323960.gif) + +- Re-naming department query param for Twilio ([#23725](https://github.com/RocketChat/Rocket.Chat/pull/23725)) + + Since the endpoint supports both, department ID and department Name, so we're renaming it to reflect the same. `departmentName` -> `department` + +- Reduce complexity in some functions ([#23387](https://github.com/RocketChat/Rocket.Chat/pull/23387)) + + Overhauls all places where eslint's `complexity` rule is disabled. + +- Stricter API types ([#23735](https://github.com/RocketChat/Rocket.Chat/pull/23735)) + + It: + - Adds stricter types for `API`; + - Enables types for `urlParams`; + - Removes mandatory passage of `undefined` payload on client; + - Corrects some regressions; + - Reassures my belief in TypeScript supremacy. + +### 🐛 Bug fixes + + +- "to users" not working in export message ([#23576](https://github.com/RocketChat/Rocket.Chat/pull/23576)) + +- **ENTERPRISE:** OAuth "Merge Roles" removes roles from users ([#23588](https://github.com/RocketChat/Rocket.Chat/pull/23588)) + + - Fix OAuth "Merge Roles": the "Merge Roles" option now synchronize only the roles described in the "**Roles to Sync**" setting available in each Custom OAuth settings' group (instead of replacing users' roles by their OAuth roles); + - Fix "Merge Roles" and "Channel Mapping" not being performed/updated on OAuth login. + +- **ENTERPRISE:** Private rooms and discussions can't be audited ([#23673](https://github.com/RocketChat/Rocket.Chat/pull/23673)) + + - Add Private rooms (groups) and Discussions to the Message Auditing (Channels) autocomplete; + - Update "Channels" tab name to "Rooms". + +- **ENTERPRISE:** Replace all occurrences of a placeholder on string instead of just first one ([#23703](https://github.com/RocketChat/Rocket.Chat/pull/23703)) + +- Advanced LDAP Sync Features ([#23608](https://github.com/RocketChat/Rocket.Chat/pull/23608)) + +- App update flow failing in HA setups ([#23607](https://github.com/RocketChat/Rocket.Chat/pull/23607)) + + The flow for app updates is broken in specific scenarios with HA setups. Here we change the method calls in the Apps-Engine to avoid race conditions + +- Apps scheduler "losing" jobs after server restart ([#23566](https://github.com/RocketChat/Rocket.Chat/pull/23566)) + + If a job is scheduled and the server restarted, said job won't be executed, giving the impression it's been lost. + + What happens is that the scheduler is only started when some app tries to schedule an app - if that happens, all jobs that are "late" will be executed; if that doesn't happen, no job will run. + + This PR starts the apps scheduler right after all apps have been loaded + +- Autofocus on search input in admin ([#23738](https://github.com/RocketChat/Rocket.Chat/pull/23738)) + + Removed "generic" autofocus on sidenav template. + +- Await promise to handle error when attempting to transfer a room ([#23739](https://github.com/RocketChat/Rocket.Chat/pull/23739)) + +- broken avatar preview when changing avatar ([#23659](https://github.com/RocketChat/Rocket.Chat/pull/23659) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari)) + +- Discussions created inside discussions ([#23733](https://github.com/RocketChat/Rocket.Chat/pull/23733)) + +- Fix typo in FR translation ([#23711](https://github.com/RocketChat/Rocket.Chat/pull/23711) by [@Cormoran96](https://github.com/Cormoran96)) + +- Fixed E2E default room settings not being honoured ([#23468](https://github.com/RocketChat/Rocket.Chat/pull/23468) by [@TheDigitalEagle](https://github.com/TheDigitalEagle)) + +- LDAP users being disabled when an AD security policy is enabled ([#23820](https://github.com/RocketChat/Rocket.Chat/pull/23820)) + +- LDAP users not being re-activated on login ([#23627](https://github.com/RocketChat/Rocket.Chat/pull/23627)) + +- Missing user roles in edit user tab ([#23734](https://github.com/RocketChat/Rocket.Chat/pull/23734)) + +- New specific endpoint for contactChatHistoryMessages with right permissions ([#23533](https://github.com/RocketChat/Rocket.Chat/pull/23533)) + + Anyone with 'View Omnichannel Rooms' permission can see the History Messages. + +- Notifications are not being filtered ([#23487](https://github.com/RocketChat/Rocket.Chat/pull/23487)) + + - Add a migration to update the `Accounts_Default_User_Preferences_pushNotifications` setting's value to the `Accounts_Default_User_Preferences_mobileNotifications` setting's value; + - Remove the `Accounts_Default_User_Preferences_mobileNotifications` setting (replaced by `Accounts_Default_User_Preferences_pushNotifications`); + - Rename 'mobileNotifications' user's preference to 'pushNotifications'. + +- Omnichannel business hours page breaking navigation ([#23595](https://github.com/RocketChat/Rocket.Chat/pull/23595) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari)) + +- Omnichannel contact center navigation ([#23691](https://github.com/RocketChat/Rocket.Chat/pull/23691)) + + Derives from: https://github.com/RocketChat/Rocket.Chat/pull/23656 + + This PR includes a different approach to solving navigation problems following the same code structure and UI definitions of other "ActionButtons" components in Sidebar. + +- Omnichannel status being changed on page refresh ([#23587](https://github.com/RocketChat/Rocket.Chat/pull/23587)) + +- Omnichannel webhooks can't be saved ([#23641](https://github.com/RocketChat/Rocket.Chat/pull/23641) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari)) + +- Performance issues when running Omnichannel job queue dispatcher ([#23661](https://github.com/RocketChat/Rocket.Chat/pull/23661)) + +- PhotoSwipe crashing on show ([#23499](https://github.com/RocketChat/Rocket.Chat/pull/23499)) + + Waits for initial content to load before showing it. + +- Prevent UserAction.addStream without Subscription ([#23705](https://github.com/RocketChat/Rocket.Chat/pull/23705)) + + When you take an Omnichannel chat from queue, the guest's typing information will appear. + +- Registration not possible when any user is blocked for multiple failed logins ([#23565](https://github.com/RocketChat/Rocket.Chat/pull/23565)) + +
+🔍 Minor changes + + +- Chore: add `no-bidi` rule ([#23695](https://github.com/RocketChat/Rocket.Chat/pull/23695)) + +- Chore: add index on appId + associations for apps_persistence collection ([#23675](https://github.com/RocketChat/Rocket.Chat/pull/23675)) + +- Chore: Api definitions ([#23701](https://github.com/RocketChat/Rocket.Chat/pull/23701)) + +- Chore: Bump Rocket.Chat@livechat to 1.10 ([#23768](https://github.com/RocketChat/Rocket.Chat/pull/23768)) + +- Chore: Convert Fiber models to async Step 1 ([#23633](https://github.com/RocketChat/Rocket.Chat/pull/23633)) + +- Chore: Generic Table ([#23745](https://github.com/RocketChat/Rocket.Chat/pull/23745)) + +- Chore: Mocha testing configuration ([#23706](https://github.com/RocketChat/Rocket.Chat/pull/23706)) + + We've been writing integration tests for the REST API quite regularly, but we can't say the same for UI-related modules. This PR is based on the assumption that _improving the developer experience on writing tests_ would increase our coverage and promote the adoption even for newcomers. + + Here as summary of the proposal: + + - Change Mocha configuration files: + - Add a base configuration (`.mocharc.base.json`); + - Rename the configuration for REST API tests (`mocha_end_to_end.opts.js -> .mocharc.api.js`); + - Add a configuration for client modules (`.mocharc.client.js`); + - Enable ESLint for them. + - Add a Mocha test command exclusive for client modules (`npm run testunit-client`); + - Enable fast watch mode: + - Configure `ts-node` to only transpile code (skip type checking); + - Define a list of files to be watched. + - Configure `mocha` environment on ESLint only for test files (required when using Mocha's globals); + - Adopt Chai as our assertion library: + - Unify the setup of Chai plugins (`chai-spies`, `chai-datetime`, `chai-dom`); + - Replace `assert` with `chai`; + - Replace `chai.expect` with `expect`. + - Enable integration tests with React components: + - Enable JSX support on our default Babel configuration; + - Adopt [testing library](https://testing-library.com/). + +- Chore: Rearrange module typings ([#23452](https://github.com/RocketChat/Rocket.Chat/pull/23452)) + + - Move all external module declarations (definitions and augmentations) to `/definition/externals`; + - ~Symlink some modules on `/definition/externals` to `/ee/server/services/definition/externals`~ Share types with `/ee/server/services`; + - Use TypeScript as server code entrypoint. + +- Chore: Remove duplicated 'name' key from rate limiter logs ([#23771](https://github.com/RocketChat/Rocket.Chat/pull/23771)) + +- Chore: Remove useCallbacks ([#23696](https://github.com/RocketChat/Rocket.Chat/pull/23696)) + +- Chore: Type omnichannel models ([#23758](https://github.com/RocketChat/Rocket.Chat/pull/23758)) + +- Chore: Update settings.ts ([#23769](https://github.com/RocketChat/Rocket.Chat/pull/23769)) + +- i18n: Language update from LingoHub 🤖 on 2021-11-01Z ([#23603](https://github.com/RocketChat/Rocket.Chat/pull/23603)) + +- i18n: Language update from LingoHub 🤖 on 2021-11-29Z ([#23812](https://github.com/RocketChat/Rocket.Chat/pull/23812)) + +- Merge master into develop & Set version to 4.2.0-develop ([#23586](https://github.com/RocketChat/Rocket.Chat/pull/23586)) + +- Regression: Units endpoint to TS ([#23757](https://github.com/RocketChat/Rocket.Chat/pull/23757)) + +- Regression: "When is the chat busier" and "Users by time of day" charts are not working ([#23815](https://github.com/RocketChat/Rocket.Chat/pull/23815)) + + - Fix "When is the chat busier" (Hours) and "Users by time of day" charts, which weren't displaying any data; + +- Regression: Add @rocket.chat/emitter to EE services ([#23802](https://github.com/RocketChat/Rocket.Chat/pull/23802)) + +- Regression: Add trash to raw models ([#23774](https://github.com/RocketChat/Rocket.Chat/pull/23774)) + +- Regression: Current Chats not Filtering ([#23803](https://github.com/RocketChat/Rocket.Chat/pull/23803)) + +- Regression: Fix incorrect API path for livechat calls ([#23778](https://github.com/RocketChat/Rocket.Chat/pull/23778)) + +- Regression: Fix LDAP sync route ([#23775](https://github.com/RocketChat/Rocket.Chat/pull/23775)) + +- Regression: Fix sendMessagesToAdmins not in Fiber ([#23770](https://github.com/RocketChat/Rocket.Chat/pull/23770)) + +- Regression: Fix sort param on omnichannel endpoints ([#23789](https://github.com/RocketChat/Rocket.Chat/pull/23789)) + +- Regression: Improve AggregationCursor types ([#23692](https://github.com/RocketChat/Rocket.Chat/pull/23692)) + +- Regression: Include files on EE services build ([#23793](https://github.com/RocketChat/Rocket.Chat/pull/23793)) + +- Regression: Mark Livechat WebRTC video calling as alpha ([#23813](https://github.com/RocketChat/Rocket.Chat/pull/23813)) + + ![image](https://user-images.githubusercontent.com/34130764/143832378-82b99a72-23e8-4115-8b28-a0d210de598b.png) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@Aman-Maheshwari](https://github.com/Aman-Maheshwari) +- [@Cormoran96](https://github.com/Cormoran96) +- [@Deepak-learner](https://github.com/Deepak-learner) +- [@Jeanstaquet](https://github.com/Jeanstaquet) +- [@TheDigitalEagle](https://github.com/TheDigitalEagle) +- [@bhardwajaditya](https://github.com/bhardwajaditya) +- [@dhruvjain99](https://github.com/dhruvjain99) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@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) +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@ggazzo](https://github.com/ggazzo) +- [@matheusbsilva137](https://github.com/matheusbsilva137) +- [@murtaza98](https://github.com/murtaza98) +- [@ostjen](https://github.com/ostjen) +- [@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) + # 4.1.2 `2021-11-08 · 3 🐛 · 3 👩‍💻👨‍💻` @@ -72,7 +363,7 @@ ### 🚀 Improvements -- Add markdown to custom fields in user Info ([#20947](https://github.com/RocketChat/Rocket.Chat/pull/20947) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Add markdown to custom fields in user Info ([#20947](https://github.com/RocketChat/Rocket.Chat/pull/20947)) Added markdown to custom fields to render links @@ -300,7 +591,6 @@ - [@cuonghuunguyen](https://github.com/cuonghuunguyen) - [@dependabot[bot]](https://github.com/dependabot[bot]) - [@wolbernd](https://github.com/wolbernd) -- [@yash-rajpal](https://github.com/yash-rajpal) ### 👩‍💻👨‍💻 Core Team 🤓 @@ -320,6 +610,7 @@ - [@tassoevan](https://github.com/tassoevan) - [@thassiov](https://github.com/thassiov) - [@tiagoevanp](https://github.com/tiagoevanp) +- [@yash-rajpal](https://github.com/yash-rajpal) # 4.0.5 `2021-10-25 · 1 🐛 · 1 🔍 · 2 👩‍💻👨‍💻` @@ -2517,15 +2808,15 @@ - **ENTERPRISE:** Omnichannel Monitors can't forward chats to departments that they are not supervising ([#22142](https://github.com/RocketChat/Rocket.Chat/pull/22142)) -- Adding Custom Fields to show on user info check ([#20955](https://github.com/RocketChat/Rocket.Chat/pull/20955) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Adding Custom Fields to show on user info check ([#20955](https://github.com/RocketChat/Rocket.Chat/pull/20955)) The setting custom fields to show under user info was not being used when rendering fields in user info. This pr adds those checks and only renders the fields mentioned under in admin -> accounts -> Custom Fields to Show in User Info. -- Adding permission 'add-team-channel' for Team Channels Contextual bar ([#21591](https://github.com/RocketChat/Rocket.Chat/pull/21591) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Adding permission 'add-team-channel' for Team Channels Contextual bar ([#21591](https://github.com/RocketChat/Rocket.Chat/pull/21591)) Added 'add-team-channel' permission to the 2 buttons in team channels contextual bar, for adding channels to teams. -- Adding retentionEnabledDefault check before showing warning message ([#20692](https://github.com/RocketChat/Rocket.Chat/pull/20692) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Adding retentionEnabledDefault check before showing warning message ([#20692](https://github.com/RocketChat/Rocket.Chat/pull/20692)) Added check for retentionEnabledDefault before showing prune warning message. @@ -2787,7 +3078,7 @@ } ``` -- Visibility of burger menu on certain width ([#20736](https://github.com/RocketChat/Rocket.Chat/pull/20736) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Visibility of burger menu on certain width ([#20736](https://github.com/RocketChat/Rocket.Chat/pull/20736)) Burger was not visible on a certain width, specifically between 600 to 780. if width is more than 780px sidebar is shown, if less than 600 then burger icon was shown. But it wasn't shown between 600px to 780 px. It was because for showing burger icon we were only checking for `isMobile` which is lenght only less than 600. So i added one more check for condition if length is less than 780 px. @@ -2970,7 +3261,6 @@ - [@siva2204](https://github.com/siva2204) - [@sumukhah](https://github.com/sumukhah) - [@umakantv](https://github.com/umakantv) -- [@yash-rajpal](https://github.com/yash-rajpal) ### 👩‍💻👨‍💻 Core Team 🤓 @@ -2991,6 +3281,7 @@ - [@tassoevan](https://github.com/tassoevan) - [@thassiov](https://github.com/thassiov) - [@tiagoevanp](https://github.com/tiagoevanp) +- [@yash-rajpal](https://github.com/yash-rajpal) # 3.14.5 `2021-06-06 · 1 🚀 · 1 🐛 · 1 👩‍💻👨‍💻` @@ -3340,7 +3631,7 @@ ![image](https://user-images.githubusercontent.com/17487063/113359447-2d1b5500-931e-11eb-81fa-86f60fcee3a9.png) -- Checking 'start-discussion' Permission for MessageBox Actions ([#21564](https://github.com/RocketChat/Rocket.Chat/pull/21564) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Checking 'start-discussion' Permission for MessageBox Actions ([#21564](https://github.com/RocketChat/Rocket.Chat/pull/21564)) Permissions 'start-discussion-other-user' and 'start-discussion' are checked everywhere before letting anyone start any discussions, this permission check was missing for message box actions, so added it. @@ -3604,7 +3895,6 @@ - [@sauravjoshi23](https://github.com/sauravjoshi23) - [@sumukhah](https://github.com/sumukhah) - [@wolbernd](https://github.com/wolbernd) -- [@yash-rajpal](https://github.com/yash-rajpal) ### 👩‍💻👨‍💻 Core Team 🤓 @@ -3625,6 +3915,7 @@ - [@tassoevan](https://github.com/tassoevan) - [@thassiov](https://github.com/thassiov) - [@tiagoevanp](https://github.com/tiagoevanp) +- [@yash-rajpal](https://github.com/yash-rajpal) # 3.13.5 `2021-05-27 · 1 🐛 · 1 👩‍💻👨‍💻` @@ -3947,7 +4238,7 @@ - Add missing `unreads` field to `users.info` REST endpoint ([#20905](https://github.com/RocketChat/Rocket.Chat/pull/20905)) -- Added hideUnreadStatus check before showing unread messages on roomList ([#20867](https://github.com/RocketChat/Rocket.Chat/pull/20867) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Added hideUnreadStatus check before showing unread messages on roomList ([#20867](https://github.com/RocketChat/Rocket.Chat/pull/20867)) Added hide unread counter check, if the show unread messages is turned off, now unread messages badge won't be shown to user. @@ -4070,7 +4361,7 @@ - Replace wrong field description on Room Information panel ([#21395](https://github.com/RocketChat/Rocket.Chat/pull/21395) by [@rafaelblink](https://github.com/rafaelblink)) -- Reply count of message is decreased after a message from thread is deleted ([#19977](https://github.com/RocketChat/Rocket.Chat/pull/19977) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Reply count of message is decreased after a message from thread is deleted ([#19977](https://github.com/RocketChat/Rocket.Chat/pull/19977)) The reply count now is decreased if a message from a thread is deleted. @@ -4287,7 +4578,7 @@ - Regression: When only 'teams' type is provided, show only rooms with teamMain on `rooms.adminRooms` endpoint ([#21322](https://github.com/RocketChat/Rocket.Chat/pull/21322)) -- Release 3.13.0 ([#21437](https://github.com/RocketChat/Rocket.Chat/pull/21437) by [@PriyaBihani](https://github.com/PriyaBihani) & [@cuonghuunguyen](https://github.com/cuonghuunguyen) & [@fcecagno](https://github.com/fcecagno) & [@lucassartor](https://github.com/lucassartor) & [@shrinish123](https://github.com/shrinish123) & [@yash-rajpal](https://github.com/yash-rajpal)) +- Release 3.13.0 ([#21437](https://github.com/RocketChat/Rocket.Chat/pull/21437) by [@PriyaBihani](https://github.com/PriyaBihani) & [@cuonghuunguyen](https://github.com/cuonghuunguyen) & [@fcecagno](https://github.com/fcecagno) & [@lucassartor](https://github.com/lucassartor) & [@shrinish123](https://github.com/shrinish123)) - Update Apps-Engine version ([#21398](https://github.com/RocketChat/Rocket.Chat/pull/21398)) @@ -4315,7 +4606,6 @@ - [@shrinish123](https://github.com/shrinish123) - [@sumukhah](https://github.com/sumukhah) - [@vova-zush](https://github.com/vova-zush) -- [@yash-rajpal](https://github.com/yash-rajpal) ### 👩‍💻👨‍💻 Core Team 🤓 @@ -4336,6 +4626,7 @@ - [@sampaiodiego](https://github.com/sampaiodiego) - [@tassoevan](https://github.com/tassoevan) - [@tiagoevanp](https://github.com/tiagoevanp) +- [@yash-rajpal](https://github.com/yash-rajpal) # 3.12.7 `2021-05-27 · 1 🐛 · 1 👩‍💻👨‍💻` @@ -4411,7 +4702,7 @@ ### 🚀 Improvements -- Close Call contextual bar after starting jitsi call. ([#21004](https://github.com/RocketChat/Rocket.Chat/pull/21004) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Close Call contextual bar after starting jitsi call. ([#21004](https://github.com/RocketChat/Rocket.Chat/pull/21004)) After jitsi call is started, if the call is started in a new window then we should close contextual tab bar. So, when 'YES' is pressed on modal, we call handleClose function if openNewWindow is true, as call doesn't starts on tab bar, it starts on new window. @@ -4421,19 +4712,16 @@ - Missing spaces on attachment ([#21020](https://github.com/RocketChat/Rocket.Chat/pull/21020)) -- Stopping Jitsi reload ([#20973](https://github.com/RocketChat/Rocket.Chat/pull/20973) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Stopping Jitsi reload ([#20973](https://github.com/RocketChat/Rocket.Chat/pull/20973)) The Function where Jitsi call is started gets called many times due to `room.usernames` dep of useMemo, this dep triggers reloading of this function many times. So removing this dep from useMemo dependencies -### 👩‍💻👨‍💻 Contributors 😍 - -- [@yash-rajpal](https://github.com/yash-rajpal) - ### 👩‍💻👨‍💻 Core Team 🤓 - [@dougfabris](https://github.com/dougfabris) - [@tassoevan](https://github.com/tassoevan) +- [@yash-rajpal](https://github.com/yash-rajpal) # 3.12.0 `2021-02-28 · 5 🎉 · 17 🚀 · 74 🐛 · 30 🔍 · 29 👩‍💻👨‍💻` @@ -4482,15 +4770,15 @@ - Added auto-focus for better user-experience. ([#19954](https://github.com/RocketChat/Rocket.Chat/pull/19954) by [@Darshilp326](https://github.com/Darshilp326)) -- Added disable button check for send invite button ([#20337](https://github.com/RocketChat/Rocket.Chat/pull/20337) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Added disable button check for send invite button ([#20337](https://github.com/RocketChat/Rocket.Chat/pull/20337)) Added Disable check for send invite button. If the text field is empty button would be disabled, and after any valid email is filled, button would get enabled -- Added key prop, removing unwanted warnings ([#20473](https://github.com/RocketChat/Rocket.Chat/pull/20473) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Added key prop, removing unwanted warnings ([#20473](https://github.com/RocketChat/Rocket.Chat/pull/20473)) Removes warnings listed on the issue -- Added Markdown links to custom status. ([#20470](https://github.com/RocketChat/Rocket.Chat/pull/20470) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Added Markdown links to custom status. ([#20470](https://github.com/RocketChat/Rocket.Chat/pull/20470)) Added markdown links to user's custom status. @@ -4516,7 +4804,7 @@ It brings more flexibility, allowing us to use different hooks and different components for each header -- Check Livechat message length through REST API endpoint ([#20366](https://github.com/RocketChat/Rocket.Chat/pull/20366) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Check Livechat message length through REST API endpoint ([#20366](https://github.com/RocketChat/Rocket.Chat/pull/20366)) Added checks for message length for livechat message api, it shouldn't exceed specified character limit. @@ -4565,21 +4853,21 @@ Added tooltips to "Expand" and "Follow Message"/"Unfollow Message" in ThreadView for coherency. -- Added Bio Structure for UserCard, rendering Skeleton View on loading Instead of [Object][Object] ([#20305](https://github.com/RocketChat/Rocket.Chat/pull/20305) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Added Bio Structure for UserCard, rendering Skeleton View on loading Instead of [Object][Object] ([#20305](https://github.com/RocketChat/Rocket.Chat/pull/20305)) Added Bio Structure for rendering Skeleton View on loading UserCard. -- Added check for view admin permission page ([#20403](https://github.com/RocketChat/Rocket.Chat/pull/20403) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Added check for view admin permission page ([#20403](https://github.com/RocketChat/Rocket.Chat/pull/20403)) Admin Permission page was visible to all, if you add admin/permissions after the base url. This should not be visible to all user, only people with certain permissions should be able to see this page. I am also able to see permissions page for open workspace of Rocket chat. ![image](https://user-images.githubusercontent.com/58601732/105829728-bfd00880-5fea-11eb-9121-6c53a752f140.png) -- Adding the accidentally deleted tag template, used by other templates ([#20772](https://github.com/RocketChat/Rocket.Chat/pull/20772) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Adding the accidentally deleted tag template, used by other templates ([#20772](https://github.com/RocketChat/Rocket.Chat/pull/20772)) Adding back accidentally deleted tag Template. -- Admin cannot clear user details like bio or nickname ([#20785](https://github.com/RocketChat/Rocket.Chat/pull/20785) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Admin cannot clear user details like bio or nickname ([#20785](https://github.com/RocketChat/Rocket.Chat/pull/20785)) When the API users.update is called to update user data, it passes data to saveUser function. Here before saving data like bio or nickname we are checking if they are available or not. If data is available then we are saving it, but we are not doing anything when data isn't available. @@ -4587,13 +4875,13 @@ - Admin Panel pages not visible in Safari ([#20912](https://github.com/RocketChat/Rocket.Chat/pull/20912)) -- Announcement with multiple lines fixed. ([#20381](https://github.com/RocketChat/Rocket.Chat/pull/20381) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Announcement with multiple lines fixed. ([#20381](https://github.com/RocketChat/Rocket.Chat/pull/20381)) Announcements with multiple lines used to break UI for announcements bar. Fixed it by replacing all break lines in announcement with empty space (" ") . The announcement modal would work as usual and show all break lines. - Atlassian Crowd login with 2FA enabled ([#20834](https://github.com/RocketChat/Rocket.Chat/pull/20834)) -- Attachment download from title fixed ([#20585](https://github.com/RocketChat/Rocket.Chat/pull/20585) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Attachment download from title fixed ([#20585](https://github.com/RocketChat/Rocket.Chat/pull/20585)) Added target = '_self' to attachment link, this seems to fix the problem, without this attribute, error page is displayed. @@ -4684,7 +4972,7 @@ ![image](https://user-images.githubusercontent.com/2493803/106494751-90f9dc80-6499-11eb-901b-5e4dbdc678ba.png) -- Fix Empty highlighted words field ([#20329](https://github.com/RocketChat/Rocket.Chat/pull/20329) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Fix Empty highlighted words field ([#20329](https://github.com/RocketChat/Rocket.Chat/pull/20329)) Able to Empty the highlighted text field in preferences @@ -4740,7 +5028,7 @@ - Add a new setting ("Add Reply-To header") in the Email settings' page to control when the Reply-To header is used in e-mail notifications; - The new setting is turned off (`false` value) by default. -- New Integration page was not being displayed ([#20670](https://github.com/RocketChat/Rocket.Chat/pull/20670) by [@yash-rajpal](https://github.com/yash-rajpal)) +- New Integration page was not being displayed ([#20670](https://github.com/RocketChat/Rocket.Chat/pull/20670)) - Notification worker stopping on error ([#20605](https://github.com/RocketChat/Rocket.Chat/pull/20605)) @@ -4954,7 +5242,6 @@ - [@paulobernardoaf](https://github.com/paulobernardoaf) - [@pierreozoux](https://github.com/pierreozoux) - [@rafaelblink](https://github.com/rafaelblink) -- [@yash-rajpal](https://github.com/yash-rajpal) ### 👩‍💻👨‍💻 Core Team 🤓 @@ -4973,6 +5260,7 @@ - [@sampaiodiego](https://github.com/sampaiodiego) - [@tassoevan](https://github.com/tassoevan) - [@tiagoevanp](https://github.com/tiagoevanp) +- [@yash-rajpal](https://github.com/yash-rajpal) # 3.11.5 `2021-04-20 · 1 🐛 · 1 👩‍💻👨‍💻` @@ -5035,7 +5323,7 @@ ### 🐛 Bug fixes -- Attachment download from title fixed ([#20585](https://github.com/RocketChat/Rocket.Chat/pull/20585) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Attachment download from title fixed ([#20585](https://github.com/RocketChat/Rocket.Chat/pull/20585)) Added target = '_self' to attachment link, this seems to fix the problem, without this attribute, error page is displayed. @@ -5054,7 +5342,6 @@ ### 👩‍💻👨‍💻 Contributors 😍 - [@lolimay](https://github.com/lolimay) -- [@yash-rajpal](https://github.com/yash-rajpal) ### 👩‍💻👨‍💻 Core Team 🤓 @@ -5062,6 +5349,7 @@ - [@renatobecker](https://github.com/renatobecker) - [@sampaiodiego](https://github.com/sampaiodiego) - [@tiagoevanp](https://github.com/tiagoevanp) +- [@yash-rajpal](https://github.com/yash-rajpal) # 3.11.0 `2021-01-31 · 8 🎉 · 9 🚀 · 52 🐛 · 44 🔍 · 32 👩‍💻👨‍💻` @@ -5167,7 +5455,7 @@ Made user avatar change buttons to be descriptive of what they do. -- Tooltip added for Kebab menu on chat header ([#20116](https://github.com/RocketChat/Rocket.Chat/pull/20116) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Tooltip added for Kebab menu on chat header ([#20116](https://github.com/RocketChat/Rocket.Chat/pull/20116)) Added the missing Tooltip for kebab menu on chat header. ![tooltip after](https://user-images.githubusercontent.com/58601732/104031406-b07f4b80-51f2-11eb-87a4-1e8da78a254f.gif) @@ -5189,12 +5477,12 @@ Users can be removed from channels without any error message. -- Added context check for closing active tabbar for member-list ([#20228](https://github.com/RocketChat/Rocket.Chat/pull/20228) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Added context check for closing active tabbar for member-list ([#20228](https://github.com/RocketChat/Rocket.Chat/pull/20228)) When we click on a username and then click on see user's full profile, a tab gets active and shows us the user's profile, the problem occurs when the tab is still active and we try to see another user's profile. In this case, tabbar gets closed. To resolve this, added context check for closing action of active tabbar. -- Added Margin between status bullet and status label ([#20199](https://github.com/RocketChat/Rocket.Chat/pull/20199) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Added Margin between status bullet and status label ([#20199](https://github.com/RocketChat/Rocket.Chat/pull/20199)) Added Margins between status bullet and status label @@ -5259,7 +5547,7 @@ After changes made on https://github.com/RocketChat/Rocket.Chat/pull/19931, the `Livechat.RegisterGuest` method started removing properties from the visitor inappropriately. The properties that did not receive value were removed from the object. Those changes were made to support the new Contact Form, but now the form has its own method to deal with Contact data so those changes are no longer necessary. -- Markdown added for Header Room topic ([#20021](https://github.com/RocketChat/Rocket.Chat/pull/20021) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Markdown added for Header Room topic ([#20021](https://github.com/RocketChat/Rocket.Chat/pull/20021)) With the new 3.10.0 version update the Links in topic section below room name were not working, for more info refer issue #20018 @@ -5339,7 +5627,7 @@ ![image](https://user-images.githubusercontent.com/27704687/106056093-0a29b600-60cd-11eb-8038-eabbc0d8fb03.png) -- Status circle in profile section ([#20016](https://github.com/RocketChat/Rocket.Chat/pull/20016) by [@yash-rajpal](https://github.com/yash-rajpal)) +- Status circle in profile section ([#20016](https://github.com/RocketChat/Rocket.Chat/pull/20016)) The Status Circle in status message text input is now centered vertically. @@ -5543,7 +5831,6 @@ - [@sushant52](https://github.com/sushant52) - [@tlskinneriv](https://github.com/tlskinneriv) - [@wggdeveloper](https://github.com/wggdeveloper) -- [@yash-rajpal](https://github.com/yash-rajpal) - [@zdumitru](https://github.com/zdumitru) ### 👩‍💻👨‍💻 Core Team 🤓 @@ -5561,6 +5848,7 @@ - [@tassoevan](https://github.com/tassoevan) - [@thassiov](https://github.com/thassiov) - [@tiagoevanp](https://github.com/tiagoevanp) +- [@yash-rajpal](https://github.com/yash-rajpal) # 3.10.5 `2021-01-27 · 1 🐛 · 1 👩‍💻👨‍💻` @@ -6401,7 +6689,7 @@ - [@sampaiodiego](https://github.com/sampaiodiego) # 3.8.0 -`2020-11-13 · 14 🎉 · 4 🚀 · 40 🐛 · 54 🔍 · 30 👩‍💻👨‍💻` +`2020-11-14 · 14 🎉 · 4 🚀 · 40 🐛 · 54 🔍 · 30 👩‍💻👨‍💻` ### Engine versions - Node: `12.18.4` diff --git a/app/api/server/api.d.ts b/app/api/server/api.d.ts index 9e968448bbc0..b66da40e4394 100644 --- a/app/api/server/api.d.ts +++ b/app/api/server/api.d.ts @@ -1,6 +1,179 @@ -import { APIClass } from '.'; +import type { JoinPathPattern, Method, MethodOf, OperationParams, OperationResult, PathPattern, UrlParams } from '../../../definition/rest'; +import type { IUser } from '../../../definition/IUser'; +import { IMethodConnection } from '../../../definition/IMethodThisType'; +import { ITwoFactorOptions } from '../../2fa/server/code'; + +type SuccessResult = { + statusCode: 200; + body: + T extends object + ? { success: true } & T + : T; +}; + +type FailureResult = { + statusCode: 400; + body: + T extends object + ? { success: false } & T + : ({ + success: false; + error: T; + stack: TStack; + errorType: TErrorType; + details: TErrorDetails; + }) & ( + undefined extends TErrorType + ? {} + : { errorType: TErrorType } + ) & ( + undefined extends TErrorDetails + ? {} + : { details: TErrorDetails extends string ? unknown : TErrorDetails } + ); +}; + +type UnauthorizedResult = { + statusCode: 403; + body: { + success: false; + error: T | 'unauthorized'; + }; +} + +export type NonEnterpriseTwoFactorOptions = { + authRequired: true; + forceTwoFactorAuthenticationForNonEnterprise: true; + twoFactorRequired: true; + permissionsRequired?: string[]; + twoFactorOptions: ITwoFactorOptions; +} + +type Options = { + permissionsRequired?: string[]; + authRequired?: boolean; + forceTwoFactorAuthenticationForNonEnterprise?: boolean; +} | { + authRequired: true; + twoFactorRequired: true; + twoFactorOptions?: ITwoFactorOptions; +} + +type Request = { + method: 'GET' | 'POST' | 'PUT' | 'DELETE'; + url: string; + headers: Record; + body: any; +} + +type ActionThis = { + urlParams: UrlParams; + // TODO make it unsafe + readonly queryParams: TMethod extends 'GET' ? Partial> : Record; + // TODO make it unsafe + readonly bodyParams: TMethod extends 'GET' ? Record : Partial>; + requestParams(): OperationParams; + getPaginationItems(): { + readonly offset: number; + readonly count: number; + }; + parseJsonQuery(): { + sort: Record; + fields: Record; + query: Record; + }; + getUserFromParams(): IUser; +} & ( + TOptions extends { authRequired: true } + ? { + readonly user: IUser; + readonly userId: string; + } + : { + readonly user: null; + readonly userId: null; + } +); + +export type ResultFor< + TMethod extends Method, + TPathPattern extends PathPattern +> = SuccessResult> | FailureResult | UnauthorizedResult; + +type Action = + ((this: ActionThis) => Promise>) + | ((this: ActionThis) => ResultFor); + +type Operation = Action | { + action: Action; +} & ({ twoFactorRequired: boolean }); + +type Operations = { + [M in MethodOf as Lowercase]: Operation, TPathPattern, TOptions>; +}; + +declare class APIClass { + processTwoFactor({ userId, request, invocation, options, connection }: { userId: string; request: Request; invocation: {twoFactorChecked: boolean}; options?: Options; connection: IMethodConnection }): void; + + addRoute< + TSubPathPattern extends string + >(subpath: TSubPathPattern, operations: Operations>): void; + + addRoute< + TSubPathPattern extends string, + TPathPattern extends JoinPathPattern + >(subpaths: TSubPathPattern[], operations: Operations): void; + + addRoute< + TSubPathPattern extends string, + TOptions extends Options + >( + subpath: TSubPathPattern, + options: TOptions, + operations: Operations, TOptions> + ): void; + + addRoute< + TSubPathPattern extends string, + TPathPattern extends JoinPathPattern, + TOptions extends Options + >( + subpaths: TSubPathPattern[], + options: TOptions, + operations: Operations + ): void; + + success(result: T): SuccessResult; + + success(): SuccessResult; + + failure< + T, + TErrorType extends string, + TStack extends string, + TErrorDetails + >( + result: T, + errorType?: TErrorType, + stack?: TStack, + error?: { details: TErrorDetails } + ): FailureResult; + + failure(result: T): FailureResult; + + failure(): FailureResult; + + unauthorized(msg?: T): UnauthorizedResult; + + defaultFieldsToExclude: { + joinCode: 0; + members: 0; + importIds: 0; + e2e: 0; + } +} export declare const API: { - v1: APIClass; + v1: APIClass<'/v1'>; default: APIClass; }; diff --git a/app/api/server/api.js b/app/api/server/api.js index 3b1d6bdfbb9e..43241eda2821 100644 --- a/app/api/server/api.js +++ b/app/api/server/api.js @@ -4,8 +4,8 @@ import { DDPCommon } from 'meteor/ddp-common'; import { DDP } from 'meteor/ddp'; import { Accounts } from 'meteor/accounts-base'; import { Restivus } from 'meteor/nimble:restivus'; -import { RateLimiter } from 'meteor/rate-limit'; import _ from 'underscore'; +import { RateLimiter } from 'meteor/rate-limit'; import { Logger } from '../../../server/lib/logger/Logger'; import { getRestPayload } from '../../../server/lib/logger/logPayloads'; @@ -273,10 +273,13 @@ export class APIClass extends Restivus { } processTwoFactor({ userId, request, invocation, options, connection }) { + if (!options.twoFactorRequired) { + return; + } const code = request.headers['x-2fa-code']; const method = request.headers['x-2fa-method']; - checkCodeForUser({ user: userId, code, method, options, connection }); + checkCodeForUser({ user: userId, code, method, options: options.twoFactorOptions, connection }); invocation.twoFactorChecked = true; } @@ -399,11 +402,9 @@ export class APIClass extends Restivus { }; Accounts._setAccountData(connection.id, 'loginToken', this.token); - if (_options.twoFactorRequired) { - api.processTwoFactor({ userId: this.userId, request: this.request, invocation, options: _options.twoFactorOptions, connection }); - } + api.processTwoFactor({ userId: this.userId, request: this.request, invocation, options: _options, connection }); - result = DDP._CurrentInvocation.withValue(invocation, () => originalAction.apply(this)) || API.v1.success(); + result = DDP._CurrentInvocation.withValue(invocation, () => Promise.await(originalAction.apply(this))) || API.v1.success(); log.http({ status: result.statusCode, @@ -447,6 +448,14 @@ export class APIClass extends Restivus { }); } + updateRateLimiterDictionaryForRoute(route, numRequestsAllowed, intervalTimeInMS) { + if (rateLimiterDictionary[route]) { + rateLimiterDictionary[route].options.numRequestsAllowed = numRequestsAllowed ?? rateLimiterDictionary[route].options.numRequestsAllowed; + rateLimiterDictionary[route].options.intervalTimeInMS = intervalTimeInMS ?? rateLimiterDictionary[route].options.intervalTimeInMS; + API.v1.reloadRoutesToRefreshRateLimiter(); + } + } + _initAuth() { const loginCompatibility = (bodyParams, request) => { // Grab the username or email that the user is logging in with @@ -771,6 +780,7 @@ settings.watch('API_Enable_Rate_Limiter_Limit_Calls_Default', (value) => { API.v1.reloadRoutesToRefreshRateLimiter(); }); + settings.watch('Prometheus_API_User_Agent', (value) => { prometheusAPIUserAgent = value; }); diff --git a/app/api/server/helpers/deprecationWarning.ts b/app/api/server/helpers/deprecationWarning.ts index edb347cd33b3..bfee0827733d 100644 --- a/app/api/server/helpers/deprecationWarning.ts +++ b/app/api/server/helpers/deprecationWarning.ts @@ -1,7 +1,7 @@ import { API } from '../api'; import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -(API as any).helperMethods.set('deprecationWarning', function _deprecationWarning({ endpoint, versionWillBeRemoved, response }: { endpoint: string; versionWillBeRemoved: string; response: any }) { +export function deprecationWarning({ endpoint, versionWillBeRemoved = '5.0', response }: { endpoint: string; versionWillBeRemoved?: string; response: T }): T { const warningMessage = `The endpoint "${ endpoint }" is deprecated and will be removed after version ${ versionWillBeRemoved }`; apiDeprecationLogger.warn(warningMessage); if (process.env.NODE_ENV === 'development') { @@ -12,4 +12,6 @@ import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarning } return response; -}); +} + +(API as any).helperMethods.set('deprecationWarning', deprecationWarning); diff --git a/app/api/server/helpers/getPaginationItems.js b/app/api/server/helpers/getPaginationItems.js index 0cff491a9763..259f79a1191a 100644 --- a/app/api/server/helpers/getPaginationItems.js +++ b/app/api/server/helpers/getPaginationItems.js @@ -10,7 +10,7 @@ API.helperMethods.set('getPaginationItems', function _getPaginationItems() { const offset = this.queryParams.offset ? parseInt(this.queryParams.offset) : 0; let count = defaultCount; - // Ensure count is an appropiate amount + // Ensure count is an appropriate amount if (typeof this.queryParams.count !== 'undefined') { count = parseInt(this.queryParams.count); } else { diff --git a/app/api/server/lib/integrations.js b/app/api/server/lib/integrations.ts similarity index 65% rename from app/api/server/lib/integrations.js rename to app/api/server/lib/integrations.ts index 55db33a636a5..ef5cab57ed94 100644 --- a/app/api/server/lib/integrations.js +++ b/app/api/server/lib/integrations.ts @@ -1,7 +1,9 @@ import { Integrations } from '../../../models/server/raw'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { IIntegration } from '../../../../definition/IIntegration'; +import { IUser } from '../../../../definition/IUser'; -const hasIntegrationsPermission = async (userId, integration) => { +const hasIntegrationsPermission = async (userId: string, integration: IIntegration): Promise => { const type = integration.type === 'webhook-incoming' ? 'incoming' : 'outgoing'; if (await hasPermissionAsync(userId, `manage-${ type }-integrations`)) { @@ -15,7 +17,15 @@ const hasIntegrationsPermission = async (userId, integration) => { return false; }; -export const findOneIntegration = async ({ userId, integrationId, createdBy }) => { +export const findOneIntegration = async ({ + userId, + integrationId, + createdBy, +}: { + userId: string; + integrationId: string; + createdBy: IUser; +}): Promise => { const integration = await Integrations.findOneByIdAndCreatedByIfExists({ _id: integrationId, createdBy }); if (!integration) { throw new Error('The integration does not exists.'); diff --git a/app/api/server/lib/rooms.js b/app/api/server/lib/rooms.js index ea184b1d2fa2..a841973e02db 100644 --- a/app/api/server/lib/rooms.js +++ b/app/api/server/lib/rooms.js @@ -1,4 +1,4 @@ -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { hasPermissionAsync, hasAtLeastOnePermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { Rooms } from '../../../models/server/raw'; import { Subscriptions } from '../../../models/server'; @@ -119,6 +119,31 @@ export async function findChannelAndPrivateAutocomplete({ uid, selector }) { }; } +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() diff --git a/app/api/server/lib/webdav.js b/app/api/server/lib/webdav.js deleted file mode 100644 index cf5a3c8ea8f1..000000000000 --- a/app/api/server/lib/webdav.js +++ /dev/null @@ -1,14 +0,0 @@ -import { WebdavAccounts } from '../../../models/server/raw'; - -export async function findWebdavAccountsByUserId({ uid }) { - return { - accounts: await WebdavAccounts.findWithUserId(uid, { - fields: { - _id: 1, - username: 1, - server_url: 1, - name: 1, - }, - }).toArray(), - }; -} diff --git a/app/api/server/lib/webdav.ts b/app/api/server/lib/webdav.ts new file mode 100644 index 000000000000..fe2f17185bb6 --- /dev/null +++ b/app/api/server/lib/webdav.ts @@ -0,0 +1,16 @@ +import { WebdavAccounts } from '../../../models/server/raw'; +import { IWebdavAccount } from '../../../../definition/IWebdavAccount'; + +export async function findWebdavAccountsByUserId({ uid }: { uid: string }): Promise<{ accounts: IWebdavAccount[] }> { + return { + accounts: await WebdavAccounts.findWithUserId(uid, { + projection: { + _id: 1, + username: 1, + // eslint-disable-next-line @typescript-eslint/camelcase + server_url: 1, + name: 1, + }, + }).toArray(), + }; +} diff --git a/app/api/server/v1/banners.ts b/app/api/server/v1/banners.ts index e56b6a5178cb..4e740cdf2c90 100644 --- a/app/api/server/v1/banners.ts +++ b/app/api/server/v1/banners.ts @@ -1,5 +1,3 @@ -import { Promise } from 'meteor/promise'; -import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { API } from '../api'; @@ -53,22 +51,15 @@ import { BannerPlatform } from '../../../../definition/IBanner'; * $ref: '#/components/schemas/ApiFailureV1' */ API.v1.addRoute('banners.getNew', { authRequired: true }, { // deprecated - get() { + async get() { check(this.queryParams, Match.ObjectIncluding({ - platform: String, + platform: Match.OneOf(...Object.values(BannerPlatform)), bid: Match.Maybe(String), })); const { platform, bid: bannerId } = this.queryParams; - if (!platform) { - throw new Meteor.Error('error-missing-param', 'The required "platform" param is missing.'); - } - if (!Object.values(BannerPlatform).includes(platform)) { - throw new Meteor.Error('error-unknown-platform', 'Platform is unknown.'); - } - - const banners = Promise.await(Banner.getBannersForUser(this.userId, platform, bannerId)); + const banners = await Banner.getBannersForUser(this.userId, platform, bannerId ?? undefined); return API.v1.success({ banners }); }, @@ -120,23 +111,19 @@ API.v1.addRoute('banners.getNew', { authRequired: true }, { // deprecated * schema: * $ref: '#/components/schemas/ApiFailureV1' */ -API.v1.addRoute('banners/:id', { authRequired: true }, { - get() { +API.v1.addRoute('banners/:id', { authRequired: true }, { // TODO: move to users/:id/banners + async get() { check(this.urlParams, Match.ObjectIncluding({ - id: String, + id: Match.Where((id: unknown): id is string => typeof id === 'string' && Boolean(id.trim())), + })); + check(this.queryParams, Match.ObjectIncluding({ + platform: Match.OneOf(...Object.values(BannerPlatform)), })); const { platform } = this.queryParams; - if (!platform) { - throw new Meteor.Error('error-missing-param', 'The required "platform" param is missing.'); - } - const { id } = this.urlParams; - if (!id) { - throw new Meteor.Error('error-missing-param', 'The required "id" param is missing.'); - } - const banners = Promise.await(Banner.getBannersForUser(this.userId, platform, id)); + const banners = await Banner.getBannersForUser(this.userId, platform, id); return API.v1.success({ banners }); }, @@ -180,21 +167,14 @@ API.v1.addRoute('banners/:id', { authRequired: true }, { * $ref: '#/components/schemas/ApiFailureV1' */ API.v1.addRoute('banners', { authRequired: true }, { - get() { + async get() { check(this.queryParams, Match.ObjectIncluding({ - platform: String, + platform: Match.OneOf(...Object.values(BannerPlatform)), })); const { platform } = this.queryParams; - if (!platform) { - throw new Meteor.Error('error-missing-param', 'The required "platform" param is missing.'); - } - - if (!Object.values(BannerPlatform).includes(platform)) { - throw new Meteor.Error('error-unknown-platform', 'Platform is unknown.'); - } - const banners = Promise.await(Banner.getBannersForUser(this.userId, platform)); + const banners = await Banner.getBannersForUser(this.userId, platform); return API.v1.success({ banners }); }, @@ -234,18 +214,14 @@ API.v1.addRoute('banners', { authRequired: true }, { * $ref: '#/components/schemas/ApiFailureV1' */ API.v1.addRoute('banners.dismiss', { authRequired: true }, { - post() { + async post() { check(this.bodyParams, Match.ObjectIncluding({ - bannerId: String, + bannerId: Match.Where((id: unknown): id is string => typeof id === 'string' && Boolean(id.trim())), })); const { bannerId } = this.bodyParams; - if (!bannerId || !bannerId.trim()) { - throw new Meteor.Error('error-missing-param', 'The required "bannerId" param is missing.'); - } - - Promise.await(Banner.dismiss(this.userId, bannerId)); + await Banner.dismiss(this.userId, bannerId); return API.v1.success(); }, }); diff --git a/app/api/server/v1/channels.js b/app/api/server/v1/channels.js index 832aca11ad53..3e0139ec80b0 100644 --- a/app/api/server/v1/channels.js +++ b/app/api/server/v1/channels.js @@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import _ from 'underscore'; -import { Rooms, Subscriptions, Messages, Uploads, Integrations, Users } from '../../../models/server'; +import { Rooms, Subscriptions, Messages, Users } from '../../../models/server'; +import { Integrations, Uploads } from '../../../models/server/raw'; import { canAccessRoom, hasPermission, hasAtLeastOnePermission, hasAllPermission } from '../../../authorization/server'; import { mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; @@ -294,19 +295,19 @@ API.v1.addRoute('channels.files', { authRequired: true }, { const ourQuery = Object.assign({}, query, { rid: findResult._id }); - const files = Uploads.find(ourQuery, { + const files = Promise.await(Uploads.find(ourQuery, { sort: sort || { name: 1 }, skip: offset, limit: count, fields, - }).fetch(); + }).toArray()); return API.v1.success({ files: files.map(addUserObjectToEveryObject), count: files.length, offset, - total: Uploads.find(ourQuery).count(), + total: Promise.await(Uploads.find(ourQuery).count()), }); }, }); @@ -340,21 +341,24 @@ API.v1.addRoute('channels.getIntegrations', { authRequired: true }, { } const { offset, count } = this.getPaginationItems(); - const { sort, fields, query } = this.parseJsonQuery(); + const { sort, fields: projection, query } = this.parseJsonQuery(); ourQuery = Object.assign(mountIntegrationQueryBasedOnPermissions(this.userId), query, ourQuery); - const integrations = Integrations.find(ourQuery, { + const cursor = Integrations.find(ourQuery, { sort: sort || { _createdAt: 1 }, skip: offset, limit: count, - fields, - }).fetch(); + projection, + }); + + const integrations = Promise.await(cursor.toArray()); + const total = Promise.await(cursor.count()); return API.v1.success({ integrations, count: integrations.length, offset, - total: Integrations.find(ourQuery).count(), + total, }); }, }); diff --git a/app/api/server/v1/chat.js b/app/api/server/v1/chat.js index eba0e4e3f668..fa3e917665fc 100644 --- a/app/api/server/v1/chat.js +++ b/app/api/server/v1/chat.js @@ -697,7 +697,7 @@ API.v1.addRoute('chat.getSnippetedMessages', { authRequired: true }, { }); API.v1.addRoute('chat.getDiscussions', { authRequired: true }, { - get() { + async get() { const { roomId, text } = this.queryParams; const { sort } = this.parseJsonQuery(); const { offset, count } = this.getPaginationItems(); @@ -705,7 +705,7 @@ API.v1.addRoute('chat.getDiscussions', { authRequired: true }, { if (!roomId) { throw new Meteor.Error('error-invalid-params', 'The required "roomId" query param is missing.'); } - const messages = Promise.await(findDiscussionsFromRoom({ + const messages = await findDiscussionsFromRoom({ uid: this.userId, roomId, text, @@ -714,7 +714,7 @@ API.v1.addRoute('chat.getDiscussions', { authRequired: true }, { count, sort, }, - })); + }); return API.v1.success(messages); }, }); diff --git a/app/api/server/v1/dns.ts b/app/api/server/v1/dns.ts index 902ef90d4782..a0b0fa5788e6 100644 --- a/app/api/server/v1/dns.ts +++ b/app/api/server/v1/dns.ts @@ -48,7 +48,7 @@ import { resolveSRV, resolveTXT } from '../../../federation/server/functions/res * $ref: '#/components/schemas/ApiFailureV1' */ API.v1.addRoute('dns.resolve.srv', { authRequired: true }, { - get() { + async get() { check(this.queryParams, Match.ObjectIncluding({ url: String, })); @@ -58,7 +58,7 @@ API.v1.addRoute('dns.resolve.srv', { authRequired: true }, { throw new Meteor.Error('error-missing-param', 'The required "url" param is missing.'); } - const resolved = Promise.await(resolveSRV(url)); + const resolved = await resolveSRV(url); return API.v1.success({ resolved }); }, @@ -99,7 +99,7 @@ API.v1.addRoute('dns.resolve.srv', { authRequired: true }, { * $ref: '#/components/schemas/ApiFailureV1' */ API.v1.addRoute('dns.resolve.txt', { authRequired: true }, { - post() { + async post() { check(this.queryParams, Match.ObjectIncluding({ url: String, })); @@ -109,7 +109,7 @@ API.v1.addRoute('dns.resolve.txt', { authRequired: true }, { throw new Meteor.Error('error-missing-param', 'The required "url" param is missing.'); } - const resolved = Promise.await(resolveTXT(url)); + const resolved = await resolveTXT(url); return API.v1.success({ resolved }); }, diff --git a/app/api/server/v1/email-inbox.js b/app/api/server/v1/email-inbox.js index e7452fc5ffe1..61368a2d0a8a 100644 --- a/app/api/server/v1/email-inbox.js +++ b/app/api/server/v1/email-inbox.js @@ -3,7 +3,7 @@ import { check, Match } from 'meteor/check'; import { API } from '../api'; import { findEmailInboxes, findOneEmailInbox, insertOneOrUpdateEmailInbox } from '../lib/emailInbox'; import { hasPermission } from '../../../authorization/server/functions/hasPermission'; -import { EmailInbox } from '../../../models'; +import { EmailInbox } from '../../../models/server/raw'; import Users from '../../../models/server/models/Users'; import { sendTestEmailToInbox } from '../../../../server/features/EmailInbox/EmailInbox_Outgoing'; @@ -79,12 +79,12 @@ API.v1.addRoute('email-inbox/:_id', { authRequired: true }, { const { _id } = this.urlParams; if (!_id) { throw new Error('error-invalid-param'); } - const emailInboxes = EmailInbox.findOneById(_id); + const emailInboxes = Promise.await(EmailInbox.findOneById(_id)); if (!emailInboxes) { return API.v1.notFound(); } - EmailInbox.removeById(_id); + Promise.await(EmailInbox.removeById(_id)); return API.v1.success({ _id }); }, }); diff --git a/app/api/server/v1/emoji-custom.js b/app/api/server/v1/emoji-custom.js index 403cca1d189c..092e41c1de97 100644 --- a/app/api/server/v1/emoji-custom.js +++ b/app/api/server/v1/emoji-custom.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { EmojiCustom } from '../../../models/server'; +import { EmojiCustom } from '../../../models/server/raw'; import { API } from '../api'; import { getUploadFormData } from '../lib/getUploadFormData'; import { findEmojisCustom } from '../lib/emoji-custom'; @@ -19,15 +19,15 @@ API.v1.addRoute('emoji-custom.list', { authRequired: true }, { } return API.v1.success({ emojis: { - update: EmojiCustom.find({ ...query, _updatedAt: { $gt: updatedSinceDate } }).fetch(), - remove: EmojiCustom.trashFindDeletedAfter(updatedSinceDate).fetch(), + update: Promise.await(EmojiCustom.find({ ...query, _updatedAt: { $gt: updatedSinceDate } }).toArray()), + remove: Promise.await(EmojiCustom.trashFindDeletedAfter(updatedSinceDate).toArray()), }, }); } return API.v1.success({ emojis: { - update: EmojiCustom.find(query).fetch(), + update: Promise.await(EmojiCustom.find(query).toArray()), remove: [], }, }); @@ -88,7 +88,7 @@ API.v1.addRoute('emoji-custom.update', { authRequired: true }, { throw new Meteor.Error('The required "_id" query param is missing.'); } - const emojiToUpdate = EmojiCustom.findOneById(fields._id); + const emojiToUpdate = Promise.await(EmojiCustom.findOneById(fields._id)); if (!emojiToUpdate) { throw new Meteor.Error('Emoji not found.'); } diff --git a/app/api/server/v1/groups.js b/app/api/server/v1/groups.js index 4cf5d029ad6b..141bb94d49d6 100644 --- a/app/api/server/v1/groups.js +++ b/app/api/server/v1/groups.js @@ -3,7 +3,8 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; -import { Subscriptions, Rooms, Messages, Uploads, Integrations, Users } from '../../../models/server'; +import { Subscriptions, Rooms, Messages, Users } from '../../../models/server'; +import { Integrations, Uploads } from '../../../models/server/raw'; import { hasPermission, hasAtLeastOnePermission, canAccessRoom, hasAllPermission } from '../../../authorization/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { API } from '../api'; @@ -272,18 +273,18 @@ API.v1.addRoute('groups.files', { authRequired: true }, { const ourQuery = Object.assign({}, query, { rid: findResult.rid }); - const files = Uploads.find(ourQuery, { + const files = Promise.await(Uploads.find(ourQuery, { sort: sort || { name: 1 }, skip: offset, limit: count, fields, - }).fetch(); + }).toArray()); return API.v1.success({ files: files.map(addUserObjectToEveryObject), count: files.length, offset, - total: Uploads.find(ourQuery).count(), + total: Promise.await(Uploads.find(ourQuery).count()), }); }, }); @@ -312,21 +313,24 @@ API.v1.addRoute('groups.getIntegrations', { authRequired: true }, { } const { offset, count } = this.getPaginationItems(); - const { sort, fields, query } = this.parseJsonQuery(); + const { sort, fields: projection, query } = this.parseJsonQuery(); const ourQuery = Object.assign(mountIntegrationQueryBasedOnPermissions(this.userId), query, { channel: { $in: channelsToSearch } }); - const integrations = Integrations.find(ourQuery, { + const cursor = Integrations.find(ourQuery, { sort: sort || { _createdAt: 1 }, skip: offset, limit: count, - fields, - }).fetch(); + projection, + }); + + const integrations = Promise.await(cursor.toArray()); + const total = Promise.await(cursor.count()); return API.v1.success({ integrations, count: integrations.length, offset, - total: Integrations.find(ourQuery).count(), + total, }); }, }); diff --git a/app/api/server/v1/im.js b/app/api/server/v1/im.js index a0325298f3f2..41d3d5dfb273 100644 --- a/app/api/server/v1/im.js +++ b/app/api/server/v1/im.js @@ -1,7 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { Subscriptions, Uploads, Users, Messages, Rooms } from '../../../models/server'; +import { Subscriptions, Users, Messages, Rooms } from '../../../models/server'; +import { Uploads } from '../../../models/server/raw'; import { canAccessRoom, hasPermission } from '../../../authorization/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { settings } from '../../../settings/server'; @@ -148,18 +149,18 @@ API.v1.addRoute(['dm.files', 'im.files'], { authRequired: true }, { const ourQuery = Object.assign({}, query, { rid: findResult.room._id }); - const files = Uploads.find(ourQuery, { + const files = Promise.await(Uploads.find(ourQuery, { sort: sort || { name: 1 }, skip: offset, limit: count, fields, - }).fetch(); + }).toArray()); return API.v1.success({ files: files.map(addUserObjectToEveryObject), count: files.length, offset, - total: Uploads.find(ourQuery).count(), + total: Promise.await(Uploads.find(ourQuery).count()), }); }, }); diff --git a/app/api/server/v1/instances.ts b/app/api/server/v1/instances.ts index e6586a7c12a7..54bd2a563d14 100644 --- a/app/api/server/v1/instances.ts +++ b/app/api/server/v1/instances.ts @@ -1,16 +1,16 @@ import { getInstanceConnection } from '../../../../server/stream/streamBroadcast'; import { hasPermission } from '../../../authorization/server'; import { API } from '../api'; -import InstanceStatus from '../../../models/server/models/InstanceStatus'; +import { InstanceStatus } from '../../../models/server/raw'; import { IInstanceStatus } from '../../../../definition/IInstanceStatus'; API.v1.addRoute('instances.get', { authRequired: true }, { - get() { + async get() { if (!hasPermission(this.userId, 'view-statistics')) { return API.v1.unauthorized(); } - const instances = InstanceStatus.find().fetch(); + const instances = await InstanceStatus.find().toArray(); return API.v1.success({ instances: instances.map((instance: IInstanceStatus) => { diff --git a/app/api/server/v1/integrations.js b/app/api/server/v1/integrations.js index 480c3e8743eb..c05544eb4b82 100644 --- a/app/api/server/v1/integrations.js +++ b/app/api/server/v1/integrations.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { hasAtLeastOnePermission } from '../../../authorization/server'; -import { IntegrationHistory, Integrations } from '../../../models'; +import { Integrations, IntegrationHistory } from '../../../models/server/raw'; import { API } from '../api'; import { mountIntegrationHistoryQueryBasedOnPermissions, mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; import { findOneIntegration } from '../lib/integrations'; @@ -63,21 +63,24 @@ API.v1.addRoute('integrations.history', { authRequired: true }, { const { id } = this.queryParams; const { offset, count } = this.getPaginationItems(); - const { sort, fields, query } = this.parseJsonQuery(); + const { sort, fields: projection, query } = this.parseJsonQuery(); const ourQuery = Object.assign(mountIntegrationHistoryQueryBasedOnPermissions(this.userId, id), query); - const history = IntegrationHistory.find(ourQuery, { + const cursor = IntegrationHistory.find(ourQuery, { sort: sort || { _updatedAt: -1 }, skip: offset, limit: count, - fields, - }).fetch(); + projection, + }); + + const history = Promise.await(cursor.toArray()); + const total = Promise.await(cursor.count()); return API.v1.success({ history, offset, items: history.length, - total: IntegrationHistory.find(ourQuery).count(), + total, }); }, }); @@ -94,21 +97,25 @@ API.v1.addRoute('integrations.list', { authRequired: true }, { } const { offset, count } = this.getPaginationItems(); - const { sort, fields, query } = this.parseJsonQuery(); + const { sort, fields: projection, query } = this.parseJsonQuery(); const ourQuery = Object.assign(mountIntegrationQueryBasedOnPermissions(this.userId), query); - const integrations = Integrations.find(ourQuery, { + const cursor = Integrations.find(ourQuery, { sort: sort || { ts: -1 }, skip: offset, limit: count, - fields, - }).fetch(); + projection, + }); + + const total = Promise.await(cursor.count()); + + const integrations = Promise.await(cursor.toArray()); return API.v1.success({ integrations, offset, items: integrations.length, - total: Integrations.find(ourQuery).count(), + total, }); }, }); @@ -138,9 +145,9 @@ API.v1.addRoute('integrations.remove', { authRequired: true }, { switch (this.bodyParams.type) { case 'webhook-outgoing': if (this.bodyParams.target_url) { - integration = Integrations.findOne({ urls: this.bodyParams.target_url }); + integration = Promise.await(Integrations.findOne({ urls: this.bodyParams.target_url })); } else if (this.bodyParams.integrationId) { - integration = Integrations.findOne({ _id: this.bodyParams.integrationId }); + integration = Promise.await(Integrations.findOne({ _id: this.bodyParams.integrationId })); } if (!integration) { @@ -155,7 +162,7 @@ API.v1.addRoute('integrations.remove', { authRequired: true }, { integration, }); case 'webhook-incoming': - integration = Integrations.findOne({ _id: this.bodyParams.integrationId }); + integration = Promise.await(Integrations.findOne({ _id: this.bodyParams.integrationId })); if (!integration) { return API.v1.failure('No integration found.'); @@ -217,9 +224,9 @@ API.v1.addRoute('integrations.update', { authRequired: true }, { switch (this.bodyParams.type) { case 'webhook-outgoing': if (this.bodyParams.target_url) { - integration = Integrations.findOne({ urls: this.bodyParams.target_url }); + integration = Promise.await(Integrations.findOne({ urls: this.bodyParams.target_url })); } else if (this.bodyParams.integrationId) { - integration = Integrations.findOne({ _id: this.bodyParams.integrationId }); + integration = Promise.await(Integrations.findOne({ _id: this.bodyParams.integrationId })); } if (!integration) { @@ -229,10 +236,10 @@ API.v1.addRoute('integrations.update', { authRequired: true }, { Meteor.call('updateOutgoingIntegration', integration._id, this.bodyParams); return API.v1.success({ - integration: Integrations.findOne({ _id: integration._id }), + integration: Promise.await(Integrations.findOne({ _id: integration._id })), }); case 'webhook-incoming': - integration = Integrations.findOne({ _id: this.bodyParams.integrationId }); + integration = Promise.await(Integrations.findOne({ _id: this.bodyParams.integrationId })); if (!integration) { return API.v1.failure('No integration found.'); @@ -241,7 +248,7 @@ API.v1.addRoute('integrations.update', { authRequired: true }, { Meteor.call('updateIncomingIntegration', integration._id, this.bodyParams); return API.v1.success({ - integration: Integrations.findOne({ _id: integration._id }), + integration: Promise.await(Integrations.findOne({ _id: integration._id })), }); default: return API.v1.failure('Invalid integration type.'); diff --git a/app/api/server/v1/invites.js b/app/api/server/v1/invites.js index fd17ec366190..f901247547db 100644 --- a/app/api/server/v1/invites.js +++ b/app/api/server/v1/invites.js @@ -7,7 +7,7 @@ import { validateInviteToken } from '../../../invites/server/functions/validateI API.v1.addRoute('listInvites', { authRequired: true }, { get() { - const result = listInvites(this.userId); + const result = Promise.await(listInvites(this.userId)); return API.v1.success(result); }, }); @@ -15,7 +15,7 @@ API.v1.addRoute('listInvites', { authRequired: true }, { API.v1.addRoute('findOrCreateInvite', { authRequired: true }, { post() { const { rid, days, maxUses } = this.bodyParams; - const result = findOrCreateInvite(this.userId, { rid, days, maxUses }); + const result = Promise.await(findOrCreateInvite(this.userId, { rid, days, maxUses })); return API.v1.success(result); }, @@ -24,7 +24,7 @@ API.v1.addRoute('findOrCreateInvite', { authRequired: true }, { API.v1.addRoute('removeInvite/:_id', { authRequired: true }, { delete() { const { _id } = this.urlParams; - const result = removeInvite(this.userId, { _id }); + const result = Promise.await(removeInvite(this.userId, { _id })); return API.v1.success(result); }, @@ -34,7 +34,7 @@ API.v1.addRoute('useInviteToken', { authRequired: true }, { post() { const { token } = this.bodyParams; // eslint-disable-next-line react-hooks/rules-of-hooks - const result = useInviteToken(this.userId, token); + const result = Promise.await(useInviteToken(this.userId, token)); return API.v1.success(result); }, @@ -46,7 +46,7 @@ API.v1.addRoute('validateInviteToken', { authRequired: false }, { let valid = true; try { - validateInviteToken(token); + Promise.await(validateInviteToken(token)); } catch (e) { valid = false; } diff --git a/app/api/server/v1/ldap.ts b/app/api/server/v1/ldap.ts index ee98484d1791..c424342d9712 100644 --- a/app/api/server/v1/ldap.ts +++ b/app/api/server/v1/ldap.ts @@ -7,7 +7,7 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { LDAP } from '../../../../server/sdk'; API.v1.addRoute('ldap.testConnection', { authRequired: true }, { - post() { + async post() { if (!this.userId) { throw new Error('error-invalid-user'); } @@ -21,20 +21,20 @@ API.v1.addRoute('ldap.testConnection', { authRequired: true }, { } try { - Promise.await(LDAP.testConnection()); + await LDAP.testConnection(); } catch (error) { SystemLogger.error(error); throw new Error('Connection_failed'); } return API.v1.success({ - message: 'Connection_success', + message: 'Connection_success' as const, }); }, }); API.v1.addRoute('ldap.testSearch', { authRequired: true }, { - post() { + async post() { check(this.bodyParams, Match.ObjectIncluding({ username: String, })); @@ -51,10 +51,10 @@ API.v1.addRoute('ldap.testSearch', { authRequired: true }, { throw new Error('LDAP_disabled'); } - Promise.await(LDAP.testSearch(this.bodyParams.username)); + await LDAP.testSearch(this.bodyParams.username); return API.v1.success({ - message: 'LDAP_User_Found', + message: 'LDAP_User_Found' as const, }); }, }); diff --git a/app/api/server/v1/permissions.js b/app/api/server/v1/permissions.js deleted file mode 100644 index 4ac1661f0786..000000000000 --- a/app/api/server/v1/permissions.js +++ /dev/null @@ -1,86 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; - -import { hasPermission } from '../../../authorization'; -import { Permissions, Roles } from '../../../models/server'; -import { API } from '../api'; - -API.v1.addRoute('permissions.listAll', { 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 "updatedSince" query parameter must be a valid date.'); - } else { - updatedSinceDate = new Date(updatedSince); - } - } - - let result; - Meteor.runAsUser(this.userId, () => { result = Meteor.call('permissions/get', updatedSinceDate); }); - - if (Array.isArray(result)) { - result = { - update: result, - remove: [], - }; - } - - return API.v1.success(result); - }, -}); - -API.v1.addRoute('permissions.update', { authRequired: true }, { - post() { - if (!hasPermission(this.userId, 'access-permissions')) { - return API.v1.failure('Editing permissions is not allowed', 'error-edit-permissions-not-allowed'); - } - - check(this.bodyParams, { - permissions: [ - Match.ObjectIncluding({ - _id: String, - roles: [String], - }), - ], - }); - - let permissionNotFound = false; - let roleNotFound = false; - Object.keys(this.bodyParams.permissions).forEach((key) => { - const element = this.bodyParams.permissions[key]; - - if (!Permissions.findOneById(element._id)) { - permissionNotFound = true; - } - - Object.keys(element.roles).forEach((key) => { - const subelement = element.roles[key]; - - if (!Roles.findOneById(subelement)) { - roleNotFound = true; - } - }); - }); - - if (permissionNotFound) { - return API.v1.failure('Invalid permission', 'error-invalid-permission'); - } if (roleNotFound) { - return API.v1.failure('Invalid role', 'error-invalid-role'); - } - - Object.keys(this.bodyParams.permissions).forEach((key) => { - const element = this.bodyParams.permissions[key]; - - Permissions.createOrUpdate(element._id, element.roles); - }); - - const result = Meteor.runAsUser(this.userId, () => Meteor.call('permissions/get')); - - return API.v1.success({ - permissions: result, - }); - }, -}); diff --git a/app/api/server/v1/permissions.ts b/app/api/server/v1/permissions.ts new file mode 100644 index 000000000000..988f4907e351 --- /dev/null +++ b/app/api/server/v1/permissions.ts @@ -0,0 +1,74 @@ +import { Meteor } from 'meteor/meteor'; + +import { hasPermission } from '../../../authorization/server'; +import { API } from '../api'; +import { Permissions, Roles } from '../../../models/server/raw'; +import { IPermission } from '../../../../definition/IPermission'; +import { isBodyParamsValidPermissionUpdate } from '../../../../definition/rest/v1/permissions'; + +API.v1.addRoute('permissions.listAll', { authRequired: true }, { + async get() { + const { updatedSince } = this.queryParams; + + let updatedSinceDate: Date | undefined; + if (updatedSince) { + if (isNaN(Date.parse(updatedSince))) { + throw new Meteor.Error('error-roomId-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); + } + updatedSinceDate = new Date(updatedSince); + } + + const result = await Meteor.call('permissions/get', updatedSinceDate) as { + update: IPermission[]; + remove: IPermission[]; + }; + + if (Array.isArray(result)) { + return API.v1.success({ + update: result, + remove: [], + }); + } + + return API.v1.success(result); + }, +}); + +API.v1.addRoute('permissions.update', { authRequired: true }, { + async post() { + if (!hasPermission(this.userId, 'access-permissions')) { + return API.v1.failure('Editing permissions is not allowed', 'error-edit-permissions-not-allowed'); + } + + const { bodyParams } = this; + + if (!isBodyParamsValidPermissionUpdate(bodyParams)) { + return API.v1.failure('Invalid body params', 'error-invalid-body-params'); + } + + const permissionKeys = bodyParams.permissions.map(({ _id }) => _id); + const permissions = await Permissions.find({ _id: { $in: permissionKeys } }).toArray(); + + if (permissions.length !== bodyParams.permissions.length) { + return API.v1.failure('Invalid permission', 'error-invalid-permission'); + } + + const roleKeys = [...new Set(bodyParams.permissions.flatMap((p) => p.roles))]; + + const roles = await Roles.find({ _id: { $in: roleKeys } }).toArray(); + + if (roles.length !== roleKeys.length) { + return API.v1.failure('Invalid role', 'error-invalid-role'); + } + + for await (const permission of bodyParams.permissions) { + await Permissions.setRoles(permission._id, permission.roles); + } + + const result = await Meteor.call('permissions/get') as IPermission[]; + + return API.v1.success({ + permissions: result, + }); + }, +}); diff --git a/app/api/server/v1/roles.js b/app/api/server/v1/roles.js deleted file mode 100644 index 39d89164e4ab..000000000000 --- a/app/api/server/v1/roles.js +++ /dev/null @@ -1,281 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; - -import { Roles, Users } from '../../../models'; -import { API } from '../api'; -import { getUsersInRole, hasPermission, hasRole } from '../../../authorization/server'; -import { settings } from '../../../settings/server/index'; -import { api } from '../../../../server/sdk/api'; - -API.v1.addRoute('roles.list', { authRequired: true }, { - get() { - const roles = Roles.find({}, { fields: { _updatedAt: 0 } }).fetch(); - - return API.v1.success({ roles }); - }, -}); - -API.v1.addRoute('roles.sync', { authRequired: true }, { - get() { - const { updatedSince } = this.queryParams; - - if (isNaN(Date.parse(updatedSince))) { - throw new Meteor.Error('error-updatedSince-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); - } - - return API.v1.success({ - roles: { - update: Roles.findByUpdatedDate(new Date(updatedSince), { fields: API.v1.defaultFieldsToExclude }).fetch(), - remove: Roles.trashFindDeletedAfter(new Date(updatedSince)).fetch(), - }, - }); - }, -}); - -API.v1.addRoute('roles.create', { authRequired: true }, { - post() { - check(this.bodyParams, { - name: String, - scope: Match.Maybe(String), - description: Match.Maybe(String), - mandatory2fa: Match.Maybe(Boolean), - }); - - const roleData = { - name: this.bodyParams.name, - scope: this.bodyParams.scope, - description: this.bodyParams.description, - mandatory2fa: this.bodyParams.mandatory2fa, - }; - - if (!hasPermission(Meteor.userId(), 'access-permissions')) { - throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed'); - } - - if (Roles.findOneByIdOrName(roleData.name)) { - throw new Meteor.Error('error-duplicate-role-names-not-allowed', 'Role name already exists'); - } - - if (['Users', 'Subscriptions'].includes(roleData.scope) === false) { - roleData.scope = 'Users'; - } - - const roleId = Roles.createWithRandomId(roleData.name, roleData.scope, roleData.description, false, roleData.mandatory2fa); - - if (settings.get('UI_DisplayRoles')) { - api.broadcast('user.roleUpdate', { - type: 'changed', - _id: roleId, - }); - } - - return API.v1.success({ - role: Roles.findOneByIdOrName(roleId, { fields: API.v1.defaultFieldsToExclude }), - }); - }, -}); - -API.v1.addRoute('roles.addUserToRole', { authRequired: true }, { - post() { - check(this.bodyParams, { - roleName: String, - username: String, - roomId: Match.Maybe(String), - }); - - const user = this.getUserFromParams(); - const { roleName, roomId } = this.bodyParams; - - if (hasRole(user._id, roleName, roomId)) { - throw new Meteor.Error('error-user-already-in-role', 'User already in role'); - } - - Meteor.runAsUser(this.userId, () => { - Meteor.call('authorization:addUserToRole', roleName, user.username, roomId); - }); - - return API.v1.success({ - role: Roles.findOneByIdOrName(this.bodyParams.roleName, { fields: API.v1.defaultFieldsToExclude }), - }); - }, -}); - -API.v1.addRoute('roles.getUsersInRole', { authRequired: true }, { - get() { - const { roomId, role } = this.queryParams; - const { offset, count = 50 } = this.getPaginationItems(); - - const fields = { - name: 1, - username: 1, - emails: 1, - avatarETag: 1, - }; - - if (!role) { - throw new Meteor.Error('error-param-not-provided', 'Query param "role" is required'); - } - if (!hasPermission(this.userId, 'access-permissions')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } - if (roomId && !hasPermission(this.userId, 'view-other-user-channels')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } - const users = getUsersInRole(role, roomId, { - limit: count, - sort: { username: 1 }, - skip: offset, - fields, - }); - return API.v1.success({ users: users.fetch(), total: users.count() }); - }, -}); - -API.v1.addRoute('roles.update', { authRequired: true }, { - post() { - check(this.bodyParams, { - roleId: String, - name: Match.Maybe(String), - scope: Match.Maybe(String), - description: Match.Maybe(String), - mandatory2fa: Match.Maybe(Boolean), - }); - - const roleData = { - roleId: this.bodyParams.roleId, - name: this.bodyParams.name, - scope: this.bodyParams.scope, - description: this.bodyParams.description, - mandatory2fa: this.bodyParams.mandatory2fa, - }; - - const role = Roles.findOneByIdOrName(roleData.roleId); - - if (!role) { - throw new Meteor.Error('error-invalid-roleId', 'This role does not exist'); - } - - if (role.protected && ((roleData.name && roleData.name !== role.name) || (roleData.scope && roleData.scope !== role.scope))) { - throw new Meteor.Error('error-role-protected', 'Role is protected'); - } - - if (roleData.name) { - const otherRole = Roles.findOneByIdOrName(roleData.name); - if (otherRole && otherRole._id !== role._id) { - throw new Meteor.Error('error-duplicate-role-names-not-allowed', 'Role name already exists'); - } - } - - if (roleData.scope) { - if (['Users', 'Subscriptions'].includes(roleData.scope) === false) { - roleData.scope = 'Users'; - } - } - - Roles.updateById(roleData.roleId, roleData.name, roleData.scope, roleData.description, roleData.mandatory2fa); - - if (settings.get('UI_DisplayRoles')) { - api.broadcast('user.roleUpdate', { - type: 'changed', - _id: roleData.roleId, - }); - } - - return API.v1.success({ - role: Roles.findOneByIdOrName(roleData.roleId, { fields: API.v1.defaultFieldsToExclude }), - }); - }, -}); - -API.v1.addRoute('roles.delete', { authRequired: true }, { - post() { - check(this.bodyParams, { - roleId: String, - }); - - if (!hasPermission(this.userId, 'access-permissions')) { - throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed'); - } - - const role = Roles.findOneByIdOrName(this.bodyParams.roleId); - - if (!role) { - throw new Meteor.Error('error-invalid-roleId', 'This role does not exist'); - } - - if (role.protected) { - throw new Meteor.Error('error-role-protected', 'Cannot delete a protected role'); - } - - const existingUsers = Roles.findUsersInRole(role.name, role.scope); - - if (existingUsers && existingUsers.count() > 0) { - throw new Meteor.Error('error-role-in-use', 'Cannot delete role because it\'s in use'); - } - - Roles.remove(role._id); - - return API.v1.success(); - }, -}); - -API.v1.addRoute('roles.removeUserFromRole', { authRequired: true }, { - post() { - check(this.bodyParams, { - roleName: String, - username: String, - scope: Match.Maybe(String), - }); - - const data = { - roleName: this.bodyParams.roleName, - username: this.bodyParams.username, - scope: this.bodyParams.scope, - }; - - if (!hasPermission(this.userId, 'access-permissions')) { - throw new Meteor.Error('error-not-allowed', 'Accessing permissions is not allowed'); - } - - const user = Users.findOneByUsername(data.username); - - if (!user) { - throw new Meteor.Error('error-invalid-user', 'There is no user with this username'); - } - - const role = Roles.findOneByIdOrName(data.roleName); - - if (!role) { - throw new Meteor.Error('error-invalid-roleId', 'This role does not exist'); - } - - if (!hasRole(user._id, role.name, data.scope)) { - throw new Meteor.Error('error-user-not-in-role', 'User is not in this role'); - } - - if (role._id === 'admin') { - const adminCount = Roles.findUsersInRole('admin').count(); - if (adminCount === 1) { - throw new Meteor.Error('error-admin-required', 'You need to have at least one admin'); - } - } - - Roles.removeUserRoles(user._id, role.name, data.scope); - - if (settings.get('UI_DisplayRoles')) { - api.broadcast('user.roleUpdate', { - type: 'removed', - _id: role._id, - u: { - _id: user._id, - username: user.username, - }, - scope: data.scope, - }); - } - - return API.v1.success({ - role, - }); - }, -}); diff --git a/app/api/server/v1/roles.ts b/app/api/server/v1/roles.ts new file mode 100644 index 000000000000..ef92c3547c2d --- /dev/null +++ b/app/api/server/v1/roles.ts @@ -0,0 +1,285 @@ +import { Meteor } from 'meteor/meteor'; +import { check, Match } from 'meteor/check'; + +import { Users } from '../../../models/server'; +import { API } from '../api'; +import { getUsersInRole, hasRole } from '../../../authorization/server'; +import { settings } from '../../../settings/server/index'; +import { api } from '../../../../server/sdk/api'; +import { Roles } from '../../../models/server/raw'; +import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; +import { isRoleAddUserToRoleProps, isRoleCreateProps, isRoleDeleteProps, isRoleRemoveUserFromRoleProps, isRoleUpdateProps } from '../../../../definition/rest/v1/roles'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; + +API.v1.addRoute('roles.list', { authRequired: true }, { + async get() { + const roles = await Roles.find({}, { projection: { _updatedAt: 0 } }).toArray(); + + return API.v1.success({ roles }); + }, +}); + +API.v1.addRoute('roles.sync', { authRequired: true }, { + async get() { + check(this.queryParams, Match.ObjectIncluding({ + updatedSince: Match.Where((value: unknown): value is string => typeof value === 'string' && !Number.isNaN(Date.parse(value))), + })); + + const { updatedSince } = this.queryParams; + + return API.v1.success({ + roles: { + update: await Roles.findByUpdatedDate(new Date(updatedSince)).toArray(), + remove: await Roles.trashFindDeletedAfter(new Date(updatedSince)).toArray(), + }, + }); + }, +}); + +API.v1.addRoute('roles.create', { authRequired: true }, { + async post() { + if (!isRoleCreateProps(this.bodyParams)) { + throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.'); + } + + const { name, scope, description, mandatory2fa } = this.bodyParams; + + if (!await hasPermissionAsync(Meteor.userId(), 'access-permissions')) { + throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed'); + } + + if (await Roles.findOneByIdOrName(name)) { + throw new Meteor.Error('error-duplicate-role-names-not-allowed', 'Role name already exists'); + } + + const roleId = (await Roles.createWithRandomId( + name, + scope && ['Users', 'Subscriptions'].includes(scope) ? scope : 'Users', + description, + false, + mandatory2fa, + )).insertedId; + + if (settings.get('UI_DisplayRoles')) { + api.broadcast('user.roleUpdate', { + type: 'changed', + _id: roleId, + }); + } + + const role = await Roles.findOneByIdOrName(roleId); + + if (!role) { + return API.v1.failure('error-role-not-found', 'Role not found'); + } + + return API.v1.success({ + role, + }); + }, +}); + +API.v1.addRoute('roles.addUserToRole', { authRequired: true }, { + async post() { + if (!isRoleAddUserToRoleProps(this.bodyParams)) { + throw new Meteor.Error('error-invalid-role-properties', isRoleAddUserToRoleProps.errors?.map((error) => error.message).join('\n')); + } + + const user = this.getUserFromParams(); + const { roleName, roomId } = this.bodyParams; + + if (hasRole(user._id, roleName, roomId)) { + throw new Meteor.Error('error-user-already-in-role', 'User already in role'); + } + + await Meteor.call('authorization:addUserToRole', roleName, user.username, roomId); + + const role = await Roles.findOneByIdOrName(roleName); + + if (!role) { + return API.v1.failure('error-role-not-found', 'Role not found'); + } + + return API.v1.success({ + role, + }); + }, +}); + +API.v1.addRoute('roles.getUsersInRole', { authRequired: true }, { + async get() { + const { roomId, role } = this.queryParams; + const { offset, count = 50 } = this.getPaginationItems(); + + const projection = { + name: 1, + username: 1, + emails: 1, + avatarETag: 1, + }; + + if (!role) { + throw new Meteor.Error('error-param-not-provided', 'Query param "role" is required'); + } + if (!await hasPermissionAsync(this.userId, 'access-permissions')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } + if (roomId && !await hasPermissionAsync(this.userId, 'view-other-user-channels')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } + const users = await getUsersInRole(role, 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() }); + }, +}); + +API.v1.addRoute('roles.update', { authRequired: true }, { + async post() { + const { bodyParams } = this; + if (!isRoleUpdateProps(bodyParams)) { + throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.'); + } + + const roleData = { + roleId: bodyParams.roleId, + name: bodyParams.name, + scope: bodyParams.scope || 'Users', + description: bodyParams.description, + mandatory2fa: bodyParams.mandatory2fa, + }; + + const role = await Roles.findOneByIdOrName(roleData.roleId); + + if (!role) { + throw new Meteor.Error('error-invalid-roleId', 'This role does not exist'); + } + + if (role.protected && ((roleData.name && roleData.name !== role.name) || (roleData.scope && roleData.scope !== role.scope))) { + throw new Meteor.Error('error-role-protected', 'Role is protected'); + } + + if (roleData.name) { + const otherRole = await Roles.findOneByIdOrName(roleData.name); + if (otherRole && otherRole._id !== role._id) { + throw new Meteor.Error('error-duplicate-role-names-not-allowed', 'Role name already exists'); + } + } + + if (['Users', 'Subscriptions'].includes(roleData.scope) === false) { + throw new Meteor.Error('error-invalid-scope', 'Invalid scope'); + } + + await Roles.updateById(roleData.roleId, roleData.name, roleData.scope, roleData.description, roleData.mandatory2fa); + + if (settings.get('UI_DisplayRoles')) { + api.broadcast('user.roleUpdate', { + type: 'changed', + _id: roleData.roleId, + }); + } + + const updatedRole = await Roles.findOneByIdOrName(roleData.roleId); + + if (!updatedRole) { + return API.v1.failure(); + } + + return API.v1.success({ + role: updatedRole, + }); + }, +}); + +API.v1.addRoute('roles.delete', { authRequired: true }, { + async post() { + const { bodyParams } = this; + if (!isRoleDeleteProps(bodyParams)) { + throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.'); + } + + if (!await hasPermissionAsync(this.userId, 'access-permissions')) { + throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed'); + } + + const role = await Roles.findOneByIdOrName(bodyParams.roleId); + + if (!role) { + throw new Meteor.Error('error-invalid-roleId', 'This role does not exist'); + } + + if (role.protected) { + throw new Meteor.Error('error-role-protected', 'Cannot delete a protected role'); + } + + const existingUsers = await Roles.findUsersInRole(role.name, role.scope); + + if (existingUsers && await existingUsers.count() > 0) { + throw new Meteor.Error('error-role-in-use', 'Cannot delete role because it\'s in use'); + } + + await Roles.removeById(role._id); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('roles.removeUserFromRole', { authRequired: true }, { + async post() { + const { bodyParams } = this; + if (!isRoleRemoveUserFromRoleProps(bodyParams)) { + throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.'); + } + + const { roleName, username, scope } = bodyParams; + + if (!await hasPermissionAsync(this.userId, 'access-permissions')) { + throw new Meteor.Error('error-not-allowed', 'Accessing permissions is not allowed'); + } + + const user = Users.findOneByUsername(username); + + if (!user) { + throw new Meteor.Error('error-invalid-user', 'There is no user with this username'); + } + + const role = await Roles.findOneByIdOrName(roleName); + + if (!role) { + throw new Meteor.Error('error-invalid-roleId', 'This role does not exist'); + } + + if (!await hasRoleAsync(user._id, role.name, scope)) { + throw new Meteor.Error('error-user-not-in-role', 'User is not in this role'); + } + + if (role._id === 'admin') { + const adminCount = await (await Roles.findUsersInRole('admin')).count(); + if (adminCount === 1) { + throw new Meteor.Error('error-admin-required', 'You need to have at least one admin'); + } + } + + await Roles.removeUserRoles(user._id, [role.name], scope); + + if (settings.get('UI_DisplayRoles')) { + api.broadcast('user.roleUpdate', { + type: 'removed', + _id: role._id, + u: { + _id: user._id, + username: user.username, + }, + scope, + }); + } + + return API.v1.success({ + role, + }); + }, +}); diff --git a/app/api/server/v1/rooms.js b/app/api/server/v1/rooms.js index 9310ac8c7b22..df793f68226b 100644 --- a/app/api/server/v1/rooms.js +++ b/app/api/server/v1/rooms.js @@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { FileUpload } from '../../../file-upload'; import { Rooms, Messages } from '../../../models'; import { API } from '../api'; -import { findAdminRooms, findChannelAndPrivateAutocomplete, findAdminRoom, findRoomsAvailableForTeams, findChannelAndPrivateAutocompleteWithPagination } from '../lib/rooms'; +import { findAdminRooms, findChannelAndPrivateAutocomplete, findAdminRoom, findAdminRoomsAutocomplete, findRoomsAvailableForTeams, findChannelAndPrivateAutocompleteWithPagination } from '../lib/rooms'; import { sendFile, sendViaEmail } from '../../../../server/lib/channelExport'; import { canAccessRoom, hasPermission } from '../../../authorization/server'; import { Media } from '../../../../server/sdk'; @@ -286,6 +286,20 @@ API.v1.addRoute('rooms.adminRooms', { authRequired: true }, { }, }); +API.v1.addRoute('rooms.autocomplete.adminRooms', { authRequired: true }, { + get() { + const { selector } = this.queryParams; + if (!selector) { + return API.v1.failure('The \'selector\' param is required'); + } + + return API.v1.success(Promise.await(findAdminRoomsAutocomplete({ + uid: this.userId, + selector: JSON.parse(selector), + }))); + }, +}); + API.v1.addRoute('rooms.adminRooms.getRoom', { authRequired: true }, { get() { const { rid } = this.requestParams(); diff --git a/app/api/server/v1/settings.js b/app/api/server/v1/settings.js deleted file mode 100644 index b9c720f3522a..000000000000 --- a/app/api/server/v1/settings.js +++ /dev/null @@ -1,166 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; -import { ServiceConfiguration } from 'meteor/service-configuration'; -import _ from 'underscore'; - -import { Settings } from '../../../models/server'; -import { hasPermission } from '../../../authorization'; -import { API } from '../api'; -import { SettingsEvents, settings } from '../../../settings/server'; -import { setValue } from '../../../settings/server/raw'; - -const fetchSettings = (query, sort, offset, count, fields) => { - const settings = Settings.find(query, { - sort: sort || { _id: 1 }, - skip: offset, - limit: count, - fields: Object.assign({ _id: 1, value: 1, enterprise: 1, invalidValue: 1, modules: 1 }, fields), - }).fetch(); - - SettingsEvents.emit('fetch-settings', settings); - return settings; -}; - -// settings endpoints -API.v1.addRoute('settings.public', { authRequired: false }, { - get() { - const { offset, count } = this.getPaginationItems(); - const { sort, fields, query } = this.parseJsonQuery(); - - let ourQuery = { - hidden: { $ne: true }, - public: true, - }; - - ourQuery = Object.assign({}, query, ourQuery); - - const settings = fetchSettings(ourQuery, sort, offset, count, fields); - - return API.v1.success({ - settings, - count: settings.length, - offset, - total: Settings.find(ourQuery).count(), - }); - }, -}); - -API.v1.addRoute('settings.oauth', { authRequired: false }, { - get() { - const mountOAuthServices = () => { - const oAuthServicesEnabled = ServiceConfiguration.configurations.find({}, { fields: { secret: 0 } }).fetch(); - - return oAuthServicesEnabled.map((service) => { - if (service.custom || ['saml', 'cas', 'wordpress'].includes(service.service)) { - return { ...service }; - } - - return { - _id: service._id, - name: service.service, - clientId: service.appId || service.clientId || service.consumerKey, - buttonLabelText: service.buttonLabelText || '', - buttonColor: service.buttonColor || '', - buttonLabelColor: service.buttonLabelColor || '', - custom: false, - }; - }); - }; - - return API.v1.success({ - services: mountOAuthServices(), - }); - }, -}); - -API.v1.addRoute('settings.addCustomOAuth', { authRequired: true, twoFactorRequired: true }, { - post() { - if (!this.requestParams().name || !this.requestParams().name.trim()) { - throw new Meteor.Error('error-name-param-not-provided', 'The parameter "name" is required'); - } - - Meteor.runAsUser(this.userId, () => { - Meteor.call('addOAuthService', this.requestParams().name, this.userId); - }); - - - return API.v1.success(); - }, -}); - -API.v1.addRoute('settings', { authRequired: true }, { - get() { - const { offset, count } = this.getPaginationItems(); - const { sort, fields, query } = this.parseJsonQuery(); - - let ourQuery = { - hidden: { $ne: true }, - }; - - if (!hasPermission(this.userId, 'view-privileged-setting')) { - ourQuery.public = true; - } - - ourQuery = Object.assign({}, query, ourQuery); - - const settings = fetchSettings(ourQuery, sort, offset, count, fields); - - return API.v1.success({ - settings, - count: settings.length, - offset, - total: Settings.find(ourQuery).count(), - }); - }, -}); - -API.v1.addRoute('settings/:_id', { authRequired: true }, { - get() { - if (!hasPermission(this.userId, 'view-privileged-setting')) { - return API.v1.unauthorized(); - } - - return API.v1.success(_.pick(Settings.findOneNotHiddenById(this.urlParams._id), '_id', 'value')); - }, - post: { - twoFactorRequired: true, - action() { - if (!hasPermission(this.userId, 'edit-privileged-setting')) { - return API.v1.unauthorized(); - } - - // allow special handling of particular setting types - const setting = Settings.findOneNotHiddenById(this.urlParams._id); - if (setting.type === 'action' && this.bodyParams && this.bodyParams.execute) { - // execute the configured method - Meteor.call(setting.value); - return API.v1.success(); - } - - if (setting.type === 'color' && this.bodyParams && this.bodyParams.editor && this.bodyParams.value) { - Settings.updateOptionsById(this.urlParams._id, { editor: this.bodyParams.editor }); - Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value); - return API.v1.success(); - } - - check(this.bodyParams, { - value: Match.Any, - }); - if (Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value)) { - settings.set(Settings.findOneNotHiddenById(this.urlParams._id)); - setValue(this.urlParams._id, this.bodyParams.value); - return API.v1.success(); - } - - return API.v1.failure(); - }, - }, -}); - -API.v1.addRoute('service.configurations', { authRequired: false }, { - get() { - return API.v1.success({ - configurations: ServiceConfiguration.configurations.find({}, { fields: { secret: 0 } }).fetch(), - }); - }, -}); diff --git a/app/api/server/v1/settings.ts b/app/api/server/v1/settings.ts new file mode 100644 index 000000000000..ca0a8ac5178d --- /dev/null +++ b/app/api/server/v1/settings.ts @@ -0,0 +1,178 @@ +import { Meteor } from 'meteor/meteor'; +import { ServiceConfiguration } from 'meteor/service-configuration'; +import _ from 'underscore'; + +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'; +import { ISetting, ISettingColor, isSettingAction, isSettingColor } from '../../../../definition/ISetting'; +import { isOauthCustomConfiguration, isSettingsUpdatePropDefault, isSettingsUpdatePropsActions, isSettingsUpdatePropsColor } from '../../../../definition/rest/v1/settings'; + + +const fetchSettings = async (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: sort || { _id: 1 }, + skip: offset, + limit: count, + projection: { _id: 1, value: 1, enterprise: 1, invalidValue: 1, modules: 1, ...fields }, + }).toArray() as unknown as ISetting[]; + + + SettingsEvents.emit('fetch-settings', settings); + return settings; +}; + +// settings endpoints +API.v1.addRoute('settings.public', { authRequired: false }, { + async get() { + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = { + ...query, + hidden: { $ne: true }, + public: true, + }; + + const settings = await fetchSettings(ourQuery, sort, offset, count, fields); + + return API.v1.success({ + settings, + count: settings.length, + offset, + total: await Settings.find(ourQuery).count(), + }); + }, +}); + +API.v1.addRoute('settings.oauth', { authRequired: false }, { + get() { + const oAuthServicesEnabled = ServiceConfiguration.configurations.find({}, { fields: { secret: 0 } }).fetch(); + + return API.v1.success({ + services: oAuthServicesEnabled.map((service) => { + if (!isOauthCustomConfiguration(service)) { + return service; + } + + if (service.custom || (service.service && ['saml', 'cas', 'wordpress'].includes(service.service))) { + return { ...service }; + } + + return { + _id: service._id, + name: service.service, + clientId: service.appId || service.clientId || service.consumerKey, + buttonLabelText: service.buttonLabelText || '', + buttonColor: service.buttonColor || '', + buttonLabelColor: service.buttonLabelColor || '', + custom: false, + }; + }), + }); + }, +}); + +API.v1.addRoute('settings.addCustomOAuth', { authRequired: true, twoFactorRequired: true }, { + async post() { + if (!this.bodyParams.name || !this.bodyParams.name.trim()) { + throw new Meteor.Error('error-name-param-not-provided', 'The parameter "name" is required'); + } + + await Meteor.call('addOAuthService', this.bodyParams.name, this.userId); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('settings', { authRequired: true }, { + async get() { + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + let ourQuery: Parameters[0] = { + hidden: { $ne: true }, + }; + + if (!hasPermission(this.userId, 'view-privileged-setting')) { + ourQuery.public = true; + } + + ourQuery = Object.assign({}, query, ourQuery); + + const settings = await fetchSettings(ourQuery, sort, offset, count, fields); + + return API.v1.success({ + settings, + count: settings.length, + offset, + total: Settings.find(ourQuery).count(), + }); + }, +}); + +API.v1.addRoute('settings/:_id', { authRequired: true }, { + async get() { + if (!hasPermission(this.userId, 'view-privileged-setting')) { + return API.v1.unauthorized(); + } + const setting = await Settings.findOneNotHiddenById(this.urlParams._id); + if (!setting) { + return API.v1.failure(); + } + return API.v1.success(_.pick(setting, '_id', 'value')); + }, + post: { + twoFactorRequired: true, + async action(): Promise> { + if (!hasPermission(this.userId, 'edit-privileged-setting')) { + return API.v1.unauthorized(); + } + + if (typeof this.urlParams._id !== 'string') { + throw new Meteor.Error('error-id-param-not-provided', 'The parameter "id" is required'); + } + + // allow special handling of particular setting types + const setting = await Settings.findOneNotHiddenById(this.urlParams._id); + + if (!setting) { + return API.v1.failure(); + } + + if (isSettingAction(setting) && isSettingsUpdatePropsActions(this.bodyParams) && this.bodyParams.execute) { + // execute the configured method + Meteor.call(setting.value); + return API.v1.success(); + } + + if (isSettingColor(setting) && isSettingsUpdatePropsColor(this.bodyParams)) { + Settings.updateOptionsById(this.urlParams._id, { editor: this.bodyParams.editor }); + Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value); + return API.v1.success(); + } + + if (isSettingsUpdatePropDefault(this.bodyParams) && await Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value)) { + const s = await Settings.findOneNotHiddenById(this.urlParams._id); + if (!s) { + return API.v1.failure(); + } + settings.set(s); + setValue(this.urlParams._id, this.bodyParams.value); + return API.v1.success(); + } + + return API.v1.failure(); + }, + }, +}); + +API.v1.addRoute('service.configurations', { authRequired: false }, { + get() { + return API.v1.success({ + configurations: ServiceConfiguration.configurations.find({}, { fields: { secret: 0 } }).fetch(), + }); + }, +}); diff --git a/app/api/server/v1/teams.ts b/app/api/server/v1/teams.ts index c3235e703e35..4f3a655aa7d4 100644 --- a/app/api/server/v1/teams.ts +++ b/app/api/server/v1/teams.ts @@ -1,6 +1,5 @@ import { FilterQuery } from 'mongodb'; import { Meteor } from 'meteor/meteor'; -import { Promise } from 'meteor/promise'; import { Match, check } from 'meteor/check'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -10,13 +9,22 @@ import { hasAtLeastOnePermission, hasPermission } from '../../../authorization/s import { Users } from '../../../models/server'; import { removeUserFromRoom } from '../../../lib/server/functions/removeUserFromRoom'; import { IUser } from '../../../../definition/IUser'; +import { isTeamsConvertToChannelProps } from '../../../../definition/rest/v1/teams/TeamsConvertToChannelProps'; +import { isTeamsRemoveRoomProps } from '../../../../definition/rest/v1/teams/TeamsRemoveRoomProps'; +import { isTeamsUpdateMemberProps } from '../../../../definition/rest/v1/teams/TeamsUpdateMemberProps'; +import { isTeamsRemoveMemberProps } from '../../../../definition/rest/v1/teams/TeamsRemoveMemberProps'; +import { isTeamsAddMembersProps } from '../../../../definition/rest/v1/teams/TeamsAddMembersProps'; +import { isTeamsDeleteProps } from '../../../../definition/rest/v1/teams/TeamsDeleteProps'; +import { isTeamsLeaveProps } from '../../../../definition/rest/v1/teams/TeamsLeaveProps'; +import { isTeamsUpdateProps } from '../../../../definition/rest/v1/teams/TeamsUpdateProps'; +import { ITeam, TEAM_TYPE } from '../../../../definition/ITeam'; API.v1.addRoute('teams.list', { authRequired: true }, { - get() { + async get() { const { offset, count } = this.getPaginationItems(); const { sort, query } = this.parseJsonQuery(); - const { records, total } = Promise.await(Team.list(this.userId, { offset, count }, { sort, query })); + const { records, total } = await Team.list(this.userId, { offset, count }, { sort, query }); return API.v1.success({ teams: records, @@ -28,14 +36,14 @@ API.v1.addRoute('teams.list', { authRequired: true }, { }); API.v1.addRoute('teams.listAll', { authRequired: true }, { - get() { + async get() { if (!hasPermission(this.userId, 'view-all-teams')) { return API.v1.unauthorized(); } const { offset, count } = this.getPaginationItems(); - const { records, total } = Promise.await(Team.listAll({ offset, count })); + const { records, total } = await Team.listAll({ offset, count }); return API.v1.success({ teams: records, @@ -47,17 +55,22 @@ API.v1.addRoute('teams.listAll', { authRequired: true }, { }); API.v1.addRoute('teams.create', { authRequired: true }, { - post() { + async post() { if (!hasPermission(this.userId, 'create-team')) { return API.v1.unauthorized(); } - const { name, type, members, room, owner } = this.bodyParams; - if (!name) { - return API.v1.failure('Body param "name" is required'); - } + check(this.bodyParams, Match.ObjectIncluding({ + name: String, + type: Match.OneOf(TEAM_TYPE.PRIVATE, TEAM_TYPE.PUBLIC), + members: Match.Maybe([String]), + room: Match.Maybe(Match.Any), + owner: Match.Maybe(String), + })); - const team = Promise.await(Team.create(this.userId, { + const { name, type, members, room, owner } = this.bodyParams; + + const team = await Team.create(this.userId, { team: { name, type, @@ -65,26 +78,34 @@ API.v1.addRoute('teams.create', { authRequired: true }, { room, members, owner, - })); + }); return API.v1.success({ team }); }, }); -API.v1.addRoute('teams.convertToChannel', { authRequired: true }, { - post() { - check(this.bodyParams, Match.ObjectIncluding({ - teamId: Match.Maybe(String), - teamName: Match.Maybe(String), - roomsToRemove: Match.Maybe([String]), - })); - const { roomsToRemove, teamId, teamName } = this.bodyParams; +const getTeamByIdOrName = async (params: { teamId: string } | { teamName: string }): Promise => { + if ('teamId' in params && params.teamId) { + return Team.getOneById(params.teamId); + } + + if ('teamName' in params && params.teamName) { + return Team.getOneByName(params.teamName); + } + + return null; +}; - if (!teamId && !teamName) { - return API.v1.failure('missing-teamId-or-teamName'); +API.v1.addRoute('teams.convertToChannel', { authRequired: true }, { + async post() { + if (!isTeamsConvertToChannelProps(this.bodyParams)) { + return API.v1.failure('invalid-body-params', isTeamsConvertToChannelProps.errors?.map((e) => e.message).join('\n ')); } - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const { roomsToRemove = [] } = this.bodyParams; + + const team = await getTeamByIdOrName(this.bodyParams); + if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -93,7 +114,7 @@ API.v1.addRoute('teams.convertToChannel', { authRequired: true }, { return API.v1.unauthorized(); } - const rooms: string[] = Promise.await(Team.getMatchingTeamRooms(team._id, roomsToRemove)); + const rooms = await Team.getMatchingTeamRooms(team._id, roomsToRemove); if (rooms.length) { rooms.forEach((room) => { @@ -101,7 +122,7 @@ API.v1.addRoute('teams.convertToChannel', { authRequired: true }, { }); } - Promise.all([ + await Promise.all([ Team.unsetTeamIdOfRooms(team._id), Team.removeAllMembersFromTeam(team._id), Team.deleteById(team._id), @@ -112,14 +133,21 @@ API.v1.addRoute('teams.convertToChannel', { authRequired: true }, { }); API.v1.addRoute('teams.addRooms', { authRequired: true }, { - post() { - const { rooms, teamId, teamName } = this.bodyParams; + async post() { + check(this.bodyParams, Match.OneOf( + Match.ObjectIncluding({ + teamId: String, + }), + Match.ObjectIncluding({ + teamName: String, + }), + )); - if (!teamId && !teamName) { - return API.v1.failure('missing-teamId-or-teamName'); - } + check(this.bodyParams, Match.ObjectIncluding({ + rooms: [String], + })); - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const team = await getTeamByIdOrName(this.bodyParams); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -128,17 +156,21 @@ API.v1.addRoute('teams.addRooms', { authRequired: true }, { return API.v1.unauthorized('error-no-permission-team-channel'); } - const validRooms = Promise.await(Team.addRooms(this.userId, rooms, team._id)); + const { rooms } = this.bodyParams; + + const validRooms = await Team.addRooms(this.userId, rooms, team._id); return API.v1.success({ rooms: validRooms }); }, }); API.v1.addRoute('teams.removeRoom', { authRequired: true }, { - post() { - const { roomId, teamId, teamName } = this.bodyParams; + async post() { + if (!isTeamsRemoveRoomProps(this.bodyParams)) { + return API.v1.failure('body-params-invalid', isTeamsRemoveRoomProps.errors?.map((error) => error.message).join('\n ')); + } - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const team = await getTeamByIdOrName(this.bodyParams); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -149,40 +181,64 @@ API.v1.addRoute('teams.removeRoom', { authRequired: true }, { const canRemoveAny = !!hasPermission(this.userId, 'view-all-team-channels', team.roomId); - const room = Promise.await(Team.removeRoom(this.userId, roomId, team._id, canRemoveAny)); + const { roomId } = this.bodyParams; + + const room = await Team.removeRoom(this.userId, roomId, team._id, canRemoveAny); return API.v1.success({ room }); }, }); API.v1.addRoute('teams.updateRoom', { authRequired: true }, { - post() { + async post() { + check(this.bodyParams, Match.ObjectIncluding({ + roomId: String, + isDefault: Boolean, + })); + const { roomId, isDefault } = this.bodyParams; - const team = Promise.await(Team.getOneByRoomId(roomId)); + const team = await Team.getOneByRoomId(roomId); + if (!team) { + return API.v1.failure('team-does-not-exist'); + } if (!hasPermission(this.userId, 'edit-team-channel', team.roomId)) { return API.v1.unauthorized(); } const canUpdateAny = !!hasPermission(this.userId, 'view-all-team-channels', team.roomId); - const room = Promise.await(Team.updateRoom(this.userId, roomId, isDefault, canUpdateAny)); + const room = await Team.updateRoom(this.userId, roomId, isDefault, canUpdateAny); return API.v1.success({ room }); }, }); API.v1.addRoute('teams.listRooms', { authRequired: true }, { - get() { - const { teamId, teamName, filter, type } = this.queryParams; + async get() { + check(this.queryParams, Match.OneOf( + Match.ObjectIncluding({ + teamId: String, + }), + Match.ObjectIncluding({ + teamName: String, + }), + )); + + check(this.queryParams, Match.ObjectIncluding({ + filter: Match.Maybe(String), + type: Match.Maybe(String), + })); + + const { filter, type } = this.queryParams; const { offset, count } = this.getPaginationItems(); - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const team = await getTeamByIdOrName(this.queryParams); if (!team) { return API.v1.failure('team-does-not-exist'); } - const allowPrivateTeam = hasPermission(this.userId, 'view-all-teams', team.roomId); + const allowPrivateTeam: boolean = hasPermission(this.userId, 'view-all-teams', team.roomId); let getAllRooms = false; if (hasPermission(this.userId, 'view-all-team-channels', team.roomId)) { @@ -190,13 +246,13 @@ API.v1.addRoute('teams.listRooms', { authRequired: true }, { } const listFilter = { - name: filter, + name: filter ?? undefined, isDefault: type === 'autoJoin', getAllRooms, allowPrivateTeam, }; - const { records, total } = Promise.await(Team.listRooms(this.userId, team._id, listFilter, { offset, count })); + const { records, total } = await Team.listRooms(this.userId, team._id, listFilter, { offset, count }); return API.v1.success({ rooms: records, @@ -208,22 +264,37 @@ API.v1.addRoute('teams.listRooms', { authRequired: true }, { }); API.v1.addRoute('teams.listRoomsOfUser', { authRequired: true }, { - get() { + async get() { + check(this.queryParams, Match.OneOf( + Match.ObjectIncluding({ + teamId: String, + }), + Match.ObjectIncluding({ + teamName: String, + }), + )); + + check(this.queryParams, Match.ObjectIncluding({ + userId: String, + canUserDelete: Match.Maybe(Boolean), + })); + const { offset, count } = this.getPaginationItems(); - const { teamId, teamName, userId, canUserDelete = false } = this.queryParams; - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const team = await getTeamByIdOrName(this.queryParams); if (!team) { return API.v1.failure('team-does-not-exist'); } const allowPrivateTeam = hasPermission(this.userId, 'view-all-teams', team.roomId); + const { userId, canUserDelete } = this.queryParams; + if (!(this.userId === userId || hasPermission(this.userId, 'view-all-team-channels', team.roomId))) { return API.v1.unauthorized(); } - const { records, total } = Promise.await(Team.listRoomsOfUser(this.userId, team._id, userId, allowPrivateTeam, canUserDelete, { offset, count })); + const { records, total } = await Team.listRoomsOfUser(this.userId, team._id, userId, allowPrivateTeam, canUserDelete ?? false, { offset, count }); return API.v1.success({ rooms: records, @@ -235,26 +306,31 @@ API.v1.addRoute('teams.listRoomsOfUser', { authRequired: true }, { }); API.v1.addRoute('teams.members', { authRequired: true }, { - get() { + async get() { const { offset, count } = this.getPaginationItems(); + check(this.queryParams, Match.OneOf( + Match.ObjectIncluding({ + teamId: String, + }), + Match.ObjectIncluding({ + teamName: String, + }), + )); + check(this.queryParams, Match.ObjectIncluding({ - teamId: Match.Maybe(String), - teamName: Match.Maybe(String), status: Match.Maybe([String]), username: Match.Maybe(String), name: Match.Maybe(String), })); - const { teamId, teamName, status, username, name } = this.queryParams; - if (!teamId && !teamName) { - return API.v1.failure('missing-teamId-or-teamName'); - } + const { status, username, name } = this.queryParams; - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const team = await getTeamByIdOrName(this.queryParams); if (!team) { return API.v1.failure('team-does-not-exist'); } + const canSeeAllMembers = hasPermission(this.userId, 'view-all-teams', team.roomId); const query = { @@ -263,7 +339,7 @@ API.v1.addRoute('teams.members', { authRequired: true }, { status: status ? { $in: status } : undefined, } as FilterQuery; - const { records, total } = Promise.await(Team.members(this.userId, team._id, canSeeAllMembers, { offset, count }, query)); + const { records, total } = await Team.members(this.userId, team._id, canSeeAllMembers, { offset, count }, query); return API.v1.success({ members: records, @@ -275,10 +351,15 @@ API.v1.addRoute('teams.members', { authRequired: true }, { }); API.v1.addRoute('teams.addMembers', { authRequired: true }, { - post() { - const { teamId, teamName, members } = this.bodyParams; + async post() { + if (!isTeamsAddMembersProps(this.bodyParams)) { + return API.v1.failure('invalid-params'); + } + + const { bodyParams } = this; + const { members } = bodyParams; - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const team = await getTeamByIdOrName(this.bodyParams); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -287,17 +368,22 @@ API.v1.addRoute('teams.addMembers', { authRequired: true }, { return API.v1.unauthorized(); } - Promise.await(Team.addMembers(this.userId, team._id, members)); + await Team.addMembers(this.userId, team._id, members); return API.v1.success(); }, }); API.v1.addRoute('teams.updateMember', { authRequired: true }, { - post() { - const { teamId, teamName, member } = this.bodyParams; + async post() { + if (!isTeamsUpdateMemberProps(this.bodyParams)) { + return API.v1.failure('invalid-params', isTeamsUpdateMemberProps.errors?.map((e) => e.message).join('\n ')); + } + + const { bodyParams } = this; + const { member } = bodyParams; - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const team = await getTeamByIdOrName(this.bodyParams); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -306,17 +392,22 @@ API.v1.addRoute('teams.updateMember', { authRequired: true }, { return API.v1.unauthorized(); } - Promise.await(Team.updateMember(team._id, member)); + await Team.updateMember(team._id, member); return API.v1.success(); }, }); API.v1.addRoute('teams.removeMember', { authRequired: true }, { - post() { - const { teamId, teamName, userId, rooms } = this.bodyParams; + async post() { + if (!isTeamsRemoveMemberProps(this.bodyParams)) { + return API.v1.failure('invalid-params', isTeamsRemoveMemberProps.errors?.map((e) => e.message).join('\n ')); + } + + const { bodyParams } = this; + const { userId, rooms } = bodyParams; - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const team = await getTeamByIdOrName(this.bodyParams); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -330,12 +421,12 @@ API.v1.addRoute('teams.removeMember', { authRequired: true }, { return API.v1.failure('invalid-user'); } - if (!Promise.await(Team.removeMembers(this.userId, team._id, [{ userId }]))) { + if (!await Team.removeMembers(this.userId, team._id, [{ userId }])) { return API.v1.failure(); } if (rooms?.length) { - const roomsFromTeam: string[] = Promise.await(Team.getMatchingTeamRooms(team._id, rooms)); + const roomsFromTeam: string[] = await Team.getMatchingTeamRooms(team._id, rooms); roomsFromTeam.forEach((rid) => { removeUserFromRoom(rid, user, { @@ -348,17 +439,24 @@ API.v1.addRoute('teams.removeMember', { authRequired: true }, { }); API.v1.addRoute('teams.leave', { authRequired: true }, { - post() { - const { teamId, teamName, rooms } = this.bodyParams; + async post() { + if (!isTeamsLeaveProps(this.bodyParams)) { + return API.v1.failure('invalid-params', isTeamsLeaveProps.errors?.map((e) => e.message).join('\n ')); + } - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const { rooms = [] } = this.bodyParams; - Promise.await(Team.removeMembers(this.userId, team._id, [{ + const team = await getTeamByIdOrName(this.bodyParams); + if (!team) { + return API.v1.failure('team-does-not-exist'); + } + + await Team.removeMembers(this.userId, team._id, [{ userId: this.userId, - }])); + }]); - if (rooms?.length) { - const roomsFromTeam: string[] = Promise.await(Team.getMatchingTeamRooms(team._id, rooms)); + if (rooms.length) { + const roomsFromTeam: string[] = await Team.getMatchingTeamRooms(team._id, rooms); roomsFromTeam.forEach((rid) => { removeUserFromRoom(rid, this.user); @@ -370,17 +468,17 @@ API.v1.addRoute('teams.leave', { authRequired: true }, { }); API.v1.addRoute('teams.info', { authRequired: true }, { - get() { - const { teamId, teamName } = this.queryParams; - - if (!teamId && !teamName) { - return API.v1.failure('Provide either the "teamId" or "teamName"'); - } - - const teamInfo = teamId - ? Promise.await(Team.getInfoById(teamId)) - : Promise.await(Team.getInfoByName(teamName)); - + async get() { + check(this.queryParams, Match.OneOf( + Match.ObjectIncluding({ + teamId: String, + }), + Match.ObjectIncluding({ + teamName: String, + }), + )); + + const teamInfo = await getTeamByIdOrName(this.queryParams); if (!teamInfo) { return API.v1.failure('Team not found'); } @@ -390,27 +488,23 @@ API.v1.addRoute('teams.info', { authRequired: true }, { }); API.v1.addRoute('teams.delete', { authRequired: true }, { - post() { - const { teamId, teamName, roomsToRemove } = this.bodyParams; - - if (!teamId && !teamName) { - return API.v1.failure('Provide either the "teamId" or "teamName"'); - } + async post() { + const { roomsToRemove = [] } = this.bodyParams; - if (roomsToRemove && !Array.isArray(roomsToRemove)) { - return API.v1.failure('The list of rooms to remove is invalid.'); + if (!isTeamsDeleteProps(this.bodyParams)) { + return API.v1.failure('invalid-params', isTeamsDeleteProps.errors?.map((e) => e.message).join('\n ')); } - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const team = await getTeamByIdOrName(this.bodyParams); if (!team) { - return API.v1.failure('Team not found.'); + return API.v1.failure('team-does-not-exist'); } if (!hasPermission(this.userId, 'delete-team', team.roomId)) { return API.v1.unauthorized(); } - const rooms: string[] = Promise.await(Team.getMatchingTeamRooms(team._id, roomsToRemove)); + const rooms: string[] = await Team.getMatchingTeamRooms(team._id, roomsToRemove); // Remove the team's main room Meteor.call('eraseRoom', team.roomId); @@ -423,41 +517,41 @@ API.v1.addRoute('teams.delete', { authRequired: true }, { } // Move every other room back to the workspace - Promise.await(Team.unsetTeamIdOfRooms(team._id)); + await Team.unsetTeamIdOfRooms(team._id); // Delete all team memberships - Team.removeAllMembersFromTeam(teamId); + Team.removeAllMembersFromTeam(team._id); // And finally delete the team itself - Promise.await(Team.deleteById(team._id)); + await Team.deleteById(team._id); return API.v1.success(); }, }); API.v1.addRoute('teams.autocomplete', { authRequired: true }, { - get() { + async get() { + check(this.queryParams, Match.ObjectIncluding({ + name: String, + })); + const { name } = this.queryParams; - const teams = Promise.await(Team.autocomplete(this.userId, name)); + const teams = await Team.autocomplete(this.userId, name); return API.v1.success({ teams }); }, }); API.v1.addRoute('teams.update', { authRequired: true }, { - post() { - check(this.bodyParams, { - teamId: String, - data: { - name: Match.Maybe(String), - type: Match.Maybe(Number), - }, - }); + async post() { + if (!isTeamsUpdateProps(this.bodyParams)) { + return API.v1.failure('invalid-params', isTeamsUpdateProps.errors?.map((e) => e.message).join('\n ')); + } - const { teamId, data } = this.bodyParams; + const { data } = this.bodyParams; - const team = teamId && Promise.await(Team.getOneById(teamId)); + const team = await getTeamByIdOrName(this.bodyParams); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -466,7 +560,7 @@ API.v1.addRoute('teams.update', { authRequired: true }, { return API.v1.unauthorized(); } - Promise.await(Team.update(this.userId, teamId, { name: data.name, type: data.type })); + await Team.update(this.userId, team._id, data); return API.v1.success(); }, diff --git a/app/api/server/v1/users.js b/app/api/server/v1/users.js index 1f3f22e38e6c..a6c514a7c8ec 100644 --- a/app/api/server/v1/users.js +++ b/app/api/server/v1/users.js @@ -27,6 +27,7 @@ import { setUserStatus } from '../../../../imports/users-presence/server/activeU import { resetTOTP } from '../../../2fa/server/functions/resetTOTP'; import { Team } from '../../../../server/sdk'; + API.v1.addRoute('users.create', { authRequired: true }, { post() { check(this.bodyParams, { @@ -283,7 +284,11 @@ API.v1.addRoute('users.list', { authRequired: true }, { }, }); -API.v1.addRoute('users.register', { authRequired: false }, { +API.v1.addRoute('users.register', { authRequired: false, + rateLimiterOptions: { + numRequestsAllowed: settings.get('Rate_Limiter_Limit_RegisterUser'), + 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.'); @@ -944,3 +949,9 @@ API.v1.addRoute('users.logout', { authRequired: true }, { }); }, }); + +settings.watch('Rate_Limiter_Limit_RegisterUser', (value) => { + const userRegisterRoute = '/api/v1/users.registerpost'; + + API.v1.updateRateLimiterDictionaryForRoute(userRegisterRoute, value); +}); diff --git a/app/apps/server/bridges/commands.ts b/app/apps/server/bridges/commands.ts index aed3d2cf72b2..fe57d2ff8bad 100644 --- a/app/apps/server/bridges/commands.ts +++ b/app/apps/server/bridges/commands.ts @@ -1,5 +1,4 @@ import { Meteor } from 'meteor/meteor'; -import { Promise as MeteorPromise } from 'meteor/promise'; import { SlashCommandContext, ISlashCommand, ISlashCommandPreviewItem } from '@rocket.chat/apps-engine/definition/slashcommands'; import { CommandBridge } from '@rocket.chat/apps-engine/server/bridges/CommandBridge'; @@ -167,7 +166,7 @@ export class AppCommandsBridge extends CommandBridge { triggerId, ); - MeteorPromise.await(this.orch.getManager()?.getCommandManager().executeCommand(command, context)); + Promise.await(this.orch.getManager()?.getCommandManager().executeCommand(command, context)); } private _appCommandPreviewer(command: string, parameters: any, message: IMessage): any { @@ -182,7 +181,7 @@ export class AppCommandsBridge extends CommandBridge { Object.freeze(params), threadId, ); - return MeteorPromise.await(this.orch.getManager()?.getCommandManager().getPreviews(command, context)); + return Promise.await(this.orch.getManager()?.getCommandManager().getPreviews(command, context)); } private async _appCommandPreviewExecutor(command: string, parameters: any, message: IMessage, preview: ISlashCommandPreviewItem, triggerId: string): Promise { @@ -199,6 +198,6 @@ export class AppCommandsBridge extends CommandBridge { triggerId, ); - MeteorPromise.await(this.orch.getManager()?.getCommandManager().executePreview(command, preview, context)); + Promise.await(this.orch.getManager()?.getCommandManager().executePreview(command, preview, context)); } } diff --git a/app/apps/server/bridges/internal.ts b/app/apps/server/bridges/internal.ts index 0d09646f3893..154adbdeb104 100644 --- a/app/apps/server/bridges/internal.ts +++ b/app/apps/server/bridges/internal.ts @@ -2,8 +2,9 @@ import { InternalBridge } from '@rocket.chat/apps-engine/server/bridges/Internal import { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import { AppServerOrchestrator } from '../orchestrator'; -import { Subscriptions, Settings } from '../../../models/server'; +import { Subscriptions } from '../../../models/server'; import { ISubscription } from '../../../../definition/ISubscription'; +import { Settings } from '../../../models/server/raw'; export class AppInternalBridge extends InternalBridge { // eslint-disable-next-line no-empty-function @@ -30,7 +31,7 @@ export class AppInternalBridge extends InternalBridge { } protected async getWorkspacePublicKey(): Promise { - const publicKeySetting = Settings.findById('Cloud_Workspace_PublicKey').fetch()[0]; + const publicKeySetting = await Settings.findOneById('Cloud_Workspace_PublicKey'); return this.orch.getConverters()?.get('settings').convertToApp(publicKeySetting); } diff --git a/app/apps/server/bridges/scheduler.ts b/app/apps/server/bridges/scheduler.ts index 698931a25be2..721c3a8c8aa0 100644 --- a/app/apps/server/bridges/scheduler.ts +++ b/app/apps/server/bridges/scheduler.ts @@ -198,7 +198,7 @@ export class AppSchedulerBridge extends SchedulerBridge { } } - private async startScheduler(): Promise { + public async startScheduler(): Promise { if (!this.isConnected) { await this.scheduler.start(); this.isConnected = true; diff --git a/app/apps/server/bridges/settings.ts b/app/apps/server/bridges/settings.ts index ad1c234b0af2..ba0626434ff9 100644 --- a/app/apps/server/bridges/settings.ts +++ b/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 '../../../models/server'; +import { Settings } from '../../../models/server/raw'; import { AppServerOrchestrator } from '../orchestrator'; export class AppSettingBridge extends ServerSettingBridge { @@ -13,9 +13,8 @@ export class AppSettingBridge extends ServerSettingBridge { protected async getAll(appId: string): Promise> { this.orch.debugLog(`The App ${ appId } is getting all the settings.`); - return Settings.find({ secret: false }) - .fetch() - .map((s: ISetting) => this.orch.getConverters()?.get('settings').convertToApp(s)); + const settings = await Settings.find({ secret: false }).toArray(); + return settings.map((s) => this.orch.getConverters()?.get('settings').convertToApp(s)); } protected async getOneById(id: string, appId: string): Promise { @@ -46,8 +45,8 @@ export class AppSettingBridge extends ServerSettingBridge { protected async isReadableById(id: string, appId: string): Promise { this.orch.debugLog(`The App ${ appId } is checking if they can read the setting ${ id }.`); - - return !Settings.findOneById(id).secret; + const setting = await Settings.findOneById(id); + return Boolean(setting && !setting.secret); } protected async updateOne(setting: ISetting & { id: string }, appId: string): Promise { diff --git a/app/apps/server/communication/rest.js b/app/apps/server/communication/rest.js index 6d84f3797b95..1386ba39cfa8 100644 --- a/app/apps/server/communication/rest.js +++ b/app/apps/server/communication/rest.js @@ -6,9 +6,10 @@ import { getUploadFormData } from '../../../api/server/lib/getUploadFormData'; import { getWorkspaceAccessToken, getUserCloudAccessToken } from '../../../cloud/server'; import { settings } from '../../../settings/server'; import { Info } from '../../../utils'; -import { Settings, Users } from '../../../models/server'; +import { Users } from '../../../models/server'; import { Apps } from '../orchestrator'; import { formatAppInstanceForRest } from '../../lib/misc/formatAppInstanceForRest'; +import { Settings } from '../../../models/server/raw'; const appsEngineVersionForMarketplace = Info.marketplaceApiVersion.replace(/-.*/g, ''); const getDefaultHeaders = () => ({ @@ -67,7 +68,7 @@ export class AppsRestApi { // Gets the Apps from the marketplace if (this.queryParams.marketplace) { const headers = getDefaultHeaders(); - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); if (token) { headers.Authorization = `Bearer ${ token }`; } @@ -91,7 +92,7 @@ export class AppsRestApi { if (this.queryParams.categories) { const headers = getDefaultHeaders(); - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); if (token) { headers.Authorization = `Bearer ${ token }`; } @@ -187,7 +188,7 @@ export class AppsRestApi { }); const marketplacePromise = new Promise((resolve, reject) => { - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); HTTP.get(`${ baseUrl }/v1/apps/${ this.bodyParams.appId }?appVersion=${ this.bodyParams.version }`, { headers: { @@ -307,7 +308,7 @@ export class AppsRestApi { const baseUrl = orchestrator.getMarketplaceUrl(); const headers = {}; - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); if (token) { headers.Authorization = `Bearer ${ token }`; } @@ -337,7 +338,7 @@ export class AppsRestApi { const baseUrl = orchestrator.getMarketplaceUrl(); const headers = {}; // DO NOT ATTACH THE FRAMEWORK/ENGINE VERSION HERE. - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); if (token) { headers.Authorization = `Bearer ${ token }`; } @@ -363,7 +364,7 @@ export class AppsRestApi { const baseUrl = orchestrator.getMarketplaceUrl(); const headers = getDefaultHeaders(); - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); if (token) { headers.Authorization = `Bearer ${ token }`; } @@ -507,12 +508,12 @@ export class AppsRestApi { const baseUrl = orchestrator.getMarketplaceUrl(); const headers = getDefaultHeaders(); - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); if (token) { headers.Authorization = `Bearer ${ token }`; } - const [workspaceIdSetting] = Settings.findById('Cloud_Workspace_Id').fetch(); + const workspaceIdSetting = Promise.await(Settings.findOneById('Cloud_Workspace_Id')); let result; try { diff --git a/app/apps/server/converters/settings.js b/app/apps/server/converters/settings.js index 82ffcd2b2f0f..bc5949bc7ccd 100644 --- a/app/apps/server/converters/settings.js +++ b/app/apps/server/converters/settings.js @@ -1,14 +1,14 @@ import { SettingType } from '@rocket.chat/apps-engine/definition/settings'; -import { Settings } from '../../../models'; +import { Settings } from '../../../models/server/raw'; export class AppSettingsConverter { constructor(orch) { this.orch = orch; } - convertById(settingId) { - const setting = Settings.findOneNotHiddenById(settingId); + async convertById(settingId) { + const setting = await Settings.findOneNotHiddenById(settingId); return this.convertToApp(setting); } diff --git a/app/apps/server/converters/uploads.js b/app/apps/server/converters/uploads.js index d95f5d10067f..efbda7ae5fd1 100644 --- a/app/apps/server/converters/uploads.js +++ b/app/apps/server/converters/uploads.js @@ -1,5 +1,5 @@ import { transformMappedData } from '../../lib/misc/transformMappedData'; -import Uploads from '../../../models/server/models/Uploads'; +import { Uploads } from '../../../models/server/raw'; export class AppUploadsConverter { constructor(orch) { @@ -7,7 +7,7 @@ export class AppUploadsConverter { } convertById(id) { - const upload = Uploads.findOneById(id); + const upload = Promise.await(Uploads.findOneById(id)); return this.convertToApp(upload); } diff --git a/app/apps/server/cron.js b/app/apps/server/cron.js index 3201612ea6b6..d38eebe060b2 100644 --- a/app/apps/server/cron.js +++ b/app/apps/server/cron.js @@ -6,8 +6,9 @@ import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import { Apps } from './orchestrator'; import { getWorkspaceAccessToken } from '../../cloud/server'; -import { Settings, Users } from '../../models/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) { @@ -27,7 +28,7 @@ const notifyAdminsAboutInvalidApps = Meteor.bindEnvironment(function _notifyAdmi const rocketCatMessage = 'There is one or more apps in an invalid state. Go to Administration > Apps to review.'; const link = '/admin/apps'; - sendMessagesToAdmins({ + Promise.await(sendMessagesToAdmins({ msgs: ({ adminUser }) => ({ msg: `*${ TAPi18n.__(title, adminUser.language) }*\n${ TAPi18n.__(rocketCatMessage, adminUser.language) }` }), banners: ({ adminUser }) => { Users.removeBannerById(adminUser._id, { id }); @@ -41,7 +42,7 @@ const notifyAdminsAboutInvalidApps = Meteor.bindEnvironment(function _notifyAdmi link, }]; }, - }); + })); return apps; }); @@ -59,19 +60,19 @@ const notifyAdminsAboutRenewedApps = Meteor.bindEnvironment(function _notifyAdmi const rocketCatMessage = 'There is one or more disabled apps with valid licenses. Go to Administration > Apps to review.'; - sendMessagesToAdmins({ + Promise.await(sendMessagesToAdmins({ msgs: ({ adminUser }) => ({ msg: `${ TAPi18n.__(rocketCatMessage, adminUser.language) }` }), - }); + })); }); export const appsUpdateMarketplaceInfo = Meteor.bindEnvironment(function _appsUpdateMarketplaceInfo() { - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); const baseUrl = Apps.getMarketplaceUrl(); - const [workspaceIdSetting] = Settings.findById('Cloud_Workspace_Id').fetch(); + const workspaceIdSetting = Promise.await(Settings.getValueById('Cloud_Workspace_Id')); const currentSeats = Users.getActiveLocalUserCount(); - const fullUrl = `${ baseUrl }/v1/workspaces/${ workspaceIdSetting.value }/apps?seats=${ currentSeats }`; + const fullUrl = `${ baseUrl }/v1/workspaces/${ workspaceIdSetting }/apps?seats=${ currentSeats }`; const options = { headers: { Authorization: `Bearer ${ token }`, diff --git a/app/apps/server/orchestrator.js b/app/apps/server/orchestrator.js index a68d24197f56..81eaad8110d8 100644 --- a/app/apps/server/orchestrator.js +++ b/app/apps/server/orchestrator.js @@ -158,7 +158,8 @@ export class AppServerOrchestrator { return this._manager.load() .then((affs) => console.log(`Loaded the Apps Framework and loaded a total of ${ affs.length } Apps!`)) - .catch((err) => console.warn('Failed to load the Apps Framework and Apps!', err)); + .catch((err) => console.warn('Failed to load the Apps Framework and Apps!', err)) + .then(() => this.getBridges().getSchedulerBridge().startScheduler()); } async unload() { diff --git a/app/apps/server/tests/messages.tests.js b/app/apps/server/tests/messages.tests.js index 9dee0c68bcda..9e4919731e8c 100644 --- a/app/apps/server/tests/messages.tests.js +++ b/app/apps/server/tests/messages.tests.js @@ -1,7 +1,5 @@ -/* eslint-env mocha */ -import 'babel-polyfill'; import mock from 'mock-require'; -import chai from 'chai'; +import { expect } from 'chai'; import { AppServerOrchestratorMock } from './mocks/orchestrator.mock'; import { appMessageMock, appMessageInvalidRoomMock } from './mocks/data/messages.data'; @@ -9,10 +7,6 @@ import { MessagesMock } from './mocks/models/Messages.mock'; import { RoomsMock } from './mocks/models/Rooms.mock'; import { UsersMock } from './mocks/models/Users.mock'; -chai.use(require('chai-datetime')); - -const { expect } = chai; - mock('../../../models', './mocks/models'); mock('meteor/random', { id: () => 1, diff --git a/app/authentication/server/lib/restrictLoginAttempts.ts b/app/authentication/server/lib/restrictLoginAttempts.ts index d3ca8f78e5ca..2b63656d87e1 100644 --- a/app/authentication/server/lib/restrictLoginAttempts.ts +++ b/app/authentication/server/lib/restrictLoginAttempts.ts @@ -1,12 +1,10 @@ import moment from 'moment'; import { ILoginAttempt } from '../ILoginAttempt'; -import { ServerEvents, Users, Rooms } from '../../../models/server/raw'; -import { IServerEventType } from '../../../../definition/IServerEvent'; -import { IUser } from '../../../../definition/IUser'; +import { ServerEvents, Users, Rooms, Sessions } from '../../../models/server/raw'; +import { IServerEventType, IServerEvent } from '../../../../definition/IServerEvent'; import { settings } from '../../../settings/server'; import { addMinutesToADate } from '../../../../lib/utils/addMinutesToADate'; -import Sessions from '../../../models/server/raw/Sessions'; import { getClientAddress } from '../../../../server/lib/getClientAddress'; import { sendMessage } from '../../../lib/server/functions'; import { Logger } from '../../../logger/server'; @@ -52,7 +50,7 @@ export const isValidLoginAttemptByIp = async (ip: string): Promise => { return true; } - const lastLogin = await Sessions.findLastLoginByIp(ip) as {loginAt?: Date} | undefined; + const lastLogin = await Sessions.findLastLoginByIp(ip); let failedAttemptsSinceLastLogin; if (!lastLogin || !lastLogin.loginAt) { @@ -92,7 +90,7 @@ export const isValidAttemptByUser = async (login: ILoginAttempt): Promise => { - const user: Partial = { + const user: IServerEvent['u'] = { _id: login.user?._id, username: login.user?.username || login.methodArguments[0].user?.username, }; @@ -142,10 +140,15 @@ export const saveFailedLoginAttempts = async (login: ILoginAttempt): Promise => { + const user: IServerEvent['u'] = { + _id: login.user?._id, + username: login.user?.username || login.methodArguments[0].user?.username, + }; + await ServerEvents.insertOne({ ip: getClientAddress(login.connection), t: IServerEventType.LOGIN, ts: new Date(), - u: login.user, + u: user, }); }; diff --git a/app/authentication/server/startup/index.js b/app/authentication/server/startup/index.js index ff9e6b02f431..ed98b62aaf28 100644 --- a/app/authentication/server/startup/index.js +++ b/app/authentication/server/startup/index.js @@ -8,8 +8,8 @@ import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; import { callbacks } from '../../../callbacks/server'; -import { Roles, Users, Settings } from '../../../models/server'; -import { Users as UsersRaw } from '../../../models/server/raw'; +import { Users, Settings } from '../../../models/server'; +import { Roles, Users as UsersRaw } from '../../../models/server/raw'; import { addUserRoles } from '../../../authorization/server'; import { getAvatarSuggestionForUser } from '../../../lib/server/functions'; import { @@ -186,8 +186,7 @@ Accounts.onCreateUser(function(options, user = {}) { if (!user.active) { const destinations = []; - - Roles.findUsersInRole('admin').forEach((adminUser) => { + Promise.await(Roles.findUsersInRole('admin').toArray()).forEach((adminUser) => { if (Array.isArray(adminUser.emails)) { adminUser.emails.forEach((email) => { destinations.push(`${ adminUser.name }<${ email.address }>`); diff --git a/app/authorization/client/hasPermission.ts b/app/authorization/client/hasPermission.ts index 744903bc5062..23a0aeeca8a6 100644 --- a/app/authorization/client/hasPermission.ts +++ b/app/authorization/client/hasPermission.ts @@ -7,11 +7,11 @@ import { IUser } from '../../../definition/IUser'; import { IRole } from '../../../definition/IRole'; import { IPermission } from '../../../definition/IPermission'; -const isValidScope = (scope: IRole['scope']): scope is keyof typeof Models => +const isValidScope = (scope: IRole['scope']): boolean => typeof scope === 'string' && scope in Models; const createPermissionValidator = (quantifier: (predicate: (permissionId: IPermission['_id']) => boolean) => boolean) => - (permissionIds: IPermission['_id'][], scope: IRole['scope'], userId: IUser['_id']): boolean => { + (permissionIds: IPermission['_id'][], scope: string | undefined, userId: IUser['_id']): boolean => { const user: IUser | null = Models.Users.findOneById(userId, { fields: { roles: 1 } }); const checkEachPermission = quantifier.bind(permissionIds); @@ -34,7 +34,7 @@ const createPermissionValidator = (quantifier: (predicate: (permissionId: IPermi return false; } - const model = Models[roleScope]; + const model = Models[roleScope as keyof typeof Models]; return model.isUserInRole && model.isUserInRole(userId, roleName, scope); }); }); @@ -46,8 +46,8 @@ const all = createPermissionValidator(Array.prototype.every); const validatePermissions = ( permissions: IPermission['_id'] | IPermission['_id'][], - scope: IRole['scope'], - predicate: (permissionIds: IPermission['_id'][], scope: IRole['scope'], userId: IUser['_id']) => boolean, + scope: string | undefined, + predicate: (permissionIds: IPermission['_id'][], scope: string | undefined, userId: IUser['_id']) => boolean, userId?: IUser['_id'] | null, ): boolean => { userId = userId ?? Meteor.userId(); @@ -65,17 +65,17 @@ const validatePermissions = ( export const hasAllPermission = ( permissions: IPermission['_id'] | IPermission['_id'][], - scope?: IRole['scope'], + scope?: string, ): boolean => validatePermissions(permissions, scope, all); export const hasAtLeastOnePermission = ( permissions: IPermission['_id'] | IPermission['_id'][], - scope?: IRole['scope'], + scope?: string, ): boolean => validatePermissions(permissions, scope, atLeastOne); export const userHasAllPermission = ( permissions: IPermission['_id'] | IPermission['_id'][], - scope?: IRole['scope'], + scope?: string, userId?: IUser['_id'] | null, ): boolean => validatePermissions(permissions, scope, all, userId); diff --git a/app/authorization/server/functions/addUserRoles.js b/app/authorization/server/functions/addUserRoles.ts similarity index 54% rename from app/authorization/server/functions/addUserRoles.js rename to app/authorization/server/functions/addUserRoles.ts index 46302e81eb8d..dda983ff6a3c 100644 --- a/app/authorization/server/functions/addUserRoles.js +++ b/app/authorization/server/functions/addUserRoles.ts @@ -2,9 +2,11 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { getRoles } from './getRoles'; -import { Users, Roles } from '../../../models'; +import { Users } from '../../../models/server'; +import { IRole, IUser } from '../../../../definition/IUser'; +import { Roles } from '../../../models/server/raw'; -export const addUserRoles = (userId, roleNames, scope) => { +export const addUserRoles = (userId: IUser['_id'], roleNames: IRole['name'][], scope?: string): boolean => { if (!userId || !roleNames) { return false; } @@ -16,17 +18,19 @@ export const addUserRoles = (userId, roleNames, scope) => { }); } - roleNames = [].concat(roleNames); + if (!Array.isArray(roleNames)) { // TODO: remove this check + roleNames = [roleNames]; + } + const existingRoleNames = _.pluck(getRoles(), '_id'); const invalidRoleNames = _.difference(roleNames, existingRoleNames); if (!_.isEmpty(invalidRoleNames)) { for (const role of invalidRoleNames) { - Roles.createOrUpdate(role); + Promise.await(Roles.createOrUpdate(role)); } } - Roles.addUserRoles(userId, roleNames, scope); - + Promise.await(Roles.addUserRoles(userId, roleNames, scope)); return true; }; diff --git a/app/authorization/server/functions/canAccessRoom.ts b/app/authorization/server/functions/canAccessRoom.ts index 5a943ec031a6..d232f890af2e 100644 --- a/app/authorization/server/functions/canAccessRoom.ts +++ b/app/authorization/server/functions/canAccessRoom.ts @@ -1,5 +1,3 @@ -import { Promise } from 'meteor/promise'; - import { Authorization } from '../../../../server/sdk'; import { IAuthorization } from '../../../../server/sdk/types/IAuthorization'; diff --git a/app/authorization/server/functions/getRoles.js b/app/authorization/server/functions/getRoles.js deleted file mode 100644 index 9d20c72d29a9..000000000000 --- a/app/authorization/server/functions/getRoles.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Roles } from '../../../models'; - -export const getRoles = () => Roles.find().fetch(); diff --git a/app/authorization/server/functions/getRoles.ts b/app/authorization/server/functions/getRoles.ts new file mode 100644 index 000000000000..27de1000bb0a --- /dev/null +++ b/app/authorization/server/functions/getRoles.ts @@ -0,0 +1,4 @@ +import { IRole } from '../../../../definition/IUser'; +import { Roles } from '../../../models/server/raw'; + +export const getRoles = (): IRole[] => Promise.await(Roles.find().toArray()); diff --git a/app/authorization/server/functions/getUsersInRole.js b/app/authorization/server/functions/getUsersInRole.js deleted file mode 100644 index 27c369acf9ff..000000000000 --- a/app/authorization/server/functions/getUsersInRole.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Roles } from '../../../models'; - -export const getUsersInRole = (roleName, scope, options) => Roles.findUsersInRole(roleName, scope, options); diff --git a/app/authorization/server/functions/getUsersInRole.ts b/app/authorization/server/functions/getUsersInRole.ts new file mode 100644 index 000000000000..740431af3f06 --- /dev/null +++ b/app/authorization/server/functions/getUsersInRole.ts @@ -0,0 +1,14 @@ + + +import { Cursor, FindOneOptions, WithoutProjection } from 'mongodb'; + +import { IRole, IUser } from '../../../../definition/IUser'; +import { Roles } from '../../../models/server/raw'; + +export function getUsersInRole(name: IRole['name'], scope?: string): Promise>; + +export function getUsersInRole(name: IRole['name'], scope: string | undefined, options: WithoutProjection>): Promise>; + +export function getUsersInRole

(name: IRole['name'], scope: string | undefined, options: FindOneOptions

): Promise>; + +export function getUsersInRole

(name: IRole['name'], scope: string | undefined, options?: any | undefined): Promise> { return Roles.findUsersInRole(name, scope, options); } diff --git a/app/authorization/server/functions/removeUserFromRoles.js b/app/authorization/server/functions/removeUserFromRoles.js index b08c2778addb..a55d722bb891 100644 --- a/app/authorization/server/functions/removeUserFromRoles.js +++ b/app/authorization/server/functions/removeUserFromRoles.js @@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { getRoles } from './getRoles'; -import { Users, Roles } from '../../../models'; +import { Users } from '../../../models/server'; +import { Roles } from '../../../models/server/raw'; export const removeUserFromRoles = (userId, roleNames, scope) => { if (!userId || !roleNames) { @@ -27,7 +28,7 @@ export const removeUserFromRoles = (userId, roleNames, scope) => { }); } - Roles.removeUserRoles(userId, roleNames, scope); + Promise.await(Roles.removeUserRoles(userId, roleNames, scope)); return true; }; diff --git a/app/authorization/server/functions/upsertPermissions.js b/app/authorization/server/functions/upsertPermissions.ts similarity index 86% rename from app/authorization/server/functions/upsertPermissions.js rename to app/authorization/server/functions/upsertPermissions.ts index 76e97fcef480..6c41b1b11af0 100644 --- a/app/authorization/server/functions/upsertPermissions.js +++ b/app/authorization/server/functions/upsertPermissions.ts @@ -1,11 +1,11 @@ /* eslint no-multi-spaces: 0 */ -import Roles from '../../../models/server/models/Roles'; -import Permissions from '../../../models/server/models/Permissions'; -import Settings from '../../../models/server/models/Settings'; import { settings } from '../../../settings/server'; import { getSettingPermissionId, CONSTANTS } from '../../lib'; +import { Permissions, Roles, Settings } from '../../../models/server/raw'; +import { IPermission } from '../../../../definition/IPermission'; +import { ISetting } from '../../../../definition/ISetting'; -export const upsertPermissions = () => { +export const upsertPermissions = async (): Promise => { // Note: // 1.if we need to create a role that can only edit channel message, but not edit group message // then we can define edit--message instead of edit-message @@ -94,6 +94,7 @@ export const upsertPermissions = () => { { _id: 'create-invite-links', roles: ['admin', 'owner', 'moderator'] }, { _id: 'view-l-room', roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'] }, { _id: 'view-livechat-manager', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'view-omnichannel-contact-center', roles: ['livechat-manager', 'livechat-agent', 'livechat-monitor', 'admin'] }, { _id: 'edit-omnichannel-contact', roles: ['livechat-manager', 'livechat-agent', 'admin'] }, { _id: 'view-livechat-rooms', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, { _id: 'close-livechat-room', roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'] }, @@ -149,11 +150,13 @@ export const upsertPermissions = () => { { _id: 'access-mailer', roles: ['admin'] }, { _id: 'pin-message', roles: ['owner', 'moderator', 'admin'] }, { _id: 'snippet-message', roles: ['owner', 'moderator', 'admin'] }, + { _id: 'mobile-upload-file', roles: ['user', 'admin'] }, + { _id: 'mobile-download-file', roles: ['user', 'admin'] }, ]; - for (const permission of permissions) { - Permissions.create(permission._id, permission.roles); + for await (const permission of permissions) { + await Permissions.create(permission._id, permission.roles); } const defaultRoles = [ @@ -170,29 +173,30 @@ export const upsertPermissions = () => { { name: 'livechat-manager', scope: 'Users', description: 'Livechat Manager' }, ]; - for (const role of defaultRoles) { - Roles.createOrUpdate(role.name, role.scope, role.description, true, false); + for await (const role of defaultRoles) { + await Roles.createOrUpdate(role.name, role.scope as 'Users' | 'Subscriptions', role.description, true, false); } - const getPreviousPermissions = function(settingId) { - const previousSettingPermissions = {}; + const getPreviousPermissions = async function(settingId?: string): Promise> { + const previousSettingPermissions: { + [key: string]: IPermission; + } = {}; - const selector = { level: CONSTANTS.SETTINGS_LEVEL }; - if (settingId) { - selector.settingId = settingId; - } + const selector = { level: 'settings' as const, ...settingId && { settingId } }; - Permissions.find(selector).forEach( - function(permission) { + await Permissions.find(selector).forEach( + function(permission: IPermission) { previousSettingPermissions[permission._id] = permission; }); return previousSettingPermissions; }; - const createSettingPermission = function(setting, previousSettingPermissions) { + const createSettingPermission = async function(setting: ISetting, previousSettingPermissions: { + [key: string]: IPermission; + }): Promise { const permissionId = getSettingPermissionId(setting._id); - const permission = { - level: CONSTANTS.SETTINGS_LEVEL, + const permission: Omit = { + level: CONSTANTS.SETTINGS_LEVEL as 'settings' | undefined, // copy those setting-properties which are needed to properly publish the setting-based permissions settingId: setting._id, group: setting.group, @@ -211,19 +215,19 @@ export const upsertPermissions = () => { permission.sectionPermissionId = getSettingPermissionId(setting.section); } - const existent = Permissions.findOne({ + const existent = await Permissions.findOne({ _id: permissionId, ...permission, }, { fields: { _id: 1 } }); if (!existent) { try { - Permissions.upsert({ _id: permissionId }, { $set: permission }); + await Permissions.update({ _id: permissionId }, { $set: permission }, { upsert: true }); } catch (e) { if (!e.message.includes('E11000')) { // E11000 refers to a MongoDB error that can occur when using unique indexes for upserts // https://docs.mongodb.com/manual/reference/method/db.collection.update/#use-unique-indexes - Permissions.upsert({ _id: permissionId }, { $set: permission }); + await Permissions.update({ _id: permissionId }, { $set: permission }, { upsert: true }); } } } @@ -231,17 +235,17 @@ export const upsertPermissions = () => { delete previousSettingPermissions[permissionId]; }; - const createPermissionsForExistingSettings = function() { - const previousSettingPermissions = getPreviousPermissions(); + const createPermissionsForExistingSettings = async function(): Promise { + const previousSettingPermissions = await getPreviousPermissions(); - Settings.findNotHidden().fetch().forEach((setting) => { + (await Settings.findNotHidden().toArray()).forEach((setting) => { createSettingPermission(setting, previousSettingPermissions); }); // remove permissions for non-existent settings - for (const obsoletePermission in previousSettingPermissions) { + for await (const obsoletePermission of Object.keys(previousSettingPermissions)) { if (previousSettingPermissions.hasOwnProperty(obsoletePermission)) { - Permissions.remove({ _id: obsoletePermission }); + await Permissions.deleteOne({ _id: obsoletePermission }); } } }; @@ -250,9 +254,9 @@ export const upsertPermissions = () => { createPermissionsForExistingSettings(); // register a callback for settings for be create in higher-level-packages - settings.on('*', function([settingId]) { - const previousSettingPermissions = getPreviousPermissions(settingId); - const setting = Settings.findOneById(settingId); + settings.on('*', async function([settingId]) { + const previousSettingPermissions = await getPreviousPermissions(settingId); + const setting = await Settings.findOneById(settingId); if (setting) { if (!setting.hidden) { createSettingPermission(setting, previousSettingPermissions); diff --git a/app/authorization/server/methods/addPermissionToRole.js b/app/authorization/server/methods/addPermissionToRole.ts similarity index 72% rename from app/authorization/server/methods/addPermissionToRole.js rename to app/authorization/server/methods/addPermissionToRole.ts index 5ca74ed3dbc9..42990b114437 100644 --- a/app/authorization/server/methods/addPermissionToRole.js +++ b/app/authorization/server/methods/addPermissionToRole.ts @@ -1,11 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { Permissions } from '../../../models/server'; + import { hasPermission } from '../functions/hasPermission'; import { CONSTANTS, AuthorizationUtils } from '../../lib'; +import { Permissions } from '../../../models/server/raw'; Meteor.methods({ - 'authorization:addPermissionToRole'(permissionId, role) { + async 'authorization:addPermissionToRole'(permissionId, role) { if (AuthorizationUtils.isPermissionRestrictedForRole(permissionId, role)) { throw new Meteor.Error('error-action-not-allowed', 'Permission is restricted', { method: 'authorization:addPermissionToRole', @@ -14,7 +15,14 @@ Meteor.methods({ } const uid = Meteor.userId(); - const permission = Permissions.findOneById(permissionId); + const permission = await Permissions.findOneById(permissionId); + + if (!permission) { + throw new Meteor.Error('error-invalid-permission', 'Permission does not exist', { + method: 'authorization:addPermissionToRole', + action: 'Adding_permission', + }); + } if (!uid || !hasPermission(uid, 'access-permissions') || (permission.level === CONSTANTS.SETTINGS_LEVEL && !hasPermission(uid, 'access-setting-permissions'))) { throw new Meteor.Error('error-action-not-allowed', 'Adding permission is not allowed', { diff --git a/app/authorization/server/methods/addUserToRole.js b/app/authorization/server/methods/addUserToRole.ts similarity index 84% rename from app/authorization/server/methods/addUserToRole.js rename to app/authorization/server/methods/addUserToRole.ts index a7fdd21ec24d..3182d327ff47 100644 --- a/app/authorization/server/methods/addUserToRole.js +++ b/app/authorization/server/methods/addUserToRole.ts @@ -1,13 +1,14 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { Users, Roles } from '../../../models/server'; +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'; Meteor.methods({ - 'authorization:addUserToRole'(roleName, username, scope) { + async 'authorization:addUserToRole'(roleName, username, scope) { if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'access-permissions')) { throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { method: 'authorization:addUserToRole', @@ -41,13 +42,13 @@ Meteor.methods({ } // verify if user can be added to given scope - if (scope && !Roles.canAddUserToRole(user._id, roleName, scope)) { + if (scope && !await Roles.canAddUserToRole(user._id, roleName, scope)) { throw new Meteor.Error('error-invalid-user', 'User is not part of given room', { method: 'authorization:addUserToRole', }); } - const add = Roles.addUserRoles(user._id, roleName, scope); + const add = await Roles.addUserRoles(user._id, [roleName], scope); if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { diff --git a/app/authorization/server/methods/deleteRole.js b/app/authorization/server/methods/deleteRole.ts similarity index 67% rename from app/authorization/server/methods/deleteRole.js rename to app/authorization/server/methods/deleteRole.ts index 8613e1761b0a..8925942b23f3 100644 --- a/app/authorization/server/methods/deleteRole.js +++ b/app/authorization/server/methods/deleteRole.ts @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; -import * as Models from '../../../models/server'; +import { Roles } from '../../../models/server/raw'; import { hasPermission } from '../functions/hasPermission'; Meteor.methods({ - 'authorization:deleteRole'(roleName) { + async 'authorization:deleteRole'(roleName) { if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'access-permissions')) { throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { method: 'authorization:deleteRole', @@ -12,7 +12,7 @@ Meteor.methods({ }); } - const role = Models.Roles.findOne(roleName); + const role = await Roles.findOne(roleName); if (!role) { throw new Meteor.Error('error-invalid-role', 'Invalid role', { method: 'authorization:deleteRole', @@ -25,16 +25,14 @@ Meteor.methods({ }); } - const roleScope = role.scope || 'Users'; - const model = Models[roleScope]; - const existingUsers = model && model.findUsersInRoles && model.findUsersInRoles(roleName); + const users = await(await Roles.findUsersInRole(roleName)).count(); - if (existingUsers && existingUsers.count() > 0) { + if (users > 0) { throw new Meteor.Error('error-role-in-use', 'Cannot delete role because it\'s in use', { method: 'authorization:deleteRole', }); } - return Models.Roles.remove(role.name); + return Roles.removeById(role.name); }, }); diff --git a/app/authorization/server/methods/removeRoleFromPermission.js b/app/authorization/server/methods/removeRoleFromPermission.ts similarity index 70% rename from app/authorization/server/methods/removeRoleFromPermission.js rename to app/authorization/server/methods/removeRoleFromPermission.ts index e0aa20ed34db..c31592a0ceca 100644 --- a/app/authorization/server/methods/removeRoleFromPermission.js +++ b/app/authorization/server/methods/removeRoleFromPermission.ts @@ -1,13 +1,18 @@ import { Meteor } from 'meteor/meteor'; -import { Permissions } from '../../../models/server'; import { hasPermission } from '../functions/hasPermission'; import { CONSTANTS } from '../../lib'; +import { Permissions } from '../../../models/server/raw'; Meteor.methods({ - 'authorization:removeRoleFromPermission'(permissionId, role) { + async 'authorization:removeRoleFromPermission'(permissionId, role) { const uid = Meteor.userId(); - const permission = Permissions.findOneById(permissionId); + const permission = await Permissions.findOneById(permissionId); + + + if (!permission) { + throw new Meteor.Error('error-permission-not-found', 'Permission not found', { method: 'authorization:removeRoleFromPermission' }); + } if (!uid || !hasPermission(uid, 'access-permissions') || (permission.level === CONSTANTS.SETTINGS_LEVEL && !hasPermission(uid, 'access-setting-permissions'))) { throw new Meteor.Error('error-action-not-allowed', 'Removing permission is not allowed', { diff --git a/app/authorization/server/methods/removeUserFromRole.js b/app/authorization/server/methods/removeUserFromRole.js index 9a36a8895870..d98ff825af9b 100644 --- a/app/authorization/server/methods/removeUserFromRole.js +++ b/app/authorization/server/methods/removeUserFromRole.js @@ -1,13 +1,13 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { Roles } 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'; Meteor.methods({ - 'authorization:removeUserFromRole'(roleName, username, scope) { + async 'authorization:removeUserFromRole'(roleName, username, scope) { if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'access-permissions')) { throw new Meteor.Error('error-action-not-allowed', 'Access permissions is not allowed', { method: 'authorization:removeUserFromRole', @@ -44,7 +44,7 @@ Meteor.methods({ }, }).count(); - const userIsAdmin = user.roles.indexOf('admin') > -1; + const userIsAdmin = user.roles?.indexOf('admin') > -1; if (adminCount === 1 && userIsAdmin) { throw new Meteor.Error('error-action-not-allowed', 'Leaving the app without admins is not allowed', { method: 'removeUserFromRole', @@ -53,7 +53,7 @@ Meteor.methods({ } } - const remove = Roles.removeUserRoles(user._id, roleName, scope); + const remove = await Roles.removeUserRoles(user._id, [roleName], scope); if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { type: 'removed', diff --git a/app/authorization/server/methods/saveRole.js b/app/authorization/server/methods/saveRole.ts similarity index 80% rename from app/authorization/server/methods/saveRole.js rename to app/authorization/server/methods/saveRole.ts index 5e09f211240d..04f431ba9906 100644 --- a/app/authorization/server/methods/saveRole.js +++ b/app/authorization/server/methods/saveRole.ts @@ -1,12 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { Roles } 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'; Meteor.methods({ - 'authorization:saveRole'(roleData) { + async 'authorization:saveRole'(roleData) { if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'access-permissions')) { throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { method: 'authorization:saveRole', @@ -24,7 +24,7 @@ Meteor.methods({ roleData.scope = 'Users'; } - const update = Roles.createOrUpdate(roleData.name, roleData.scope, roleData.description, false, roleData.mandatory2fa); + const update = await Roles.createOrUpdate(roleData.name, roleData.scope, roleData.description, false, roleData.mandatory2fa); if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { type: 'changed', diff --git a/app/authorization/server/streamer/permissions/index.js b/app/authorization/server/streamer/permissions/index.js deleted file mode 100644 index edffbdfe3e73..000000000000 --- a/app/authorization/server/streamer/permissions/index.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import Permissions from '../../../../models/server/models/Permissions'; - -Meteor.methods({ - 'permissions/get'(updatedAt) { - // TODO: should we return this for non logged users? - // TODO: we could cache this collection - - const records = Permissions.find().fetch(); - - if (updatedAt instanceof Date) { - return { - update: records.filter((record) => record._updatedAt > updatedAt), - remove: Permissions.trashFindDeletedAfter( - updatedAt, - {}, - { fields: { _id: 1, _deletedAt: 1 } }, - ).fetch(), - }; - } - - return records; - }, -}); diff --git a/app/authorization/server/streamer/permissions/index.ts b/app/authorization/server/streamer/permissions/index.ts new file mode 100644 index 000000000000..fcc3bad0e34c --- /dev/null +++ b/app/authorization/server/streamer/permissions/index.ts @@ -0,0 +1,30 @@ +import { Meteor } from 'meteor/meteor'; +import { check, Match } from 'meteor/check'; + +import { Permissions } from '../../../../models/server/raw'; + +Meteor.methods({ + async 'permissions/get'(updatedAt: Date) { + check(updatedAt, Match.Maybe(Date)); + + // TODO: should we return this for non logged users? + // TODO: we could cache this collection + + const records = await Permissions.find( + updatedAt && { _updatedAt: { $gt: updatedAt } }, + ).toArray(); + + if (updatedAt instanceof Date) { + return { + update: records, + remove: await Permissions.trashFindDeletedAfter( + updatedAt, + {}, + { projection: { _id: 1, _deletedAt: 1 } }, + ).toArray(), + }; + } + + return records; + }, +}); diff --git a/app/autotranslate/server/permissions.js b/app/autotranslate/server/permissions.js deleted file mode 100644 index 64ce0028fa87..000000000000 --- a/app/autotranslate/server/permissions.js +++ /dev/null @@ -1,11 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Permissions } from '../../models'; - -Meteor.startup(() => { - if (Permissions) { - if (!Permissions.findOne({ _id: 'auto-translate' })) { - Permissions.insert({ _id: 'auto-translate', roles: ['admin'] }); - } - } -}); diff --git a/app/autotranslate/server/permissions.ts b/app/autotranslate/server/permissions.ts new file mode 100644 index 000000000000..5ce05e8f1ef7 --- /dev/null +++ b/app/autotranslate/server/permissions.ts @@ -0,0 +1,9 @@ +import { Meteor } from 'meteor/meteor'; + +import { Permissions } from '../../models/server/raw'; + +Meteor.startup(async () => { + if (!await Permissions.findOne({ _id: 'auto-translate' })) { + Permissions.create('auto-translate', ['admin']); + } +}); diff --git a/app/cas/server/cas_server.js b/app/cas/server/cas_server.js index 646e87a8f053..cc569eeab441 100644 --- a/app/cas/server/cas_server.js +++ b/app/cas/server/cas_server.js @@ -10,7 +10,8 @@ import CAS from 'cas'; import { logger } from './cas_rocketchat'; import { settings } from '../../settings'; -import { Rooms, CredentialTokens } from '../../models/server'; +import { Rooms } from '../../models/server'; +import { CredentialTokens } from '../../models/server/raw'; import { _setRealName } from '../../lib'; import { createRoom } from '../../lib/server/functions/createRoom'; @@ -43,7 +44,7 @@ const casTicket = function(req, token, callback) { service: `${ appUrl }/_cas/${ token }`, }); - cas.validate(ticketId, Meteor.bindEnvironment(function(err, status, username, details) { + cas.validate(ticketId, Meteor.bindEnvironment(async function(err, status, username, details) { if (err) { logger.error(`error when trying to validate: ${ err.message }`); } else if (status) { @@ -54,11 +55,11 @@ const casTicket = function(req, token, callback) { if (details && details.attributes) { _.extend(user_info, { attributes: details.attributes }); } - CredentialTokens.create(token, user_info); + await CredentialTokens.create(token, user_info); } else { logger.error(`Unable to validate ticket: ${ ticketId }`); } - // logger.debug("Receveied response: " + JSON.stringify(details, null , 4)); + // logger.debug("Received response: " + JSON.stringify(details, null , 4)); callback(); })); @@ -114,7 +115,8 @@ Accounts.registerLoginHandler(function(options) { return undefined; } - const credentials = CredentialTokens.findOneById(options.cas.credentialToken); + // TODO: Sync wrapper due to the chain conversion to async models + const credentials = Promise.await(CredentialTokens.findOneNotExpiredById(options.cas.credentialToken)); if (credentials === undefined) { throw new Meteor.Error(Accounts.LoginCancelledError.numericError, 'no matching login attempt found'); diff --git a/app/channel-settings/server/functions/saveRoomName.js b/app/channel-settings/server/functions/saveRoomName.js index 5d3197d133b3..0cc31cfa77b4 100644 --- a/app/channel-settings/server/functions/saveRoomName.js +++ b/app/channel-settings/server/functions/saveRoomName.js @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; -import { Rooms, Messages, Subscriptions, Integrations } from '../../../models/server'; +import { Rooms, Messages, Subscriptions } from '../../../models/server'; +import { Integrations } from '../../../models/server/raw'; import { roomTypes, getValidRoomName } from '../../../utils/server'; import { callbacks } from '../../../callbacks/server'; import { checkUsernameAvailability } from '../../../lib/server/functions'; @@ -19,7 +20,7 @@ const updateRoomName = (rid, displayName, isDiscussion) => { return Rooms.setNameById(rid, slugifiedRoomName, displayName) && Subscriptions.updateNameAndAlertByRoomId(rid, slugifiedRoomName, displayName); }; -export const saveRoomName = function(rid, displayName, user, sendMessage = true) { +export async function saveRoomName(rid, displayName, user, sendMessage = true) { const room = Rooms.findOneById(rid); if (roomTypes.getConfig(room.t).preventRenaming()) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { @@ -35,10 +36,10 @@ export const saveRoomName = function(rid, displayName, user, sendMessage = true) return; } - Integrations.updateRoomName(room.name, displayName); + await Integrations.updateRoomName(room.name, displayName); if (sendMessage) { Messages.createRoomRenamedWithRoomIdRoomNameAndUser(rid, displayName, user); } callbacks.run('afterRoomNameChange', { rid, name: displayName, oldName: room.name }); return displayName; -}; +} diff --git a/app/channel-settings/server/methods/saveRoomSettings.js b/app/channel-settings/server/methods/saveRoomSettings.js index 811c492fb70d..59c0bb239f79 100644 --- a/app/channel-settings/server/methods/saveRoomSettings.js +++ b/app/channel-settings/server/methods/saveRoomSettings.js @@ -128,7 +128,7 @@ const validators = { const settingSavers = { roomName({ value, rid, user, room }) { - if (!saveRoomName(rid, value, user)) { + if (!Promise.await(saveRoomName(rid, value, user))) { return; } @@ -231,13 +231,13 @@ const settingSavers = { favorite({ value, rid }) { Rooms.saveFavoriteById(rid, value.favorite, value.defaultValue); }, - roomAvatar({ value, rid, user }) { - setRoomAvatar(rid, value, user); + async roomAvatar({ value, rid, user }) { + await setRoomAvatar(rid, value, user); }, }; Meteor.methods({ - saveRoomSettings(rid, settings, value) { + async saveRoomSettings(rid, settings, value) { const userId = Meteor.userId(); if (!userId) { @@ -313,10 +313,10 @@ Meteor.methods({ }); // saving data - Object.keys(settings).forEach((setting) => { + for await (const setting of Object.keys(settings)) { const value = settings[setting]; - const saver = settingSavers[setting]; + const saver = await settingSavers[setting]; if (saver) { saver({ value, @@ -325,7 +325,7 @@ Meteor.methods({ user, }); } - }); + } Meteor.defer(function() { const room = Rooms.findOneById(rid); diff --git a/app/cloud/server/functions/buildRegistrationData.js b/app/cloud/server/functions/buildRegistrationData.js index d8ecff67687f..5346558e23ab 100644 --- a/app/cloud/server/functions/buildRegistrationData.js +++ b/app/cloud/server/functions/buildRegistrationData.js @@ -1,10 +1,11 @@ import { settings } from '../../../settings/server'; -import { Users, Statistics } from '../../../models/server'; +import { Users } from '../../../models/server'; +import { Statistics } from '../../../models/server/raw'; import { statistics } from '../../../statistics'; import { LICENSE_VERSION } from '../license'; -export function buildWorkspaceRegistrationData() { - const stats = Statistics.findLast() || statistics.get(); +export async function buildWorkspaceRegistrationData() { + const stats = await Statistics.findLast() || statistics.get(); const address = settings.get('Site_Url'); const siteName = settings.get('Site_Name'); diff --git a/app/cloud/server/functions/startRegisterWorkspace.js b/app/cloud/server/functions/startRegisterWorkspace.js index bb533c79c580..2f9e4f90b789 100644 --- a/app/cloud/server/functions/startRegisterWorkspace.js +++ b/app/cloud/server/functions/startRegisterWorkspace.js @@ -7,18 +7,17 @@ import { Settings } from '../../../models'; import { buildWorkspaceRegistrationData } from './buildRegistrationData'; import { SystemLogger } from '../../../../server/lib/logger/system'; - -export function startRegisterWorkspace(resend = false) { +export async function startRegisterWorkspace(resend = false) { const { workspaceRegistered, connectToCloud } = retrieveRegistrationStatus(); if ((workspaceRegistered && connectToCloud) || process.env.TEST_MODE) { - syncWorkspace(true); + await syncWorkspace(true); return true; } Settings.updateValueById('Register_Server', true); - const regInfo = buildWorkspaceRegistrationData(); + const regInfo = await buildWorkspaceRegistrationData(); const cloudUrl = settings.get('Cloud_Url'); diff --git a/app/cloud/server/functions/syncWorkspace.js b/app/cloud/server/functions/syncWorkspace.js index 03f67acf4a4b..1b1021402e25 100644 --- a/app/cloud/server/functions/syncWorkspace.js +++ b/app/cloud/server/functions/syncWorkspace.js @@ -10,13 +10,13 @@ import { getAndCreateNpsSurvey } from '../../../../server/services/nps/getAndCre import { NPS, Banner } from '../../../../server/sdk'; import { SystemLogger } from '../../../../server/lib/logger/system'; -export function syncWorkspace(reconnectCheck = false) { +export async function syncWorkspace(reconnectCheck = false) { const { workspaceRegistered, connectToCloud } = retrieveRegistrationStatus(); if (!workspaceRegistered || (!connectToCloud && !reconnectCheck)) { return false; } - const info = buildWorkspaceRegistrationData(); + const info = await buildWorkspaceRegistrationData(); const workspaceUrl = settings.get('Cloud_Workspace_Registration_Client_Uri'); @@ -64,11 +64,11 @@ export function syncWorkspace(reconnectCheck = false) { const startAt = new Date(data.nps.startAt); - Promise.await(NPS.create({ + await NPS.create({ npsId, startAt, expireAt: new Date(expireAt), - })); + }); const now = new Date(); @@ -79,19 +79,19 @@ export function syncWorkspace(reconnectCheck = false) { // add banners if (data.banners) { - for (const banner of data.banners) { + for await (const banner of data.banners) { const { createdAt, expireAt, startAt, } = banner; - Promise.await(Banner.create({ + await Banner.create({ ...banner, createdAt: new Date(createdAt), expireAt: new Date(expireAt), startAt: new Date(startAt), - })); + }); } } diff --git a/app/cloud/server/index.js b/app/cloud/server/index.js index 98ae3b710b96..eb239b095c68 100644 --- a/app/cloud/server/index.js +++ b/app/cloud/server/index.js @@ -6,9 +6,12 @@ import { getWorkspaceAccessToken } from './functions/getWorkspaceAccessToken'; import { getWorkspaceAccessTokenWithScope } from './functions/getWorkspaceAccessTokenWithScope'; import { getWorkspaceLicense } from './functions/getWorkspaceLicense'; import { getUserCloudAccessToken } from './functions/getUserCloudAccessToken'; +import { retrieveRegistrationStatus } from './functions/retrieveRegistrationStatus'; import { getWorkspaceKey } from './functions/getWorkspaceKey'; import { syncWorkspace } from './functions/syncWorkspace'; +import { connectWorkspace } from './functions/connectWorkspace'; import { settings } from '../../settings/server'; +import { SystemLogger } from '../../../server/lib/logger/system'; const licenseCronName = 'Cloud Workspace Sync'; @@ -34,6 +37,22 @@ Meteor.startup(function() { job: syncWorkspace, }); }); + + const { workspaceRegistered } = retrieveRegistrationStatus(); + + if (process.env.REG_TOKEN && process.env.REG_TOKEN !== '' && !workspaceRegistered) { + try { + SystemLogger.info('REG_TOKEN Provided. Attempting to register'); + + if (!connectWorkspace(process.env.REG_TOKEN)) { + throw new Error('Couldn\'t register with token. Please make sure token is valid or hasn\'t already been used'); + } + + console.log('Successfully registered with token provided by REG_TOKEN!'); + } catch (e) { + SystemLogger.error('An error occured registering with token.', e.message); + } + } }); export { getWorkspaceAccessToken, getWorkspaceAccessTokenWithScope, getWorkspaceLicense, getWorkspaceKey, getUserCloudAccessToken }; diff --git a/app/cloud/server/methods.js b/app/cloud/server/methods.js index 7723566601f5..83847711a603 100644 --- a/app/cloud/server/methods.js +++ b/app/cloud/server/methods.js @@ -26,7 +26,7 @@ Meteor.methods({ return retrieveRegistrationStatus(); }, - 'cloud:getWorkspaceRegisterData'() { + async 'cloud:getWorkspaceRegisterData'() { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cloud:getWorkspaceRegisterData' }); } @@ -35,9 +35,9 @@ Meteor.methods({ throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'cloud:getWorkspaceRegisterData' }); } - return Buffer.from(JSON.stringify(buildWorkspaceRegistrationData())).toString('base64'); + return Buffer.from(JSON.stringify(await buildWorkspaceRegistrationData())).toString('base64'); }, - 'cloud:registerWorkspace'() { + async 'cloud:registerWorkspace'() { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cloud:startRegister' }); } @@ -48,7 +48,7 @@ Meteor.methods({ return startRegisterWorkspace(); }, - 'cloud:syncWorkspace'() { + async 'cloud:syncWorkspace'() { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cloud:syncWorkspace' }); } diff --git a/app/crowd/server/crowd.js b/app/crowd/server/crowd.js index d5b4b5b97ef3..dba130ef489e 100644 --- a/app/crowd/server/crowd.js +++ b/app/crowd/server/crowd.js @@ -208,7 +208,7 @@ export class CROWD { if (settings.get('CROWD_Remove_Orphaned_Users') === true) { logger.info('Removing user:', crowd_username); Meteor.defer(function() { - deleteUser(user._id); + Promise.await(deleteUser(user._id)); logger.info('User removed:', crowd_username); }); } diff --git a/app/custom-oauth/server/custom_oauth_server.js b/app/custom-oauth/server/custom_oauth_server.js index 2be67a092547..2e62be9647eb 100644 --- a/app/custom-oauth/server/custom_oauth_server.js +++ b/app/custom-oauth/server/custom_oauth_server.js @@ -334,6 +334,8 @@ export class CustomOAuth { return; } + callbacks.run('afterProcessOAuthUser', { serviceName, serviceData, user }); + // User already created or merged and has identical name as before if (user.services && user.services[serviceName] && user.services[serviceName].id === serviceData.id && user.name === serviceData.name) { return; @@ -343,8 +345,6 @@ export class CustomOAuth { throw new Meteor.Error('CustomOAuth', `User with username ${ user.username } already exists`); } - callbacks.run('afterProcessOAuthUser', { serviceName, serviceData, user }); - const serviceIdKey = `services.${ serviceName }.id`; const update = { $set: { diff --git a/app/custom-oauth/server/transform_helpers.tests.js b/app/custom-oauth/server/transform_helpers.tests.js index ec1475780e68..5139edb2410e 100644 --- a/app/custom-oauth/server/transform_helpers.tests.js +++ b/app/custom-oauth/server/transform_helpers.tests.js @@ -1,5 +1,3 @@ -/* eslint-env mocha */ - import { expect } from 'chai'; import { diff --git a/app/custom-sounds/server/methods/deleteCustomSound.js b/app/custom-sounds/server/methods/deleteCustomSound.js index b72c852bacfc..d168fac12d1d 100644 --- a/app/custom-sounds/server/methods/deleteCustomSound.js +++ b/app/custom-sounds/server/methods/deleteCustomSound.js @@ -1,16 +1,16 @@ import { Meteor } from 'meteor/meteor'; -import { CustomSounds } from '../../../models'; +import { CustomSounds } from '../../../models/server/raw'; import { hasPermission } from '../../../authorization'; import { Notifications } from '../../../notifications'; import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; Meteor.methods({ - deleteCustomSound(_id) { + async deleteCustomSound(_id) { let sound = null; if (hasPermission(this.userId, 'manage-sounds')) { - sound = CustomSounds.findOneById(_id); + sound = await CustomSounds.findOneById(_id); } else { throw new Meteor.Error('not_authorized'); } @@ -20,7 +20,7 @@ Meteor.methods({ } RocketChatFileCustomSoundsInstance.deleteFile(`${ sound._id }.${ sound.extension }`); - CustomSounds.removeById(_id); + await CustomSounds.removeById(_id); Notifications.notifyAll('deleteCustomSound', { soundData: sound }); return true; diff --git a/app/custom-sounds/server/methods/insertOrUpdateSound.js b/app/custom-sounds/server/methods/insertOrUpdateSound.js index d3fe25e0173b..b1fa7c749747 100644 --- a/app/custom-sounds/server/methods/insertOrUpdateSound.js +++ b/app/custom-sounds/server/methods/insertOrUpdateSound.js @@ -3,12 +3,12 @@ import s from 'underscore.string'; import { check } from 'meteor/check'; import { hasPermission } from '../../../authorization'; -import { CustomSounds } from '../../../models'; +import { CustomSounds } from '../../../models/server/raw'; import { Notifications } from '../../../notifications'; import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; Meteor.methods({ - insertOrUpdateSound(soundData) { + async insertOrUpdateSound(soundData) { if (!hasPermission(this.userId, 'manage-sounds')) { throw new Meteor.Error('not_authorized'); } @@ -34,9 +34,9 @@ Meteor.methods({ if (soundData._id) { check(soundData._id, String); - matchingResults = CustomSounds.findByNameExceptId(soundData.name, soundData._id).fetch(); + matchingResults = await CustomSounds.findByNameExceptId(soundData.name, soundData._id).toArray(); } else { - matchingResults = CustomSounds.findByName(soundData.name).fetch(); + matchingResults = await CustomSounds.findByName(soundData.name).toArray(); } if (matchingResults.length > 0) { @@ -50,7 +50,7 @@ Meteor.methods({ extension: soundData.extension, }; - const _id = CustomSounds.create(createSound); + const _id = await (await CustomSounds.create(createSound)).insertedId; createSound._id = _id; return _id; @@ -61,7 +61,7 @@ Meteor.methods({ } if (soundData.name !== soundData.previousName) { - CustomSounds.setName(soundData._id, soundData.name); + await CustomSounds.setName(soundData._id, soundData.name); Notifications.notifyAll('updateCustomSound', { soundData }); } diff --git a/app/custom-sounds/server/methods/listCustomSounds.js b/app/custom-sounds/server/methods/listCustomSounds.js index 90bf6db20435..475da52286be 100644 --- a/app/custom-sounds/server/methods/listCustomSounds.js +++ b/app/custom-sounds/server/methods/listCustomSounds.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; -import { CustomSounds } from '../../../models'; +import { CustomSounds } from '../../../models/server/raw'; Meteor.methods({ - listCustomSounds() { - return CustomSounds.find({}).fetch(); + async listCustomSounds() { + return CustomSounds.find({}).toArray(); }, }); diff --git a/app/discussion/client/createDiscussionMessageAction.js b/app/discussion/client/createDiscussionMessageAction.js index 7d107be268b2..120001c9e52d 100644 --- a/app/discussion/client/createDiscussionMessageAction.js +++ b/app/discussion/client/createDiscussionMessageAction.js @@ -22,12 +22,12 @@ Meteor.startup(function() { label: 'Discussion_start', context: ['message', 'message-mobile'], async action() { - const { msg: message } = messageArgs(this); + const { msg: message, room } = messageArgs(this); imperativeModal.open({ component: CreateDiscussion, props: { - defaultParentRoom: message.rid, + defaultParentRoom: room.prid || room._id, onClose: imperativeModal.close, parentMessageId: message._id, nameSuggestion: message?.msg?.substr(0, 140), diff --git a/app/discussion/client/discussionFromMessageBox.js b/app/discussion/client/discussionFromMessageBox.js index 668cf6ac75b5..e2a8b29c845b 100644 --- a/app/discussion/client/discussionFromMessageBox.js +++ b/app/discussion/client/discussionFromMessageBox.js @@ -20,7 +20,7 @@ Meteor.startup(function() { imperativeModal.open({ component: CreateDiscussion, props: { - defaultParentRoom: data.rid, + defaultParentRoom: data.prid || data.rid, onClose: imperativeModal.close, }, }); diff --git a/app/discussion/server/methods/createDiscussion.js b/app/discussion/server/methods/createDiscussion.js index b9d28a0d58df..8c1ea7dbb3d3 100644 --- a/app/discussion/server/methods/createDiscussion.js +++ b/app/discussion/server/methods/createDiscussion.js @@ -38,7 +38,7 @@ const mentionMessage = (rid, { _id, username, name }, message_embedded) => { }; const create = ({ prid, pmid, t_name, reply, users, user, encrypted }) => { - // if you set both, prid and pmid, and the rooms doesnt match... should throw an error) + // if you set both, prid and pmid, and the rooms dont match... should throw an error) let message = false; if (pmid) { message = Messages.findOne({ _id: pmid }); diff --git a/app/discussion/server/permissions.js b/app/discussion/server/permissions.ts similarity index 87% rename from app/discussion/server/permissions.js rename to app/discussion/server/permissions.ts index 3d54e4c66b16..da3ac2ee2290 100644 --- a/app/discussion/server/permissions.js +++ b/app/discussion/server/permissions.ts @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; -import { Permissions } from '../../models'; +import { Permissions } from '../../models/server/raw'; + Meteor.startup(() => { // Add permissions for discussion diff --git a/app/e2e/server/beforeCreateRoom.js b/app/e2e/server/beforeCreateRoom.js index ce3b21ad6935..a8c6a8933519 100644 --- a/app/e2e/server/beforeCreateRoom.js +++ b/app/e2e/server/beforeCreateRoom.js @@ -3,9 +3,8 @@ import { settings } from '../../settings/server'; callbacks.add('beforeCreateRoom', ({ type, extraData }) => { if ( - settings.get('E2E_Enabled') && ((type === 'd' && settings.get('E2E_Enabled_Default_DirectRooms')) - || (type === 'p' && settings.get('E2E_Enabled_Default_PrivateRooms'))) - ) { + settings.get('E2E_Enable') && ((type === 'd' && settings.get('E2E_Enabled_Default_DirectRooms')) + || (type === 'p' && settings.get('E2E_Enabled_Default_PrivateRooms')))) { extraData.encrypted = extraData.encrypted ?? true; } }); diff --git a/app/e2e/server/settings.ts b/app/e2e/server/settings.ts index 3b3aad9e6dc7..20c624fed9b5 100644 --- a/app/e2e/server/settings.ts +++ b/app/e2e/server/settings.ts @@ -11,11 +11,13 @@ settingsRegistry.addGroup('E2E Encryption', function() { this.add('E2E_Enabled_Default_DirectRooms', false, { type: 'boolean', + public: true, enableQuery: { _id: 'E2E_Enable', value: true }, }); this.add('E2E_Enabled_Default_PrivateRooms', false, { type: 'boolean', + public: true, enableQuery: { _id: 'E2E_Enable', value: true }, }); }); diff --git a/app/emoji-custom/server/methods/deleteEmojiCustom.js b/app/emoji-custom/server/methods/deleteEmojiCustom.js index 7393f245b459..2964c5ff6cd6 100644 --- a/app/emoji-custom/server/methods/deleteEmojiCustom.js +++ b/app/emoji-custom/server/methods/deleteEmojiCustom.js @@ -2,22 +2,22 @@ import { Meteor } from 'meteor/meteor'; import { api } from '../../../../server/sdk/api'; import { hasPermission } from '../../../authorization'; -import { EmojiCustom } from '../../../models'; +import { EmojiCustom } from '../../../models/server/raw'; import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom'; Meteor.methods({ - deleteEmojiCustom(emojiID) { + async deleteEmojiCustom(emojiID) { if (!hasPermission(this.userId, 'manage-emoji')) { throw new Meteor.Error('not_authorized'); } - const emoji = EmojiCustom.findOneById(emojiID); + const emoji = await EmojiCustom.findOneById(emojiID); if (emoji == null) { throw new Meteor.Error('Custom_Emoji_Error_Invalid_Emoji', 'Invalid emoji', { method: 'deleteEmojiCustom' }); } RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${ emoji.name }.${ emoji.extension }`)); - EmojiCustom.removeById(emojiID); + await EmojiCustom.removeById(emojiID); api.broadcast('emoji.deleteCustom', emoji); return true; diff --git a/app/emoji-custom/server/methods/insertOrUpdateEmoji.js b/app/emoji-custom/server/methods/insertOrUpdateEmoji.js index b96b40b2fbd0..23843c81cec9 100644 --- a/app/emoji-custom/server/methods/insertOrUpdateEmoji.js +++ b/app/emoji-custom/server/methods/insertOrUpdateEmoji.js @@ -4,12 +4,12 @@ import s from 'underscore.string'; import limax from 'limax'; import { hasPermission } from '../../../authorization'; -import { EmojiCustom } from '../../../models'; +import { EmojiCustom } from '../../../models/server/raw'; import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom'; import { api } from '../../../../server/sdk/api'; Meteor.methods({ - insertOrUpdateEmoji(emojiData) { + async insertOrUpdateEmoji(emojiData) { if (!hasPermission(this.userId, 'manage-emoji')) { throw new Meteor.Error('not_authorized'); } @@ -50,14 +50,14 @@ Meteor.methods({ let matchingResults = []; if (emojiData._id) { - matchingResults = EmojiCustom.findByNameOrAliasExceptID(emojiData.name, emojiData._id).fetch(); - for (const alias of emojiData.aliases) { - matchingResults = matchingResults.concat(EmojiCustom.findByNameOrAliasExceptID(alias, emojiData._id).fetch()); + matchingResults = await EmojiCustom.findByNameOrAliasExceptID(emojiData.name, emojiData._id).toArray(); + for await (const alias of emojiData.aliases) { + matchingResults = matchingResults.concat(await EmojiCustom.findByNameOrAliasExceptID(alias, emojiData._id).toArray()); } } else { - matchingResults = EmojiCustom.findByNameOrAlias(emojiData.name).fetch(); - for (const alias of emojiData.aliases) { - matchingResults = matchingResults.concat(EmojiCustom.findByNameOrAlias(alias).fetch()); + matchingResults = await EmojiCustom.findByNameOrAlias(emojiData.name).toArray(); + for await (const alias of emojiData.aliases) { + matchingResults = matchingResults.concat(await EmojiCustom.findByNameOrAlias(alias).toArray()); } } @@ -77,7 +77,7 @@ Meteor.methods({ extension: emojiData.extension, }; - const _id = EmojiCustom.create(createEmoji); + const _id = (await EmojiCustom.create(createEmoji)).insertedId; api.broadcast('emoji.updateCustom', createEmoji); @@ -90,7 +90,7 @@ Meteor.methods({ RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${ emojiData.previousName }.${ emojiData.extension }`)); RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${ emojiData.previousName }.${ emojiData.previousExtension }`)); - EmojiCustom.setExtension(emojiData._id, emojiData.extension); + await EmojiCustom.setExtension(emojiData._id, emojiData.extension); } else if (emojiData.name !== emojiData.previousName) { const rs = RocketChatFileEmojiCustomInstance.getFileWithReadStream(encodeURIComponent(`${ emojiData.previousName }.${ emojiData.previousExtension }`)); if (rs !== null) { @@ -104,13 +104,13 @@ Meteor.methods({ } if (emojiData.name !== emojiData.previousName) { - EmojiCustom.setName(emojiData._id, emojiData.name); + await EmojiCustom.setName(emojiData._id, emojiData.name); } if (emojiData.aliases) { - EmojiCustom.setAliases(emojiData._id, emojiData.aliases); + await EmojiCustom.setAliases(emojiData._id, emojiData.aliases); } else { - EmojiCustom.setAliases(emojiData._id, []); + await EmojiCustom.setAliases(emojiData._id, []); } api.broadcast('emoji.updateCustom', emojiData); diff --git a/app/emoji-custom/server/methods/listEmojiCustom.js b/app/emoji-custom/server/methods/listEmojiCustom.js index d06b382af85e..d66aeee1a6ad 100644 --- a/app/emoji-custom/server/methods/listEmojiCustom.js +++ b/app/emoji-custom/server/methods/listEmojiCustom.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; -import { EmojiCustom } from '../../../models'; +import { EmojiCustom } from '../../../models/server/raw'; Meteor.methods({ - listEmojiCustom(options = {}) { - return EmojiCustom.find(options).fetch(); + async listEmojiCustom(options = {}) { + return EmojiCustom.find(options).toArray(); }, }); diff --git a/app/federation/server/endpoints/dispatch.js b/app/federation/server/endpoints/dispatch.js index ae392ac8aac8..333a30bbeebf 100644 --- a/app/federation/server/endpoints/dispatch.js +++ b/app/federation/server/endpoints/dispatch.js @@ -4,12 +4,13 @@ import { API } from '../../../api/server'; import { serverLogger } from '../lib/logger'; import { contextDefinitions, eventTypes } from '../../../models/server/models/FederationEvents'; import { - FederationRoomEvents, FederationServers, + 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 { Notifications } from '../../../notifications/server'; @@ -139,7 +140,7 @@ const eventHandlers = { // Refresh the servers list if (federationAltered) { - FederationServers.refreshServers(); + await FederationServers.refreshServers(); // Update the room's federation property Rooms.update({ _id: roomId }, { $set: { 'federation.domains': domainsAfterAdd } }); @@ -163,7 +164,7 @@ const eventHandlers = { Subscriptions.removeByRoomIdAndUserId(roomId, user._id); // Refresh the servers list - FederationServers.refreshServers(); + await FederationServers.refreshServers(); // Update the room's federation property Rooms.update({ _id: roomId }, { $set: { 'federation.domains': domainsAfterRemoval } }); @@ -186,7 +187,7 @@ const eventHandlers = { Subscriptions.removeByRoomIdAndUserId(roomId, user._id); // Refresh the servers list - FederationServers.refreshServers(); + await FederationServers.refreshServers(); // Update the room's federation property Rooms.update({ _id: roomId }, { $set: { 'federation.domains': domainsAfterRemoval } }); @@ -226,7 +227,7 @@ const eventHandlers = { const { federation: { origin } } = denormalizedMessage; - const { upload, buffer } = getUpload(origin, denormalizedMessage.file._id); + const { upload, buffer } = await getUpload(origin, denormalizedMessage.file._id); const oldUploadId = upload._id; @@ -444,7 +445,7 @@ const eventHandlers = { }; API.v1.addRoute('federation.events.dispatch', { authRequired: false, rateLimiterOptions: { numRequestsAllowed: 30, intervalTimeInMS: 1000 } }, { - async post() { + post() { if (!isFederationEnabled()) { return API.v1.failure('Federation not enabled'); } @@ -454,7 +455,7 @@ API.v1.addRoute('federation.events.dispatch', { authRequired: false, rateLimiter let payload; try { - payload = decryptIfNeeded(this.request, this.bodyParams); + payload = Promise.await(decryptIfNeeded(this.request, this.bodyParams)); } catch (err) { return API.v1.failure('Could not decrypt payload'); } @@ -472,7 +473,7 @@ API.v1.addRoute('federation.events.dispatch', { authRequired: false, rateLimiter let eventResult; if (eventHandlers[event.type]) { - eventResult = await eventHandlers[event.type](event); + eventResult = Promise.await(eventHandlers[event.type](event)); } // If there was an error handling the event, take action @@ -480,7 +481,7 @@ API.v1.addRoute('federation.events.dispatch', { authRequired: false, rateLimiter try { serverLogger.debug({ msg: 'federation.events.dispatch => Event has missing parents', event }); - requestEventsFromLatest(event.origin, getFederationDomain(), contextDefinitions.defineType(event), event.context, eventResult.latestEventIds); + Promise.await(requestEventsFromLatest(event.origin, getFederationDomain(), contextDefinitions.defineType(event), event.context, eventResult.latestEventIds)); // And stop handling the events break; diff --git a/app/federation/server/endpoints/requestFromLatest.js b/app/federation/server/endpoints/requestFromLatest.js index cac0168c8c12..84fd69f88d3a 100644 --- a/app/federation/server/endpoints/requestFromLatest.js +++ b/app/federation/server/endpoints/requestFromLatest.js @@ -8,7 +8,7 @@ import { isFederationEnabled } from '../lib/isFederationEnabled'; import { dispatchEvents } from '../handler'; API.v1.addRoute('federation.events.requestFromLatest', { authRequired: false }, { - async post() { + post() { if (!isFederationEnabled()) { return API.v1.failure('Federation not enabled'); } @@ -18,7 +18,7 @@ API.v1.addRoute('federation.events.requestFromLatest', { authRequired: false }, let payload; try { - payload = decryptIfNeeded(this.request, this.bodyParams); + payload = Promise.await(decryptIfNeeded(this.request, this.bodyParams)); } catch (err) { return API.v1.failure('Could not decrypt payload'); } @@ -54,7 +54,7 @@ API.v1.addRoute('federation.events.requestFromLatest', { authRequired: false }, } // Dispatch all the events, on the same request - dispatchEvents([fromDomain], missingEvents); + Promise.await(dispatchEvents([fromDomain], missingEvents)); return API.v1.success(); }, diff --git a/app/federation/server/endpoints/uploads.js b/app/federation/server/endpoints/uploads.js index 7735a630f15e..a997b2aff307 100644 --- a/app/federation/server/endpoints/uploads.js +++ b/app/federation/server/endpoints/uploads.js @@ -1,5 +1,5 @@ import { API } from '../../../api/server'; -import { Uploads } from '../../../models/server'; +import { Uploads } from '../../../models/server/raw'; import { FileUpload } from '../../../file-upload/server'; import { isFederationEnabled } from '../lib/isFederationEnabled'; @@ -11,7 +11,7 @@ API.v1.addRoute('federation.uploads', { authRequired: false }, { const { upload_id } = this.requestParams(); - const upload = Uploads.findOneById(upload_id); + const upload = Promise.await(Uploads.findOneById(upload_id)); if (!upload) { return API.v1.failure('There is no such file in this server'); diff --git a/app/federation/server/functions/addUser.js b/app/federation/server/functions/addUser.js index eebd1656260b..314b7893fbc1 100644 --- a/app/federation/server/functions/addUser.js +++ b/app/federation/server/functions/addUser.js @@ -1,15 +1,16 @@ import { Meteor } from 'meteor/meteor'; import * as federationErrors from './errors'; -import { FederationServers, Users } from '../../../models/server'; +import { Users } from '../../../models/server'; +import { FederationServers } from '../../../models/server/raw'; import { getUserByUsername } from '../handler'; -export function addUser(query) { +export async function addUser(query) { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'addUser' }); } - const user = getUserByUsername(query); + const user = await getUserByUsername(query); if (!user) { throw federationErrors.userNotFound(query); @@ -22,7 +23,7 @@ export function addUser(query) { userId = Users.create(user); // Refresh the servers list - FederationServers.refreshServers(); + await FederationServers.refreshServers(); } catch (err) { // This might get called twice by the createDirectMessage method // so we need to handle the situation accordingly diff --git a/app/federation/server/functions/dashboard.js b/app/federation/server/functions/dashboard.js index 137ef802c5dc..3f256bf3d88a 100644 --- a/app/federation/server/functions/dashboard.js +++ b/app/federation/server/functions/dashboard.js @@ -1,21 +1,22 @@ import { Meteor } from 'meteor/meteor'; -import { FederationServers, FederationRoomEvents, Users } from '../../../models/server'; +import { FederationRoomEvents, Users } from '../../../models/server'; +import { FederationServers } from '../../../models/server/raw'; -export function getStatistics() { +export async function getStatistics() { const numberOfEvents = FederationRoomEvents.find().count(); const numberOfFederatedUsers = Users.findRemote().count(); - const numberOfServers = FederationServers.find().count(); + const numberOfServers = await FederationServers.find().count(); return { numberOfEvents, numberOfFederatedUsers, numberOfServers }; } -export function federationGetOverviewData() { +export async function federationGetOverviewData() { if (!Meteor.userId()) { throw new Meteor.Error('not-authorized'); } - const { numberOfEvents, numberOfFederatedUsers, numberOfServers } = getStatistics(); + const { numberOfEvents, numberOfFederatedUsers, numberOfServers } = await getStatistics(); return { data: [{ @@ -31,12 +32,12 @@ export function federationGetOverviewData() { }; } -export function federationGetServers() { +export async function federationGetServers() { if (!Meteor.userId()) { throw new Meteor.Error('not-authorized'); } - const servers = FederationServers.find().fetch(); + const servers = await FederationServers.find().toArray(); return { data: servers, diff --git a/app/federation/server/functions/helpers.js b/app/federation/server/functions/helpers.js deleted file mode 100644 index 4113b6014edd..000000000000 --- a/app/federation/server/functions/helpers.js +++ /dev/null @@ -1,69 +0,0 @@ -import { Settings, Subscriptions, Users } from '../../../models/server'; -import { STATUS_ENABLED, STATUS_REGISTERING } from '../constants'; - -export const getNameAndDomain = (fullyQualifiedName) => fullyQualifiedName.split('@'); -export const isFullyQualified = (name) => name.indexOf('@') !== -1; - -export function isRegisteringOrEnabled() { - const status = Settings.findOneById('FEDERATION_Status'); - return [STATUS_ENABLED, STATUS_REGISTERING].includes(status && status.value); -} - -export function updateStatus(status) { - Settings.updateValueById('FEDERATION_Status', status); -} - -export function updateEnabled(enabled) { - Settings.updateValueById('FEDERATION_Enabled', enabled); -} - -export const checkRoomType = (room) => room.t === 'p' || room.t === 'd'; -export const checkRoomDomainsLength = (domains) => domains.length <= (process.env.FEDERATED_DOMAINS_LENGTH || 10); - -export const hasExternalDomain = ({ federation }) => { - // same test as isFederated(room) - if (!federation) { - return false; - } - - return federation.domains - .some((domain) => domain !== federation.origin); -}; - -export const isLocalUser = ({ federation }, localDomain) => - !federation || federation.origin === localDomain; - -export const getFederatedRoomData = (room) => { - let hasFederatedUser = false; - - let users = null; - let subscriptions = null; - - if (room.t === 'd') { - // Check if there is a federated user on this room - hasFederatedUser = room.usernames.some(isFullyQualified); - } else { - // Find all subscriptions of this room - subscriptions = Subscriptions.findByRoomIdWhenUsernameExists(room._id).fetch(); - subscriptions = subscriptions.reduce((acc, s) => { - acc[s.u._id] = s; - - return acc; - }, {}); - - // Get all user ids - const userIds = Object.keys(subscriptions); - - // Load all the users - users = Users.findUsersWithUsernameByIds(userIds).fetch(); - - // Check if there is a federated user on this room - hasFederatedUser = users.some((u) => isFullyQualified(u.username)); - } - - return { - hasFederatedUser, - users, - subscriptions, - }; -}; diff --git a/app/federation/server/functions/helpers.ts b/app/federation/server/functions/helpers.ts new file mode 100644 index 000000000000..e8cb5b3e5170 --- /dev/null +++ b/app/federation/server/functions/helpers.ts @@ -0,0 +1,77 @@ +import { IRoom, isDirectMessageRoom } from '../../../../definition/IRoom'; +import { ISubscription } from '../../../../definition/ISubscription'; +import { IRegisterUser, IUser } from '../../../../definition/IUser'; +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('@'); + +export const isFullyQualified = (name: string): boolean => name.indexOf('@') !== -1; + +export async function isRegisteringOrEnabled(): Promise { + const value = await Settings.getValueById('FEDERATION_Status'); + return typeof value === 'string' && [STATUS_ENABLED, STATUS_REGISTERING].includes(value); +} + +export async function updateStatus(status: string): Promise { + await Settings.updateValueById('FEDERATION_Status', status); +} + +export async function updateEnabled(enabled: boolean): Promise { + await Settings.updateValueById('FEDERATION_Enabled', enabled); +} + +export const checkRoomType = (room: IRoom): boolean => room.t === 'p' || room.t === 'd'; +export const checkRoomDomainsLength = (domains: unknown[]): boolean => domains.length <= (process.env.FEDERATED_DOMAINS_LENGTH || 10); + +export const hasExternalDomain = ({ federation }: { federation: { origin: string; domains: string[] } }): boolean => { + // same test as isFederated(room) + if (!federation) { + return false; + } + + return federation.domains + .some((domain) => domain !== federation.origin); +}; + +export const isLocalUser = ({ federation }: { federation: { origin: string } }, localDomain: string): boolean => + !federation || federation.origin === localDomain; + +export const getFederatedRoomData = (room: IRoom): { + hasFederatedUser: boolean; + users: IUser[]; + subscriptions: { [k: string]: ISubscription } | undefined; +} => { + if (isDirectMessageRoom(room)) { + // Check if there is a federated user on this room + + return { + users: [], + hasFederatedUser: room.usernames.some(isFullyQualified), + subscriptions: undefined, + }; + } + + // Find all subscriptions of this room + const s = Subscriptions.findByRoomIdWhenUsernameExists(room._id).fetch() as ISubscription[]; + const subscriptions = s.reduce((acc, s) => { + acc[s.u._id] = s; + return acc; + }, {} as { [k: string]: ISubscription }); + + // Get all user ids + const userIds = Object.keys(subscriptions); + + // Load all the users + const users: IRegisterUser[] = Users.findUsersWithUsernameByIds(userIds).fetch(); + + // Check if there is a federated user on this room + const hasFederatedUser = users.some((u) => isFullyQualified(u.username)); + + return { + hasFederatedUser, + users, + subscriptions, + }; +}; diff --git a/app/federation/server/handler/index.js b/app/federation/server/handler/index.js index 7827ddf063a7..46aec0b7dcea 100644 --- a/app/federation/server/handler/index.js +++ b/app/federation/server/handler/index.js @@ -5,7 +5,7 @@ import { clientLogger } from '../lib/logger'; import { isFederationEnabled } from '../lib/isFederationEnabled'; import { federationRequestToPeer } from '../lib/http'; -export function federationSearchUsers(query) { +export async function federationSearchUsers(query) { if (!isFederationEnabled()) { throw disabled('client.searchUsers'); } @@ -16,12 +16,12 @@ export function federationSearchUsers(query) { const uri = `/api/v1/federation.users.search?${ qs.stringify({ username, domain: peerDomain }) }`; - const { data: { users } } = federationRequestToPeer('GET', peerDomain, uri); + const { data: { users } } = await federationRequestToPeer('GET', peerDomain, uri); return users; } -export function getUserByUsername(query) { +export async function getUserByUsername(query) { if (!isFederationEnabled()) { throw disabled('client.searchUsers'); } @@ -32,12 +32,12 @@ export function getUserByUsername(query) { const uri = `/api/v1/federation.users.getByUsername?${ qs.stringify({ username }) }`; - const { data: { user } } = federationRequestToPeer('GET', peerDomain, uri); + const { data: { user } } = await federationRequestToPeer('GET', peerDomain, uri); return user; } -export function requestEventsFromLatest(domain, fromDomain, contextType, contextQuery, latestEventIds) { +export async function requestEventsFromLatest(domain, fromDomain, contextType, contextQuery, latestEventIds) { if (!isFederationEnabled()) { throw disabled('client.requestEventsFromLatest'); } @@ -46,11 +46,11 @@ export function requestEventsFromLatest(domain, fromDomain, contextType, context const uri = '/api/v1/federation.events.requestFromLatest'; - federationRequestToPeer('POST', domain, uri, { fromDomain, contextType, contextQuery, latestEventIds }); + await federationRequestToPeer('POST', domain, uri, { fromDomain, contextType, contextQuery, latestEventIds }); } -export function dispatchEvents(domains, events) { +export async function dispatchEvents(domains, events) { if (!isFederationEnabled()) { throw disabled('client.dispatchEvents'); } @@ -61,17 +61,17 @@ export function dispatchEvents(domains, events) { const uri = '/api/v1/federation.events.dispatch'; - for (const domain of domains) { - federationRequestToPeer('POST', domain, uri, { events }, { ignoreErrors: true }); + for await (const domain of domains) { + await federationRequestToPeer('POST', domain, uri, { events }, { ignoreErrors: true }); } } -export function dispatchEvent(domains, event) { - dispatchEvents([...new Set(domains)], [event]); +export async function dispatchEvent(domains, event) { + await dispatchEvents([...new Set(domains)], [event]); } -export function getUpload(domain, fileId) { - const { data: { upload, buffer } } = federationRequestToPeer('GET', domain, `/api/v1/federation.uploads?${ qs.stringify({ upload_id: fileId }) }`); +export async function getUpload(domain, fileId) { + const { data: { upload, buffer } } = await federationRequestToPeer('GET', domain, `/api/v1/federation.uploads?${ qs.stringify({ upload_id: fileId }) }`); return { upload, buffer: Buffer.from(buffer) }; } diff --git a/app/federation/server/hooks/afterCreateDirectRoom.js b/app/federation/server/hooks/afterCreateDirectRoom.js index ac05794e1c2e..79e6fc992836 100644 --- a/app/federation/server/hooks/afterCreateDirectRoom.js +++ b/app/federation/server/hooks/afterCreateDirectRoom.js @@ -41,7 +41,7 @@ async function afterCreateDirectRoom(room, extras) { })); // Dispatch the events - dispatchEvents(normalizedRoom.federation.domains, [genesisEvent, ...events]); + await dispatchEvents(normalizedRoom.federation.domains, [genesisEvent, ...events]); } catch (err) { await deleteRoom(room._id); diff --git a/app/federation/server/hooks/afterCreateRoom.js b/app/federation/server/hooks/afterCreateRoom.js index 75dfeeac6575..905e108740cf 100644 --- a/app/federation/server/hooks/afterCreateRoom.js +++ b/app/federation/server/hooks/afterCreateRoom.js @@ -47,7 +47,7 @@ export async function doAfterCreateRoom(room, users, subscriptions) { const genesisEvent = await FederationRoomEvents.createGenesisEvent(getFederationDomain(), normalizedRoom); // Dispatch the events - dispatchEvents(normalizedRoom.federation.domains, [genesisEvent, ...addUserEvents]); + await dispatchEvents(normalizedRoom.federation.domains, [genesisEvent, ...addUserEvents]); } async function afterCreateRoom(roomOwner, room) { diff --git a/app/federation/server/lib/crypt.js b/app/federation/server/lib/crypt.js index 7a231a13fb91..5a7685a2e9e0 100644 --- a/app/federation/server/lib/crypt.js +++ b/app/federation/server/lib/crypt.js @@ -1,19 +1,19 @@ -import { FederationKeys } from '../../../models/server'; +import { FederationKeys } from '../../../models/server/raw'; import { getFederationDomain } from './getFederationDomain'; import { search } from './dns'; import { cryptLogger } from './logger'; -export function decrypt(data, peerKey) { +export async function decrypt(data, peerKey) { // // Decrypt the payload const payloadBuffer = Buffer.from(data); // Decrypt with the peer's public key try { - data = FederationKeys.loadKey(peerKey, 'public').decryptPublic(payloadBuffer); + data = (await FederationKeys.loadKey(peerKey, 'public')).decryptPublic(payloadBuffer); // Decrypt with the local private key - data = FederationKeys.getPrivateKey().decrypt(data); + data = (await FederationKeys.getPrivateKey()).decrypt(data); } catch (err) { cryptLogger.error(err); @@ -23,7 +23,7 @@ export function decrypt(data, peerKey) { return JSON.parse(data.toString()); } -export function decryptIfNeeded(request, bodyParams) { +export async function decryptIfNeeded(request, bodyParams) { // // Look for the domain that sent this event const remotePeerDomain = request.headers['x-federation-domain']; @@ -48,17 +48,17 @@ export function decryptIfNeeded(request, bodyParams) { return decrypt(bodyParams, peerKey); } -export function encrypt(data, peerKey) { +export async function encrypt(data, peerKey) { if (!data) { return data; } try { // Encrypt with the peer's public key - data = FederationKeys.loadKey(peerKey, 'public').encrypt(data); + data = (await FederationKeys.loadKey(peerKey, 'public')).encrypt(data); // Encrypt with the local private key - return FederationKeys.getPrivateKey().encryptPrivate(data); + return (await FederationKeys.getPrivateKey()).encryptPrivate(data); } catch (err) { cryptLogger.error(err); diff --git a/app/federation/server/lib/dns.js b/app/federation/server/lib/dns.js index 0c4e2f348e1b..0080ddae625b 100644 --- a/app/federation/server/lib/dns.js +++ b/app/federation/server/lib/dns.js @@ -17,12 +17,12 @@ const memoizedDnsResolveTXT = mem(dnsResolveTXT, { maxAge: cacheMaxAge }); const hubUrl = process.env.NODE_ENV === 'development' ? 'http://localhost:8080' : 'https://hub.rocket.chat'; -export function registerWithHub(peerDomain, url, publicKey) { +export async function registerWithHub(peerDomain, url, publicKey) { const body = { domain: peerDomain, url, public_key: publicKey }; try { // If there is no DNS entry for that, get from the Hub - federationRequest('POST', `${ hubUrl }/api/v1/peers`, body); + await federationRequest('POST', `${ hubUrl }/api/v1/peers`, body); return true; } catch (err) { @@ -32,12 +32,12 @@ export function registerWithHub(peerDomain, url, publicKey) { } } -export function searchHub(peerDomain) { +export async function searchHub(peerDomain) { try { dnsLogger.debug(`searchHub: peerDomain=${ peerDomain }`); // If there is no DNS entry for that, get from the Hub - const { data: { peer } } = federationRequest('GET', `${ hubUrl }/api/v1/peers?search=${ peerDomain }`); + const { data: { peer } } = await federationRequest('GET', `${ hubUrl }/api/v1/peers?search=${ peerDomain }`); if (!peer) { dnsLogger.debug(`searchHub: could not find peerDomain=${ peerDomain }`); diff --git a/app/federation/server/lib/http.js b/app/federation/server/lib/http.js index 542a2d32ef9e..e18d09b8e86d 100644 --- a/app/federation/server/lib/http.js +++ b/app/federation/server/lib/http.js @@ -6,14 +6,14 @@ import { getFederationDomain } from './getFederationDomain'; import { search } from './dns'; import { encrypt } from './crypt'; -export function federationRequest(method, url, body, headers, peerKey = null) { +export async function federationRequest(method, url, body, headers, peerKey = null) { let data = null; if ((method === 'POST' || method === 'PUT') && body) { data = EJSON.toJSONValue(body); if (peerKey) { - data = encrypt(data, peerKey); + data = await encrypt(data, peerKey); } } @@ -22,7 +22,7 @@ export function federationRequest(method, url, body, headers, peerKey = null) { return MeteorHTTP.call(method, url, { data, timeout: 2000, headers: { ...headers, 'x-federation-domain': getFederationDomain() } }); } -export function federationRequestToPeer(method, peerDomain, uri, body, options = {}) { +export async function federationRequestToPeer(method, peerDomain, uri, body, options = {}) { const ignoreErrors = peerDomain === getFederationDomain() ? false : options.ignoreErrors; const { url: baseUrl, publicKey } = search(peerDomain); @@ -39,7 +39,7 @@ export function federationRequestToPeer(method, peerDomain, uri, body, options = try { httpLogger.debug({ msg: 'federationRequestToPeer', url: `${ baseUrl }${ uri }` }); - result = federationRequest(method, `${ baseUrl }${ uri }`, body, options.headers || {}, peerKey); + result = await federationRequest(method, `${ baseUrl }${ uri }`, body, options.headers || {}, peerKey); } catch (err) { httpLogger.error({ msg: `${ ignoreErrors ? '[IGNORED] ' : '' }Error`, err }); diff --git a/app/federation/server/startup/generateKeys.js b/app/federation/server/startup/generateKeys.js index 012cdd0b48f4..32eaacc30418 100644 --- a/app/federation/server/startup/generateKeys.js +++ b/app/federation/server/startup/generateKeys.js @@ -1,6 +1,8 @@ -import { FederationKeys } from '../../../models/server'; +import { FederationKeys } from '../../../models/server/raw'; // Create key pair if needed -if (!FederationKeys.getPublicKey()) { - FederationKeys.generateKeys(); -} +(async () => { + if (!await FederationKeys.getPublicKey()) { + await FederationKeys.generateKeys(); + } +})(); diff --git a/app/federation/server/startup/settings.ts b/app/federation/server/startup/settings.ts index cfa7fda19e6a..36ade9e70eeb 100644 --- a/app/federation/server/startup/settings.ts +++ b/app/federation/server/startup/settings.ts @@ -7,11 +7,11 @@ 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'; +import { FederationKeys } from '../../../models/server/raw'; import { STATUS_ENABLED, STATUS_REGISTERING, STATUS_ERROR_REGISTERING, STATUS_DISABLED } from '../constants'; -Meteor.startup(function() { - const federationPublicKey = FederationKeys.getPublicKeyString(); +Meteor.startup(async function() { + const federationPublicKey = await FederationKeys.getPublicKeyString(); settingsRegistry.addGroup('Federation', function() { this.add('FEDERATION_Enabled', false, { @@ -36,7 +36,7 @@ Meteor.startup(function() { // disableReset: true, }); - this.add('FEDERATION_Public_Key', federationPublicKey, { + this.add('FEDERATION_Public_Key', federationPublicKey || '', { readonly: true, type: 'string', multiline: true, @@ -65,26 +65,26 @@ Meteor.startup(function() { }); }); -const updateSettings = function(): void { +const updateSettings = async function(): Promise { // Get the key pair - if (getFederationDiscoveryMethod() === 'hub' && !isRegisteringOrEnabled()) { + if (getFederationDiscoveryMethod() === 'hub' && !Promise.await(isRegisteringOrEnabled())) { // Register with hub try { - updateStatus(STATUS_REGISTERING); + await updateStatus(STATUS_REGISTERING); - registerWithHub(getFederationDomain(), settings.get('Site_Url'), FederationKeys.getPublicKeyString()); + await registerWithHub(getFederationDomain(), settings.get('Site_Url'), await FederationKeys.getPublicKeyString()); - updateStatus(STATUS_ENABLED); + await updateStatus(STATUS_ENABLED); } catch (err) { // Disable federation - updateEnabled(false); + await updateEnabled(false); - updateStatus(STATUS_ERROR_REGISTERING); + await updateStatus(STATUS_ERROR_REGISTERING); } - } else { - updateStatus(STATUS_ENABLED); + return; } + await updateStatus(STATUS_ENABLED); }; // Add settings listeners @@ -92,11 +92,11 @@ settings.watch('FEDERATION_Enabled', function enableOrDisable(value) { setupLogger.info(`Federation is ${ value ? 'enabled' : 'disabled' }`); if (value) { - updateSettings(); + Promise.await(updateSettings()); enableCallbacks(); } else { - updateStatus(STATUS_DISABLED); + Promise.await(updateStatus(STATUS_DISABLED)); disableCallbacks(); } diff --git a/app/file-upload/server/lib/FileUpload.js b/app/file-upload/server/lib/FileUpload.js index aa8d49ab408e..70ecac27f203 100644 --- a/app/file-upload/server/lib/FileUpload.js +++ b/app/file-upload/server/lib/FileUpload.js @@ -2,6 +2,7 @@ import fs from 'fs'; import stream from 'stream'; import { Meteor } from 'meteor/meteor'; +import { Mongo } from 'meteor/mongo'; import streamBuffers from 'stream-buffers'; import Future from 'fibers/future'; import sharp from 'sharp'; @@ -13,9 +14,7 @@ import filesize from 'filesize'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import { settings } from '../../../settings/server'; -import Uploads from '../../../models/server/models/Uploads'; -import UserDataFiles from '../../../models/server/models/UserDataFiles'; -import Avatars from '../../../models/server/models/Avatars'; +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'; @@ -41,6 +40,9 @@ settings.watch('FileUpload_MaxFileSize', function(value) { } }); +const AvatarModel = new Mongo.Collection(Avatars.col.collectionName); +const UserDataFilesModel = new Mongo.Collection(UserDataFiles.col.collectionName); +const UploadsModel = new Mongo.Collection(Uploads.col.collectionName); export const FileUpload = { handlers: {}, @@ -139,7 +141,7 @@ export const FileUpload = { defaultUploads() { return { - collection: Uploads.model, + collection: UploadsModel, filter: new UploadFS.Filter({ onCheck: FileUpload.validateFileUpload, }), @@ -161,7 +163,7 @@ export const FileUpload = { defaultAvatars() { return { - collection: Avatars.model, + collection: AvatarModel, filter: new UploadFS.Filter({ onCheck: FileUpload.validateAvatarUpload, }), @@ -176,7 +178,7 @@ export const FileUpload = { defaultUserDataFiles() { return { - collection: UserDataFiles.model, + collection: UserDataFilesModel, getPath(file) { return `${ settings.get('uniqueID') }/uploads/userData/${ file.userId }`; }, @@ -254,7 +256,7 @@ export const FileUpload = { }, resizeImagePreview(file) { - file = Uploads.findOneById(file._id); + file = Promise.await(Uploads.findOneById(file._id)); file = FileUpload.addExtensionTo(file); const image = FileUpload.getStore('Uploads')._store.getReadStream(file._id, file); @@ -279,7 +281,7 @@ export const FileUpload = { return; } - file = Uploads.findOneById(file._id); + file = Promise.await(Uploads.findOneById(file._id)); file = FileUpload.addExtensionTo(file); const store = FileUpload.getStore('Uploads'); const image = store._store.getReadStream(file._id, file); @@ -378,11 +380,11 @@ export const FileUpload = { } // update file record to match user's username const user = Users.findOneById(file.userId); - const oldAvatar = Avatars.findOneByName(user.username); + const oldAvatar = Promise.await(Avatars.findOneByName(user.username)); if (oldAvatar) { - Avatars.deleteFile(oldAvatar._id); + Promise.await(Avatars.deleteFile(oldAvatar._id)); } - Avatars.updateFileNameById(file._id, user.username); + Promise.await(Avatars.updateFileNameById(file._id, user.username)); // console.log('upload finished ->', file); }, @@ -567,15 +569,16 @@ export class FileUploadClass { } delete(fileId) { + // TODO: Remove this method if (this.store && this.store.delete) { this.store.delete(fileId); } - return this.model.deleteFile(fileId); + return Promise.await(this.model.deleteFile(fileId)); } deleteById(fileId) { - const file = this.model.findOneById(fileId); + const file = Promise.await(this.model.findOneById(fileId)); if (!file) { return; @@ -587,7 +590,7 @@ export class FileUploadClass { } deleteByName(fileName) { - const file = this.model.findOneByName(fileName); + const file = Promise.await(this.model.findOneByName(fileName)); if (!file) { return; @@ -600,7 +603,7 @@ export class FileUploadClass { deleteByRoomId(rid) { - const file = this.model.findOneByRoomId(rid); + const file = Promise.await(this.model.findOneByRoomId(rid)); if (!file) { return; diff --git a/app/file-upload/server/lib/requests.js b/app/file-upload/server/lib/requests.js index 80a3b4213b38..3b2e8dad19d7 100644 --- a/app/file-upload/server/lib/requests.js +++ b/app/file-upload/server/lib/requests.js @@ -1,13 +1,13 @@ import { WebApp } from 'meteor/webapp'; import { FileUpload } from './FileUpload'; -import { Uploads } from '../../../models'; +import { Uploads } from '../../../models/server/raw'; -WebApp.connectHandlers.use(FileUpload.getPath(), function(req, res, next) { +WebApp.connectHandlers.use(FileUpload.getPath(), async function(req, res, next) { const match = /^\/([^\/]+)\/(.*)/.exec(req.url); if (match && match[1]) { - const file = Uploads.findOneById(match[1]); + const file = await Uploads.findOneById(match[1]); if (file) { if (!FileUpload.requestCanAccessFiles(req)) { diff --git a/app/file-upload/server/methods/getS3FileUrl.js b/app/file-upload/server/methods/getS3FileUrl.js index f68f720d171b..cfffdfcc032a 100644 --- a/app/file-upload/server/methods/getS3FileUrl.js +++ b/app/file-upload/server/methods/getS3FileUrl.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { UploadFS } from 'meteor/jalik:ufs'; import { settings } from '../../../settings/server'; -import { Uploads } from '../../../models'; +import { Uploads } from '../../../models/server/raw'; let protectedFiles; @@ -11,11 +11,11 @@ settings.watch('FileUpload_ProtectFiles', function(value) { }); Meteor.methods({ - getS3FileUrl(fileId) { + async getS3FileUrl(fileId) { if (protectedFiles && !Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendFileMessage' }); } - const file = Uploads.findOneById(fileId); + const file = await Uploads.findOneById(fileId); return UploadFS.getStore('AmazonS3:Uploads').getRedirectURL(file); }, diff --git a/app/file-upload/server/methods/sendFileMessage.ts b/app/file-upload/server/methods/sendFileMessage.ts index 80dec7bca683..886f9167e07e 100644 --- a/app/file-upload/server/methods/sendFileMessage.ts +++ b/app/file-upload/server/methods/sendFileMessage.ts @@ -3,8 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import _ from 'underscore'; -import { Uploads } from '../../../models/server'; -import { Rooms } from '../../../models/server/raw'; +import { Rooms, Uploads } from '../../../models/server/raw'; import { callbacks } from '../../../callbacks/server'; import { FileUpload } from '../lib/FileUpload'; import { canAccessRoom } from '../../../authorization/server/functions/canAccessRoom'; @@ -35,7 +34,7 @@ Meteor.methods({ tmid: Match.Optional(String), }); - Uploads.updateFileComplete(file._id, user._id, _.omit(file, '_id')); + await Uploads.updateFileComplete(file._id, user._id, _.omit(file, '_id')); const fileUrl = FileUpload.getPath(`${ file._id }/${ encodeURI(file.name) }`); diff --git a/app/highlight-words/tests/helper.tests.js b/app/highlight-words/tests/helper.tests.js index 28c5fd075164..2b4e895d0e65 100644 --- a/app/highlight-words/tests/helper.tests.js +++ b/app/highlight-words/tests/helper.tests.js @@ -1,7 +1,4 @@ -/* eslint-env mocha */ - -import 'babel-polyfill'; -import assert from 'assert'; +import { expect } from 'chai'; import { highlightWords, getRegexHighlight, getRegexHighlightUrl } from '../client/helper'; @@ -14,7 +11,7 @@ describe('helper', () => { urlRegex: getRegexHighlightUrl(highlight), }))); - assert.equal(res, 'here is some word'); + expect(res).to.be.equal('here is some word'); }); describe('handles links', () => { @@ -25,7 +22,7 @@ describe('helper', () => { urlRegex: getRegexHighlightUrl(highlight), }))); - assert.equal(res, 'here we go https://somedomain.com/here-some.word/pulls more words after'); + expect(res).to.be.equal('here we go https://somedomain.com/here-some.word/pulls more words after'); }); it('not highlighting two links', () => { @@ -36,7 +33,7 @@ describe('helper', () => { urlRegex: getRegexHighlightUrl(highlight), }))); - assert.equal(res, msg); + expect(res).to.be.equal(msg); }); it('not highlighting link but keep words on message highlighted', () => { @@ -46,7 +43,7 @@ describe('helper', () => { urlRegex: getRegexHighlightUrl(highlight), }))); - assert.equal(res, 'here we go https://somedomain.com/here-some.foo/pulls more foo after'); + expect(res).to.be.equal('here we go https://somedomain.com/here-some.foo/pulls more foo after'); }); }); }); diff --git a/app/integrations/server/api/api.js b/app/integrations/server/api/api.js index 17a04b6c3582..eb223c67c9ad 100644 --- a/app/integrations/server/api/api.js +++ b/app/integrations/server/api/api.js @@ -14,6 +14,7 @@ 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 = {}; @@ -129,9 +130,10 @@ function removeIntegration(options, user) { incomingLogger.info('Remove integration'); incomingLogger.debug(options); - const integrationToRemove = Models.Integrations.findOne({ - urls: options.target_url, - }); + const integrationToRemove = Promise.await(Integrations.findOneByUrl(options.target_url)); + if (!integrationToRemove) { + return API.v1.failure('integration-not-found'); + } Meteor.runAsUser(user._id, () => Meteor.call('deleteOutgoingIntegration', integrationToRemove._id)); @@ -373,10 +375,10 @@ const Api = new WebHookAPI({ } } - this.integration = Models.Integrations.findOne({ + this.integration = Promise.await(Integrations.findOne({ _id: this.request.params.integrationId, token: decodeURIComponent(this.request.params.token), - }); + })); if (!this.integration) { incomingLogger.info(`Invalid integration id ${ this.request.params.integrationId } or token ${ this.request.params.token }`); diff --git a/app/integrations/server/lib/triggerHandler.js b/app/integrations/server/lib/triggerHandler.js index 33cc33ddb24d..27f71a4e5729 100644 --- a/app/integrations/server/lib/triggerHandler.js +++ b/app/integrations/server/lib/triggerHandler.js @@ -10,6 +10,7 @@ import Fiber from 'fibers'; import Future from 'fibers/future'; 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'; @@ -22,7 +23,7 @@ export class RocketChatIntegrationHandler { this.compiledScripts = {}; this.triggers = {}; - Models.Integrations.find({ type: 'webhook-outgoing' }).fetch().forEach((data) => this.addIntegration(data)); + Promise.await(Integrations.find({ type: 'webhook-outgoing' }).forEach((data) => this.addIntegration(data))); } addIntegration(record) { @@ -142,11 +143,11 @@ export class RocketChatIntegrationHandler { } if (historyId) { - Models.IntegrationHistory.update({ _id: historyId }, { $set: history }); + Promise.await(IntegrationHistory.updateOne({ _id: historyId }, { $set: history })); return historyId; } history._createdAt = new Date(); - return Models.IntegrationHistory.insert(Object.assign({ _id: Random.id() }, history)); + return Promise.await(IntegrationHistory.insertOne({ _id: Random.id(), ...history })); } // Trigger is the trigger, nameOrId is a string which is used to try and find a room, room is a room, message is a message, and data contains "user_name" if trigger.impersonateUser is truthful. @@ -715,7 +716,7 @@ export class RocketChatIntegrationHandler { if (result.statusCode === 410) { this.updateHistory({ historyId, step: 'after-process-http-status-410', error: true }); outgoingLogger.error(`Disabling the Integration "${ trigger.name }" because the status code was 401 (Gone).`); - Models.Integrations.update({ _id: trigger._id }, { $set: { enabled: false } }); + Promise.await(Integrations.updateOne({ _id: trigger._id }, { $set: { enabled: false } })); return; } diff --git a/app/integrations/server/methods/clearIntegrationHistory.js b/app/integrations/server/methods/clearIntegrationHistory.ts similarity index 70% rename from app/integrations/server/methods/clearIntegrationHistory.js rename to app/integrations/server/methods/clearIntegrationHistory.ts index 87eec581e37a..f4ef3e974b96 100644 --- a/app/integrations/server/methods/clearIntegrationHistory.js +++ b/app/integrations/server/methods/clearIntegrationHistory.ts @@ -1,17 +1,20 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../authorization'; -import { IntegrationHistory, Integrations } from '../../../models'; +import { hasPermission } from '../../../authorization/server'; +import { IntegrationHistory, Integrations } from '../../../models/server/raw'; import notifications from '../../../notifications/server/lib/Notifications'; Meteor.methods({ - clearIntegrationHistory(integrationId) { + async clearIntegrationHistory(integrationId) { let integration; if (hasPermission(this.userId, 'manage-outgoing-integrations') || hasPermission(this.userId, 'manage-outgoing-integrations', 'bot')) { - integration = Integrations.findOne(integrationId); + integration = await Integrations.findOneById(integrationId); } else if (hasPermission(this.userId, 'manage-own-outgoing-integrations') || hasPermission(this.userId, 'manage-own-outgoing-integrations', 'bot')) { - integration = Integrations.findOne(integrationId, { fields: { '_createdBy._id': this.userId } }); + integration = await Integrations.findOne({ + _id: integrationId, + '_createdBy._id': this.userId, + }); } else { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'clearIntegrationHistory' }); } @@ -20,7 +23,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'clearIntegrationHistory' }); } - IntegrationHistory.removeByIntegrationId(integrationId); + await IntegrationHistory.removeByIntegrationId(integrationId); notifications.streamIntegrationHistory.emit(integrationId, { type: 'removed' }); diff --git a/app/integrations/server/methods/incoming/addIncomingIntegration.js b/app/integrations/server/methods/incoming/addIncomingIntegration.js index 6e86dacd5700..23b339ed48fb 100644 --- a/app/integrations/server/methods/incoming/addIncomingIntegration.js +++ b/app/integrations/server/methods/incoming/addIncomingIntegration.js @@ -4,13 +4,14 @@ import { Babel } from 'meteor/babel-compiler'; import _ from 'underscore'; import s from 'underscore.string'; -import { hasPermission, hasAllPermission } from '../../../../authorization'; -import { Users, Rooms, Integrations, Roles, Subscriptions } from '../../../../models'; +import { hasPermission, hasAllPermission } from '../../../../authorization/server'; +import { Users, Rooms, Subscriptions } from '../../../../models/server'; +import { Integrations, Roles } from '../../../../models/server/raw'; const validChannelChars = ['@', '#']; Meteor.methods({ - addIncomingIntegration(integration) { + async addIncomingIntegration(integration) { if (!hasPermission(this.userId, 'manage-incoming-integrations') && !hasPermission(this.userId, 'manage-own-incoming-integrations')) { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'addIncomingIntegration' }); } @@ -95,9 +96,11 @@ Meteor.methods({ integration._createdAt = new Date(); integration._createdBy = Users.findOne(this.userId, { fields: { username: 1 } }); - Roles.addUserRoles(user._id, 'bot'); + await Roles.addUserRoles(user._id, 'bot'); - integration._id = Integrations.insert(integration); + const result = await Integrations.insertOne(integration); + + integration._id = result.insertedId; return integration; }, diff --git a/app/integrations/server/methods/incoming/deleteIncomingIntegration.js b/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts similarity index 56% rename from app/integrations/server/methods/incoming/deleteIncomingIntegration.js rename to app/integrations/server/methods/incoming/deleteIncomingIntegration.ts index 96c25116a10d..bbd158f20ae0 100644 --- a/app/integrations/server/methods/incoming/deleteIncomingIntegration.js +++ b/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts @@ -1,16 +1,19 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../../authorization'; -import { Integrations } from '../../../../models'; +import { hasPermission } from '../../../../authorization/server'; +import { Integrations } from '../../../../models/server/raw'; Meteor.methods({ - deleteIncomingIntegration(integrationId) { + async deleteIncomingIntegration(integrationId) { let integration; if (hasPermission(this.userId, 'manage-incoming-integrations')) { - integration = Integrations.findOne(integrationId); + integration = Integrations.findOneById(integrationId); } else if (hasPermission(this.userId, 'manage-own-incoming-integrations')) { - integration = Integrations.findOne(integrationId, { fields: { '_createdBy._id': this.userId } }); + integration = Integrations.findOne({ + _id: integrationId, + '_createdBy._id': this.userId, + }); } else { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'deleteIncomingIntegration' }); } @@ -19,7 +22,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'deleteIncomingIntegration' }); } - Integrations.remove({ _id: integrationId }); + await Integrations.removeById(integrationId); return true; }, diff --git a/app/integrations/server/methods/incoming/updateIncomingIntegration.js b/app/integrations/server/methods/incoming/updateIncomingIntegration.js index 5e7b3517ba0e..fc5a6d384b95 100644 --- a/app/integrations/server/methods/incoming/updateIncomingIntegration.js +++ b/app/integrations/server/methods/incoming/updateIncomingIntegration.js @@ -3,13 +3,14 @@ import { Babel } from 'meteor/babel-compiler'; import _ from 'underscore'; import s from 'underscore.string'; -import { Integrations, Rooms, Users, Roles, Subscriptions } from '../../../../models'; -import { hasAllPermission, hasPermission } from '../../../../authorization'; +import { Rooms, Users, Subscriptions } from '../../../../models/server'; +import { Integrations, Roles } from '../../../../models/server/raw'; +import { hasAllPermission, hasPermission } from '../../../../authorization/server'; const validChannelChars = ['@', '#']; Meteor.methods({ - updateIncomingIntegration(integrationId, integration) { + async updateIncomingIntegration(integrationId, integration) { if (!_.isString(integration.channel) || integration.channel.trim() === '') { throw new Meteor.Error('error-invalid-channel', 'Invalid channel', { method: 'updateIncomingIntegration' }); } @@ -25,9 +26,9 @@ Meteor.methods({ let currentIntegration; if (hasPermission(this.userId, 'manage-incoming-integrations')) { - currentIntegration = Integrations.findOne(integrationId); + currentIntegration = await Integrations.findOneById(integrationId); } else if (hasPermission(this.userId, 'manage-own-incoming-integrations')) { - currentIntegration = Integrations.findOne({ _id: integrationId, '_createdBy._id': this.userId }); + currentIntegration = await Integrations.findOne({ _id: integrationId, '_createdBy._id': this.userId }); } else { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'updateIncomingIntegration' }); } @@ -43,14 +44,14 @@ Meteor.methods({ integration.scriptCompiled = Babel.compile(integration.script, babelOptions).code; integration.scriptError = undefined; - Integrations.update(integrationId, { + await Integrations.updateOne({ _id: integrationId }, { $set: { scriptCompiled: integration.scriptCompiled }, $unset: { scriptError: 1 }, }); } catch (e) { integration.scriptCompiled = undefined; integration.scriptError = _.pick(e, 'name', 'message', 'stack'); - Integrations.update(integrationId, { + await Integrations.updateOne({ _id: integrationId }, { $set: { scriptError: integration.scriptError, }, @@ -100,9 +101,9 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-post-as-user', 'Invalid Post As User', { method: 'updateIncomingIntegration' }); } - Roles.addUserRoles(user._id, 'bot'); + await Roles.addUserRoles(user._id, 'bot'); - Integrations.update(integrationId, { + await Integrations.updateOne({ _id: integrationId }, { $set: { enabled: integration.enabled, name: integration.name, @@ -117,6 +118,6 @@ Meteor.methods({ }, }); - return Integrations.findOne(integrationId); + return Integrations.findOneById(integrationId); }, }); diff --git a/app/integrations/server/methods/outgoing/addOutgoingIntegration.js b/app/integrations/server/methods/outgoing/addOutgoingIntegration.js index 5baf6e88cda4..ae6f1aa6933d 100644 --- a/app/integrations/server/methods/outgoing/addOutgoingIntegration.js +++ b/app/integrations/server/methods/outgoing/addOutgoingIntegration.js @@ -1,11 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../../authorization'; -import { Users, Integrations } from '../../../../models'; +import { hasPermission } from '../../../../authorization/server'; +import { Users } from '../../../../models/server'; +import { Integrations } from '../../../../models/server/raw'; import { integrations } from '../../../lib/rocketchat'; Meteor.methods({ - addOutgoingIntegration(integration) { + async addOutgoingIntegration(integration) { if (!hasPermission(this.userId, 'manage-outgoing-integrations') && !hasPermission(this.userId, 'manage-own-outgoing-integrations') && !hasPermission(this.userId, 'manage-outgoing-integrations', 'bot') @@ -17,7 +18,9 @@ Meteor.methods({ integration._createdAt = new Date(); integration._createdBy = Users.findOne(this.userId, { fields: { username: 1 } }); - integration._id = Integrations.insert(integration); + + const result = await Integrations.insertOne(integration); + integration._id = result.insertedId; return integration; }, diff --git a/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.js b/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts similarity index 63% rename from app/integrations/server/methods/outgoing/deleteOutgoingIntegration.js rename to app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts index 07823b22bb2c..a63e845eaa77 100644 --- a/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.js +++ b/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts @@ -1,16 +1,19 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../../authorization'; -import { IntegrationHistory, Integrations } from '../../../../models'; +import { hasPermission } from '../../../../authorization/server'; +import { IntegrationHistory, Integrations } from '../../../../models/server/raw'; Meteor.methods({ - deleteOutgoingIntegration(integrationId) { + async deleteOutgoingIntegration(integrationId) { let integration; if (hasPermission(this.userId, 'manage-outgoing-integrations') || hasPermission(this.userId, 'manage-outgoing-integrations', 'bot')) { - integration = Integrations.findOne(integrationId); + integration = Integrations.findOneById(integrationId); } else if (hasPermission(this.userId, 'manage-own-outgoing-integrations') || hasPermission(this.userId, 'manage-own-outgoing-integrations', 'bot')) { - integration = Integrations.findOne(integrationId, { fields: { '_createdBy._id': this.userId } }); + integration = Integrations.findOne({ + _id: integrationId, + '_createdBy._id': this.userId, + }); } else { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'deleteOutgoingIntegration' }); } @@ -19,8 +22,8 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'deleteOutgoingIntegration' }); } - Integrations.remove({ _id: integrationId }); - IntegrationHistory.removeByIntegrationId(integrationId); + await Integrations.removeById(integrationId); + await IntegrationHistory.removeByIntegrationId(integrationId); return true; }, diff --git a/app/integrations/server/methods/outgoing/replayOutgoingIntegration.js b/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts similarity index 69% rename from app/integrations/server/methods/outgoing/replayOutgoingIntegration.js rename to app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts index 8d88cde3ea28..bf3136525bc9 100644 --- a/app/integrations/server/methods/outgoing/replayOutgoingIntegration.js +++ b/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts @@ -1,17 +1,20 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../../authorization'; -import { Integrations, IntegrationHistory } from '../../../../models'; +import { hasPermission } from '../../../../authorization/server'; +import { Integrations, IntegrationHistory } from '../../../../models/server/raw'; import { triggerHandler } from '../../lib/triggerHandler'; Meteor.methods({ - replayOutgoingIntegration({ integrationId, historyId }) { + async replayOutgoingIntegration({ integrationId, historyId }) { let integration; if (hasPermission(this.userId, 'manage-outgoing-integrations') || hasPermission(this.userId, 'manage-outgoing-integrations', 'bot')) { - integration = Integrations.findOne(integrationId); + integration = await Integrations.findOneById(integrationId); } else if (hasPermission(this.userId, 'manage-own-outgoing-integrations') || hasPermission(this.userId, 'manage-own-outgoing-integrations', 'bot')) { - integration = Integrations.findOne(integrationId, { fields: { '_createdBy._id': this.userId } }); + integration = await Integrations.findOne({ + _id: integrationId, + '_createdBy._id': this.userId, + }); } else { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'replayOutgoingIntegration' }); } @@ -20,7 +23,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'replayOutgoingIntegration' }); } - const history = IntegrationHistory.findOneByIntegrationIdAndHistoryId(integration._id, historyId); + const history = await IntegrationHistory.findOneByIntegrationIdAndHistoryId(integration._id, historyId); if (!history) { throw new Meteor.Error('error-invalid-integration-history', 'Invalid Integration History', { method: 'replayOutgoingIntegration' }); diff --git a/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js b/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js index 981a7890bc29..e9e4bc1ba968 100644 --- a/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js +++ b/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js @@ -1,11 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../../authorization'; -import { Integrations, Users } from '../../../../models'; +import { hasPermission } from '../../../../authorization/server'; +import { Users } from '../../../../models/server'; +import { Integrations } from '../../../../models/server/raw'; import { integrations } from '../../../lib/rocketchat'; Meteor.methods({ - updateOutgoingIntegration(integrationId, integration) { + async updateOutgoingIntegration(integrationId, integration) { integration = integrations.validateOutgoing(integration, this.userId); if (!integration.token || integration.token.trim() === '') { @@ -15,9 +16,9 @@ Meteor.methods({ let currentIntegration; if (hasPermission(this.userId, 'manage-outgoing-integrations')) { - currentIntegration = Integrations.findOne(integrationId); + currentIntegration = await Integrations.findOneById(integrationId); } else if (hasPermission(this.userId, 'manage-own-outgoing-integrations')) { - currentIntegration = Integrations.findOne({ _id: integrationId, '_createdBy._id': this.userId }); + currentIntegration = await Integrations.findOne({ _id: integrationId, '_createdBy._id': this.userId }); } else { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'updateOutgoingIntegration' }); } @@ -26,18 +27,18 @@ Meteor.methods({ throw new Meteor.Error('invalid_integration', '[methods] updateOutgoingIntegration -> integration not found'); } if (integration.scriptCompiled) { - Integrations.update(integrationId, { + await Integrations.updateOne({ _id: integrationId }, { $set: { scriptCompiled: integration.scriptCompiled }, $unset: { scriptError: 1 }, }); } else { - Integrations.update(integrationId, { + await Integrations.updateOne({ _id: integrationId }, { $set: { scriptError: integration.scriptError }, $unset: { scriptCompiled: 1 }, }); } - Integrations.update(integrationId, { + await Integrations.updateOne({ _id: integrationId }, { $set: { event: integration.event, enabled: integration.enabled, @@ -65,6 +66,6 @@ Meteor.methods({ }, }); - return Integrations.findOne(integrationId); + return Integrations.findOneById(integrationId); }, }); diff --git a/app/invites/server/functions/findOrCreateInvite.js b/app/invites/server/functions/findOrCreateInvite.js index c875a53906d0..3b608e7121e0 100644 --- a/app/invites/server/functions/findOrCreateInvite.js +++ b/app/invites/server/functions/findOrCreateInvite.js @@ -3,7 +3,8 @@ import { Random } from 'meteor/random'; import { hasPermission } from '../../../authorization'; import { Notifications } from '../../../notifications'; -import { Invites, Subscriptions, Rooms } from '../../../models/server'; +import { Subscriptions, Rooms } from '../../../models/server'; +import { Invites } from '../../../models/server/raw'; import { settings } from '../../../settings'; import { getURL } from '../../../utils/lib/getURL'; import { roomTypes, RoomMemberActions } from '../../../utils/server'; @@ -23,7 +24,7 @@ function getInviteUrl(invite) { const possibleDays = [0, 1, 7, 15, 30]; const possibleUses = [0, 1, 5, 10, 25, 50, 100]; -export const findOrCreateInvite = (userId, invite) => { +export const findOrCreateInvite = async (userId, invite) => { if (!userId || !invite) { return false; } @@ -57,7 +58,7 @@ export const findOrCreateInvite = (userId, invite) => { } // Before anything, let's check if there's an existing invite with the same settings for the same channel and user and that has not yet expired. - const existing = Invites.findOneByUserRoomMaxUsesAndExpiration(userId, invite.rid, maxUses, days); + const existing = await Invites.findOneByUserRoomMaxUsesAndExpiration(userId, invite.rid, maxUses, days); // If an existing invite was found, return it's _id instead of creating a new one. if (existing) { @@ -86,7 +87,7 @@ export const findOrCreateInvite = (userId, invite) => { uses: 0, }; - Invites.create(createInvite); + await Invites.insertOne(createInvite); Notifications.notifyUser(userId, 'updateInvites', { invite: createInvite }); createInvite.url = getInviteUrl(createInvite); diff --git a/app/invites/server/functions/listInvites.js b/app/invites/server/functions/listInvites.js index 476a5f729e09..10d67435237d 100644 --- a/app/invites/server/functions/listInvites.js +++ b/app/invites/server/functions/listInvites.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../authorization'; -import { Invites } from '../../../models'; +import { hasPermission } from '../../../authorization/server'; +import { Invites } from '../../../models/server/raw'; -export const listInvites = (userId) => { +export const listInvites = async (userId) => { if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'listInvites' }); } @@ -12,5 +12,5 @@ export const listInvites = (userId) => { throw new Meteor.Error('not_authorized'); } - return Invites.find({}).fetch(); + return Invites.find({}).toArray(); }; diff --git a/app/invites/server/functions/removeInvite.js b/app/invites/server/functions/removeInvite.js index eadbe67966b3..0ea066a8e287 100644 --- a/app/invites/server/functions/removeInvite.js +++ b/app/invites/server/functions/removeInvite.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; -import Invites from '../../../models/server/models/Invites'; +import { Invites } from '../../../models/server/raw'; -export const removeInvite = (userId, invite) => { +export const removeInvite = async (userId, invite) => { if (!userId || !invite) { return false; } @@ -17,13 +17,13 @@ export const removeInvite = (userId, invite) => { } // Before anything, let's check if there's an existing invite - const existing = Invites.findOneById(invite._id); + const existing = await Invites.findOneById(invite._id); if (!existing) { throw new Meteor.Error('invalid-invitation-id', 'Invalid Invitation _id', { method: 'removeInvite' }); } - Invites.removeById(invite._id); + await Invites.removeById(invite._id); return true; }; diff --git a/app/invites/server/functions/useInviteToken.js b/app/invites/server/functions/useInviteToken.js index 3cf638fd3e94..6fcef0a40788 100644 --- a/app/invites/server/functions/useInviteToken.js +++ b/app/invites/server/functions/useInviteToken.js @@ -1,11 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { Invites, Users, Subscriptions } from '../../../models/server'; +import { Users, Subscriptions } from '../../../models/server'; +import { Invites } from '../../../models/server/raw'; import { validateInviteToken } from './validateInviteToken'; import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom'; import { roomTypes, RoomMemberActions } from '../../../utils/server'; -export const useInviteToken = (userId, token) => { +export const useInviteToken = async (userId, token) => { if (!userId) { throw new Meteor.Error('error-invalid-user', 'The user is invalid', { method: 'useInviteToken', field: 'userId' }); } @@ -14,7 +15,7 @@ export const useInviteToken = (userId, token) => { throw new Meteor.Error('error-invalid-token', 'The invite token is invalid.', { method: 'useInviteToken', field: 'token' }); } - const { inviteData, room } = validateInviteToken(token); + const { inviteData, room } = await validateInviteToken(token); if (!roomTypes.getConfig(room.t).allowMemberAction(room, RoomMemberActions.INVITE)) { throw new Meteor.Error('error-room-type-not-allowed', 'Can\'t join room of this type via invite', { method: 'useInviteToken', field: 'token' }); @@ -25,7 +26,7 @@ export const useInviteToken = (userId, token) => { const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { fields: { _id: 1 } }); if (!subscription) { - Invites.increaseUsageById(inviteData._id); + await Invites.increaseUsageById(inviteData._id); } // If the user already has an username, then join the invite room, diff --git a/app/invites/server/functions/validateInviteToken.js b/app/invites/server/functions/validateInviteToken.js index dda8add8b612..81febb439442 100644 --- a/app/invites/server/functions/validateInviteToken.js +++ b/app/invites/server/functions/validateInviteToken.js @@ -1,13 +1,14 @@ import { Meteor } from 'meteor/meteor'; -import { Invites, Rooms } from '../../../models'; +import { Rooms } from '../../../models'; +import { Invites } from '../../../models/server/raw'; -export const validateInviteToken = (token) => { +export const validateInviteToken = async (token) => { if (!token || typeof token !== 'string') { throw new Meteor.Error('error-invalid-token', 'The invite token is invalid.', { method: 'validateInviteToken', field: 'token' }); } - const inviteData = Invites.findOneById(token); + const inviteData = await Invites.findOneById(token); if (!inviteData) { throw new Meteor.Error('error-invalid-token', 'The invite token is invalid.', { method: 'validateInviteToken', field: 'token' }); } diff --git a/app/lib/server/functions/addOAuthService.ts b/app/lib/server/functions/addOAuthService.ts index a41bb2ee174e..1a2a220fcddc 100644 --- a/app/lib/server/functions/addOAuthService.ts +++ b/app/lib/server/functions/addOAuthService.ts @@ -57,6 +57,18 @@ export function addOAuthService(name: string, values: { [k: string]: string | bo enterprise: true, invalidValue: false, modules: ['oauth-enterprise'] }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-roles_to_sync` , values.rolesToSync || '', { type: 'string', + group: 'OAuth', + section: `Custom OAuth: ${ name }`, + i18nLabel: 'Accounts_OAuth_Custom_Roles_To_Sync', + i18nDescription: 'Accounts_OAuth_Custom_Roles_To_Sync_Description', + enterprise: true, + enableQuery: { + _id: `Accounts_OAuth_Custom-${ name }-merge_roles`, + value: true, + }, + invalidValue: '', + modules: ['oauth-enterprise'] }); settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-merge_users`, values.mergeUsers || false, { type: 'boolean', group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Merge_Users', persistent: true }); settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-show_button` , values.showButton || true , { type: 'boolean', group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Show_Button_On_Login_Page', persistent: true }); settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-groups_channel_map` , values.channelsMap || '{\n\t"rocket-admin": "admin",\n\t"tech-support": "support"\n}' , { type: 'code' , multiline: true, code: 'application/json', group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Channel_Map', persistent: true }); diff --git a/app/lib/server/functions/closeOmnichannelConversations.ts b/app/lib/server/functions/closeOmnichannelConversations.ts index 46be29d8a984..c5cff5081f35 100644 --- a/app/lib/server/functions/closeOmnichannelConversations.ts +++ b/app/lib/server/functions/closeOmnichannelConversations.ts @@ -12,8 +12,9 @@ type SubscribedRooms = { export const closeOmnichannelConversations = (user: IUser, subscribedRooms: SubscribedRooms[]): void => { const roomsInfo = LivechatRooms.findByIds(subscribedRooms.map(({ rid }) => rid)); - const language = settings.get('Language') || 'en'; - roomsInfo.map((room: any) => - Livechat.closeRoom({ user, visitor: {}, room, comment: TAPi18n.__('Agent_deactivated', { lng: language }) }), - ); + const language = settings.get('Language') || 'en'; + const comment = TAPi18n.__('Agent_deactivated', { lng: language }); + roomsInfo.forEach((room: any) => { + Livechat.closeRoom({ user, visitor: {}, room, comment }); + }); }; diff --git a/app/lib/server/functions/deleteMessage.ts b/app/lib/server/functions/deleteMessage.ts index 8f698842e205..96e563b1a464 100644 --- a/app/lib/server/functions/deleteMessage.ts +++ b/app/lib/server/functions/deleteMessage.ts @@ -2,14 +2,15 @@ import { Meteor } from 'meteor/meteor'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; -import { Messages, Uploads, Rooms } from '../../../models/server'; +import { Messages, Rooms } from '../../../models/server'; +import { Uploads } from '../../../models/server/raw'; import { Notifications } from '../../../notifications/server'; import { callbacks } from '../../../callbacks/server'; import { Apps } from '../../../apps/server'; import { IMessage } from '../../../../definition/IMessage'; import { IUser } from '../../../../definition/IUser'; -export const deleteMessage = function(message: IMessage, user: IUser): void { +export const deleteMessage = async function(message: IMessage, user: IUser): Promise { const deletedMsg = Messages.findOneById(message._id); const isThread = deletedMsg.tcount > 0; const keepHistory = settings.get('Message_KeepHistory') || isThread; @@ -36,9 +37,9 @@ export const deleteMessage = function(message: IMessage, user: IUser): void { Messages.setHiddenById(message._id, true); } - files.forEach((file) => { - file?._id && Uploads.update(file._id, { $set: { _hidden: true } }); - }); + for await (const file of files) { + file?._id && await Uploads.update({ _id: file._id }, { $set: { _hidden: true } }); + } } else { if (!showDeletedStatus) { Messages.removeById(message._id); diff --git a/app/lib/server/functions/deleteUser.js b/app/lib/server/functions/deleteUser.js index 680517db5405..4193774e1136 100644 --- a/app/lib/server/functions/deleteUser.js +++ b/app/lib/server/functions/deleteUser.js @@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { FileUpload } from '../../../file-upload/server'; -import { Users, Subscriptions, Messages, Rooms, Integrations, FederationServers } from '../../../models/server'; +import { Users, Subscriptions, Messages, Rooms } from '../../../models/server'; +import { FederationServers, Integrations } from '../../../models/server/raw'; import { settings } from '../../../settings/server'; import { updateGroupDMsName } from './updateGroupDMsName'; import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; @@ -10,7 +11,7 @@ import { getSubscribedRoomsForUserWithDetails, shouldRemoveOrChangeOwner } from import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; import { api } from '../../../../server/sdk/api'; -export const deleteUser = function(userId, confirmRelinquish = false) { +export async function deleteUser(userId, confirmRelinquish = false) { const user = Users.findOneById(userId, { fields: { username: 1, avatarOrigin: 1, federation: 1 }, }); @@ -36,7 +37,7 @@ export const deleteUser = function(userId, confirmRelinquish = false) { // Users without username can't do anything, so there is nothing to remove if (user.username != null) { - relinquishRoomOwnerships(userId, subscribedRooms); + await relinquishRoomOwnerships(userId, subscribedRooms); const messageErasureType = settings.get('Message_ErasureType'); switch (messageErasureType) { @@ -64,7 +65,7 @@ export const deleteUser = function(userId, confirmRelinquish = false) { FileUpload.getStore('Avatars').deleteByName(user.username); } - Integrations.disableByUserId(userId); // Disables all the integrations which rely on the user being deleted. + await Integrations.disableByUserId(userId); // Disables all the integrations which rely on the user being deleted. api.broadcast('user.deleted', user); } @@ -75,5 +76,5 @@ export const deleteUser = function(userId, confirmRelinquish = false) { updateGroupDMsName(user); // Refresh the servers list - FederationServers.refreshServers(); -}; + await FederationServers.refreshServers(); +} diff --git a/app/lib/server/functions/relinquishRoomOwnerships.js b/app/lib/server/functions/relinquishRoomOwnerships.js index 7c56e3bc05a5..f5c403f1b2b1 100644 --- a/app/lib/server/functions/relinquishRoomOwnerships.js +++ b/app/lib/server/functions/relinquishRoomOwnerships.js @@ -1,5 +1,6 @@ import { FileUpload } from '../../../file-upload/server'; -import { Subscriptions, Messages, Rooms, Roles } from '../../../models/server'; +import { Subscriptions, Messages, Rooms } from '../../../models/server'; +import { Roles } from '../../../models/server/raw'; const bulkRoomCleanUp = (rids) => { // no bulk deletion for files @@ -12,11 +13,14 @@ const bulkRoomCleanUp = (rids) => { ])); }; -export const relinquishRoomOwnerships = function(userId, subscribedRooms, removeDirectMessages = true) { +export const relinquishRoomOwnerships = async function(userId, subscribedRooms, removeDirectMessages = true) { // change owners - subscribedRooms - .filter(({ shouldChangeOwner }) => shouldChangeOwner) - .forEach(({ newOwner, rid }) => Roles.addUserRoles(newOwner, ['owner'], rid)); + const changeOwner = subscribedRooms + .filter(({ shouldChangeOwner }) => shouldChangeOwner); + + for await (const { newOwner, rid } of changeOwner) { + await Roles.addUserRoles(newOwner, ['owner'], rid); + } const roomIdsToRemove = subscribedRooms.filter(({ shouldBeRemoved }) => shouldBeRemoved).map(({ rid }) => rid); diff --git a/app/lib/server/functions/setRoomAvatar.js b/app/lib/server/functions/setRoomAvatar.js index 9b0ea487c758..540de2799201 100644 --- a/app/lib/server/functions/setRoomAvatar.js +++ b/app/lib/server/functions/setRoomAvatar.js @@ -2,13 +2,14 @@ import { Meteor } from 'meteor/meteor'; import { RocketChatFile } from '../../../file'; import { FileUpload } from '../../../file-upload'; -import { Rooms, Avatars, Messages } from '../../../models/server'; +import { Rooms, Messages } from '../../../models/server'; +import { Avatars } from '../../../models/server/raw'; import { api } from '../../../../server/sdk/api'; -export const setRoomAvatar = function(rid, dataURI, user) { +export const setRoomAvatar = async function(rid, dataURI, user) { const fileStore = FileUpload.getStore('Avatars'); - const current = Avatars.findOneByRoomId(rid); + const current = await Avatars.findOneByRoomId(rid); if (!dataURI) { fileStore.deleteByRoomId(rid); diff --git a/app/lib/server/functions/setUserActiveStatus.js b/app/lib/server/functions/setUserActiveStatus.js index 8089fbf7d1cf..77a137695fdd 100644 --- a/app/lib/server/functions/setUserActiveStatus.js +++ b/app/lib/server/functions/setUserActiveStatus.js @@ -63,7 +63,7 @@ export function setUserActiveStatus(userId, active, confirmRelinquish = false) { } closeOmnichannelConversations(user, livechatSubscribedRooms); - relinquishRoomOwnerships(user, chatSubscribedRooms, false); + Promise.await(relinquishRoomOwnerships(user, chatSubscribedRooms, false)); } if (active && !user.active) { diff --git a/app/lib/server/functions/setUsername.js b/app/lib/server/functions/setUsername.js index 8795fb6b01fc..97a05291f5d1 100644 --- a/app/lib/server/functions/setUsername.js +++ b/app/lib/server/functions/setUsername.js @@ -3,7 +3,8 @@ import s from 'underscore.string'; import { Accounts } from 'meteor/accounts-base'; import { settings } from '../../../settings'; -import { Users, Invites } from '../../../models/server'; +import { Users } from '../../../models/server'; +import { Invites } from '../../../models/server/raw'; import { hasPermission } from '../../../authorization'; import { RateLimiter } from '../lib'; import { addUserToRoom } from './addUserToRoom'; @@ -70,7 +71,7 @@ export const _setUsername = function(userId, u, fullUser) { // If it's the first username and the user has an invite Token, then join the invite room if (!previousUsername && user.inviteToken) { - const inviteData = Invites.findOneById(user.inviteToken); + const inviteData = Promise.await(Invites.findOneById(user.inviteToken)); if (inviteData && inviteData.rid) { addUserToRoom(inviteData.rid, user); } diff --git a/app/lib/server/lib/getRoomRoles.js b/app/lib/server/lib/getRoomRoles.js index 9c3718628782..6ed6527c5368 100644 --- a/app/lib/server/lib/getRoomRoles.js +++ b/app/lib/server/lib/getRoomRoles.js @@ -1,7 +1,8 @@ import _ from 'underscore'; import { settings } from '../../../settings'; -import { Subscriptions, Users, Roles } from '../../../models'; +import { Subscriptions, Users } from '../../../models'; +import { Roles } from '../../../models/server/raw'; export function getRoomRoles(rid) { const options = { @@ -17,7 +18,7 @@ export function getRoomRoles(rid) { const UI_Use_Real_Name = settings.get('UI_Use_Real_Name') === true; - const roles = Roles.find({ scope: 'Subscriptions', description: { $exists: 1, $ne: '' } }).fetch(); + const roles = Promise.await(Roles.find({ scope: 'Subscriptions', description: { $exists: 1, $ne: '' } }).toArray()); const subscriptions = Subscriptions.findByRoomIdAndRoles(rid, _.pluck(roles, '_id'), options).fetch(); if (!UI_Use_Real_Name) { diff --git a/app/lib/server/methods/deleteMessage.js b/app/lib/server/methods/deleteMessage.js index 086b9caee7f9..8be7da4e5e45 100644 --- a/app/lib/server/methods/deleteMessage.js +++ b/app/lib/server/methods/deleteMessage.js @@ -6,7 +6,7 @@ import { Messages } from '../../../models'; import { deleteMessage } from '../functions'; Meteor.methods({ - deleteMessage(message) { + async deleteMessage(message) { check(message, Match.ObjectIncluding({ _id: String, })); diff --git a/app/lib/server/methods/deleteUserOwnAccount.js b/app/lib/server/methods/deleteUserOwnAccount.js index 1ff7494a8751..2d7f269b08f7 100644 --- a/app/lib/server/methods/deleteUserOwnAccount.js +++ b/app/lib/server/methods/deleteUserOwnAccount.js @@ -9,7 +9,7 @@ import { Users } from '../../../models'; import { deleteUser } from '../functions'; Meteor.methods({ - deleteUserOwnAccount(password, confirmRelinquish) { + async deleteUserOwnAccount(password, confirmRelinquish) { check(password, String); if (!Meteor.userId()) { @@ -39,7 +39,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-username', 'Invalid username', { method: 'deleteUserOwnAccount' }); } - deleteUser(userId, confirmRelinquish); + await deleteUser(userId, confirmRelinquish); return true; }, diff --git a/app/lib/server/methods/leaveRoom.js b/app/lib/server/methods/leaveRoom.ts similarity index 76% rename from app/lib/server/methods/leaveRoom.js rename to app/lib/server/methods/leaveRoom.ts index 561d7bdb548a..cce12a25b8f8 100644 --- a/app/lib/server/methods/leaveRoom.js +++ b/app/lib/server/methods/leaveRoom.ts @@ -1,13 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { hasPermission, hasRole, getUsersInRole } from '../../../authorization'; -import { Subscriptions, Rooms } from '../../../models'; +import { hasPermission, hasRole } from '../../../authorization/server'; +import { Subscriptions, Rooms } from '../../../models/server'; import { removeUserFromRoom } from '../functions'; import { roomTypes, RoomMemberActions } from '../../../utils/server'; +import { Roles } from '../../../models/server/raw'; Meteor.methods({ - leaveRoom(rid) { + async leaveRoom(rid) { check(rid, String); if (!Meteor.userId()) { @@ -17,7 +18,7 @@ Meteor.methods({ const room = Rooms.findOneById(rid); const user = Meteor.user(); - if (!roomTypes.getConfig(room.t).allowMemberAction(room, RoomMemberActions.LEAVE)) { + if (!user || !roomTypes.getConfig(room.t).allowMemberAction(room, RoomMemberActions.LEAVE)) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'leaveRoom' }); } @@ -32,7 +33,8 @@ 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 numOwners = getUsersInRole('owner', room._id).count(); + const cursor = await Roles.findUsersInRole('owner', room._id); + const numOwners = Promise.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/app/lib/server/methods/refreshOAuthService.js b/app/lib/server/methods/refreshOAuthService.ts similarity index 66% rename from app/lib/server/methods/refreshOAuthService.js rename to app/lib/server/methods/refreshOAuthService.ts index e0ef565cb45e..14dbeaa1fcd6 100644 --- a/app/lib/server/methods/refreshOAuthService.js +++ b/app/lib/server/methods/refreshOAuthService.ts @@ -1,11 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { ServiceConfiguration } from 'meteor/service-configuration'; -import { hasPermission } from '../../../authorization'; -import { Settings } from '../../../models'; +import { hasPermission } from '../../../authorization/server'; +import { Settings } from '../../../models/server/raw'; Meteor.methods({ - refreshOAuthService() { + async refreshOAuthService() { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'refreshOAuthService' }); } @@ -16,6 +16,6 @@ Meteor.methods({ ServiceConfiguration.configurations.remove({}); - Settings.update({ _id: /^(Accounts_OAuth_|SAML_|CAS_|Blockstack_).+/ }, { $set: { _updatedAt: new Date() } }, { multi: true }); + await Settings.update({ _id: /^(Accounts_OAuth_|SAML_|CAS_|Blockstack_).+/ }, { $set: { _updatedAt: new Date() } }, { multi: true }); }, }); diff --git a/app/lib/server/methods/removeOAuthService.js b/app/lib/server/methods/removeOAuthService.js deleted file mode 100644 index 5c271f3e75f0..000000000000 --- a/app/lib/server/methods/removeOAuthService.js +++ /dev/null @@ -1,51 +0,0 @@ -import { capitalize } from '@rocket.chat/string-helpers'; -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; - -import { hasPermission } from '../../../authorization'; -import { Settings } from '../../../models/server'; - -Meteor.methods({ - removeOAuthService(name) { - check(name, String); - - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'removeOAuthService' }); - } - - if (hasPermission(Meteor.userId(), 'add-oauth-service') !== true) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'removeOAuthService' }); - } - - name = name.toLowerCase().replace(/[^a-z0-9_]/g, ''); - name = capitalize(name); - Settings.removeById(`Accounts_OAuth_Custom-${ name }`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-url`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-token_path`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-identity_path`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-authorize_path`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-scope`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-access_token_param`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-token_sent_via`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-identity_token_sent_via`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-id`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-secret`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-button_label_text`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-button_label_color`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-button_color`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-login_style`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-key_field`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-username_field`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-email_field`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-name_field`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-avatar_field`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-roles_claim`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-merge_roles`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-merge_users`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-show_button`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-groups_claim`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-channels_admin`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-map_channels`); - Settings.removeById(`Accounts_OAuth_Custom-${ name }-groups_channel_map`); - }, -}); diff --git a/app/lib/server/methods/removeOAuthService.ts b/app/lib/server/methods/removeOAuthService.ts new file mode 100644 index 000000000000..4aecbcdc53df --- /dev/null +++ b/app/lib/server/methods/removeOAuthService.ts @@ -0,0 +1,55 @@ +import { capitalize } from '@rocket.chat/string-helpers'; +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; + +import { hasPermission } from '../../../authorization/server'; +import { Settings } from '../../../models/server/raw'; + + +Meteor.methods({ + async removeOAuthService(name) { + check(name, String); + + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'removeOAuthService' }); + } + + if (hasPermission(Meteor.userId(), 'add-oauth-service') !== true) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'removeOAuthService' }); + } + + name = name.toLowerCase().replace(/[^a-z0-9_]/g, ''); + name = capitalize(name); + await Promise.all([ + Settings.removeById(`Accounts_OAuth_Custom-${ name }`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-url`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-token_path`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-identity_path`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-authorize_path`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-scope`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-access_token_param`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-token_sent_via`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-identity_token_sent_via`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-id`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-secret`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-button_label_text`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-button_label_color`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-button_color`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-login_style`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-key_field`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-username_field`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-email_field`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-name_field`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-avatar_field`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-roles_claim`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-merge_roles`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-roles_to_sync`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-merge_users`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-show_button`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-groups_claim`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-channels_admin`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-map_channels`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-groups_channel_map`), + ]); + }, +}); diff --git a/app/lib/server/methods/saveSetting.js b/app/lib/server/methods/saveSetting.js index b375b1ad5952..993915db53f6 100644 --- a/app/lib/server/methods/saveSetting.js +++ b/app/lib/server/methods/saveSetting.js @@ -3,11 +3,11 @@ import { Match, check } from 'meteor/check'; import { hasPermission, hasAllPermission } from '../../../authorization/server'; import { getSettingPermissionId } from '../../../authorization/lib'; -import { Settings } from '../../../models'; import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; +import { Settings } from '../../../models/server/raw'; Meteor.methods({ - saveSetting: twoFactorRequired(function(_id, value, editor) { + saveSetting: twoFactorRequired(async function(_id, value, editor) { const uid = Meteor.userId(); if (!uid) { throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { @@ -26,7 +26,7 @@ Meteor.methods({ // Verify the _id passed in is a string. check(_id, String); - const setting = Settings.db.findOneById(_id); + const setting = await Settings.findOneById(_id); // Verify the value is what it should be switch (setting.type) { @@ -44,7 +44,7 @@ Meteor.methods({ break; } - Settings.updateValueAndEditorById(_id, value, editor); + await Settings.updateValueAndEditorById(_id, value, editor); return true; }), }); diff --git a/app/lib/server/methods/saveSettings.js b/app/lib/server/methods/saveSettings.js index 6b99b3c7665c..6da862bd9aa5 100644 --- a/app/lib/server/methods/saveSettings.js +++ b/app/lib/server/methods/saveSettings.js @@ -1,13 +1,13 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { hasPermission } from '../../../authorization'; -import { Settings } from '../../../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(function(params = []) { + saveSettings: twoFactorRequired(async function(params = []) { const uid = Meteor.userId(); const settingsNotAllowed = []; if (uid === null) { @@ -18,16 +18,16 @@ Meteor.methods({ const editPrivilegedSetting = hasPermission(uid, 'edit-privileged-setting'); const manageSelectedSettings = hasPermission(uid, 'manage-selected-settings'); - params.forEach(({ _id, value }) => { + await Promise.all(params.map(async ({ _id, value }) => { // Verify the _id passed in is a string. check(_id, String); if (!editPrivilegedSetting && !(manageSelectedSettings && hasPermission(uid, getSettingPermissionId(_id)))) { return settingsNotAllowed.push(_id); } - const setting = Settings.db.findOneById(_id); + const setting = await Settings.findOneById(_id); // Verify the value is what it should be - switch (setting.type) { + switch (setting?.type) { case 'roomPick': check(value, Match.OneOf([Object], '')); break; @@ -44,7 +44,7 @@ Meteor.methods({ check(value, String); break; } - }); + })); if (settingsNotAllowed.length) { throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { @@ -53,8 +53,8 @@ Meteor.methods({ }); } - params.forEach(({ _id, value, editor }) => Settings.updateValueById(_id, value, editor)); + await Promise.all(params.map(({ _id, value, editor }) => Settings.updateValueById(_id, value, editor))); return true; - }), + }, {}), }); diff --git a/app/lib/server/startup/oAuthServicesUpdate.js b/app/lib/server/startup/oAuthServicesUpdate.js index f3206824e277..c0a4d6f46b58 100644 --- a/app/lib/server/startup/oAuthServicesUpdate.js +++ b/app/lib/server/startup/oAuthServicesUpdate.js @@ -55,6 +55,7 @@ function _OAuthServicesUpdate() { data.mergeUsers = settings.get(`${ key }-merge_users`); data.mapChannels = settings.get(`${ key }-map_channels`); data.mergeRoles = settings.get(`${ key }-merge_roles`); + data.rolesToSync = settings.get(`${ key }-roles_to_sync`); data.showButton = settings.get(`${ key }-show_button`); new CustomOAuth(serviceName.toLowerCase(), { @@ -78,6 +79,7 @@ function _OAuthServicesUpdate() { channelsAdmin: data.channelsAdmin, mergeUsers: data.mergeUsers, mergeRoles: data.mergeRoles, + rolesToSync: data.rolesToSync, accessTokenParam: data.accessTokenParam, showButton: data.showButton, }); @@ -184,6 +186,7 @@ function customOAuthServicesInit() { mergeUsers: process.env[`${ serviceKey }_merge_users`] === 'true', mapChannels: process.env[`${ serviceKey }_map_channels`], mergeRoles: process.env[`${ serviceKey }_merge_roles`] === 'true', + rolesToSync: process.env[`${ serviceKey }_roles_to_sync`], showButton: process.env[`${ serviceKey }_show_button`] === 'true', avatarField: process.env[`${ serviceKey }_avatar_field`], }; diff --git a/app/lib/server/startup/rateLimiter.js b/app/lib/server/startup/rateLimiter.js index 464404b88048..e10921f3e8de 100644 --- a/app/lib/server/startup/rateLimiter.js +++ b/app/lib/server/startup/rateLimiter.js @@ -108,10 +108,9 @@ const checkNameForStream = (name) => name && !names.has(name) && name.startsWith const ruleIds = {}; -const callback = (message, name) => (reply, input) => { +const callback = (msg, name) => (reply, input) => { if (reply.allowed === false) { - logger.info('DDP RATE LIMIT:', message); - logger.info({ ...reply, ...input }); + logger.info({ msg, reply, input }); metrics.ddpRateLimitExceeded.inc({ limit_name: name, user_id: input.userId, diff --git a/app/lib/server/startup/settings.ts b/app/lib/server/startup/settings.ts index b8c3309b5cf2..1562634a24ee 100644 --- a/app/lib/server/startup/settings.ts +++ b/app/lib/server/startup/settings.ts @@ -1675,10 +1675,6 @@ settingsRegistry.addGroup('Setup_Wizard', function() { key: 'aerospaceDefense', i18nLabel: 'Aerospace_and_Defense', }, - { - key: 'blockchain', - i18nLabel: 'Blockchain', - }, { key: 'consulting', i18nLabel: 'Consulting', @@ -2977,7 +2973,7 @@ settingsRegistry.addGroup('Setup_Wizard', function() { }); settingsRegistry.addGroup('Rate Limiter', function() { - this.section('DDP Rate Limiter', function() { + this.section('DDP_Rate_Limiter', function() { this.add('DDP_Rate_Limit_IP_Enabled', true, { type: 'boolean' }); this.add('DDP_Rate_Limit_IP_Requests_Allowed', 120000, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_IP_Enabled', value: true } }); this.add('DDP_Rate_Limit_IP_Interval_Time', 60000, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_IP_Enabled', value: true } }); @@ -2999,12 +2995,16 @@ settingsRegistry.addGroup('Rate Limiter', function() { this.add('DDP_Rate_Limit_Connection_By_Method_Interval_Time', 10000, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_Connection_By_Method_Enabled', value: true } }); }); - this.section('API Rate Limiter', function() { + this.section('API_Rate_Limiter', function() { this.add('API_Enable_Rate_Limiter', true, { type: 'boolean' }); this.add('API_Enable_Rate_Limiter_Dev', true, { type: 'boolean', enableQuery: { _id: 'API_Enable_Rate_Limiter', value: true } }); this.add('API_Enable_Rate_Limiter_Limit_Calls_Default', 10, { type: 'int', enableQuery: { _id: 'API_Enable_Rate_Limiter', value: true } }); this.add('API_Enable_Rate_Limiter_Limit_Time_Default', 60000, { type: 'int', enableQuery: { _id: 'API_Enable_Rate_Limiter', value: true } }); }); + + this.section('Feature_Limiting', function() { + this.add('Rate_Limiter_Limit_RegisterUser', 1, { type: 'int', enableQuery: { _id: 'API_Enable_Rate_Limiter', value: true } }); + }); }); settingsRegistry.addGroup('Troubleshoot', function() { diff --git a/app/lib/tests/server.tests.js b/app/lib/tests/server.tests.js index cc6a4de04b1a..a606cff94901 100644 --- a/app/lib/tests/server.tests.js +++ b/app/lib/tests/server.tests.js @@ -1,7 +1,3 @@ -/* eslint-env mocha */ -import 'babel-polyfill'; -import assert from 'assert'; - import { expect } from 'chai'; import './server.mocks.js'; @@ -41,11 +37,11 @@ describe('PasswordPolicyClass', () => { describe('Password tests with default options', () => { it('should allow all passwords', () => { const passwordPolice = new PasswordPolicyClass({ throwError: false }); - assert.equal(passwordPolice.validate(), false); - assert.equal(passwordPolice.validate(''), false); - assert.equal(passwordPolice.validate(' '), false); - assert.equal(passwordPolice.validate('a'), true); - assert.equal(passwordPolice.validate('aaaaaaaaa'), true); + expect(passwordPolice.validate()).to.be.equal(false); + expect(passwordPolice.validate('')).to.be.equal(false); + expect(passwordPolice.validate(' ')).to.be.equal(false); + expect(passwordPolice.validate('a')).to.be.equal(true); + expect(passwordPolice.validate('aaaaaaaaa')).to.be.equal(true); }); }); }); diff --git a/app/livechat/client/lib/chartHandler.js b/app/livechat/client/lib/chartHandler.js index 7af6388409b1..f99571f0e66d 100644 --- a/app/livechat/client/lib/chartHandler.js +++ b/app/livechat/client/lib/chartHandler.js @@ -194,9 +194,9 @@ export const drawDoughnutChart = async (chart, title, chartContext, dataLabels, data: dataPoints, // data points corresponding to data labels, x-axis points backgroundColor: [ '#2de0a5', - '#ffd21f', - '#f5455c', '#cbced1', + '#f5455c', + '#ffd21f', ], borderWidth: 0, }], diff --git a/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.html b/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.html index 1b5e7ec2b104..7b83a9db8de7 100644 --- a/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.html +++ b/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.html @@ -33,17 +33,25 @@

{{_ "No_results_found"}}

{{else}} -
- +
+ {{/if}} {{/if}} diff --git a/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.js b/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.js index c73136c07a8f..cb3171f67c65 100644 --- a/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.js +++ b/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.js @@ -36,6 +36,12 @@ Template.contactChatHistoryMessages.helpers({ empty() { return Template.instance().messages.get().length === 0; }, + hasError() { + return Template.instance().hasError.get(); + }, + error() { + return Template.instance().error.get(); + }, }); Template.contactChatHistoryMessages.events({ @@ -72,15 +78,23 @@ Template.contactChatHistoryMessages.onCreated(function() { this.searchTerm = new ReactiveVar(''); this.isLoading = new ReactiveVar(true); this.limit = new ReactiveVar(MESSAGES_LIMIT); + this.hasError = new ReactiveVar(false); + this.error = new ReactiveVar(null); this.loadMessages = async (url) => { this.isLoading.set(true); const offset = this.offset.get(); - const { messages, total } = await APIClient.v1.get(url); - this.messages.set(offset === 0 ? messages : this.messages.get().concat(messages)); - this.hasMore.set(total > this.messages.get().length); - this.isLoading.set(false); + try { + const { messages, total } = await APIClient.v1.get(url); + this.messages.set(offset === 0 ? messages : this.messages.get().concat(messages)); + this.hasMore.set(total > this.messages.get().length); + } catch (e) { + this.hasError.set(true); + this.error.set(e); + } finally { + this.isLoading.set(false); + } }; this.autorun(() => { @@ -92,7 +106,7 @@ Template.contactChatHistoryMessages.onCreated(function() { return this.loadMessages(`chat.search/?roomId=${ this.rid }&searchText=${ searchTerm }&count=${ limit }&offset=${ offset }&sort={"ts": 1}`); } - this.loadMessages(`channels.messages/?roomId=${ this.rid }&count=${ limit }&offset=${ offset }&sort={"ts": 1}&query={"$or": [ {"t": {"$exists": false} }, {"t": "livechat-close"} ] }`); + this.loadMessages(`livechat/${ this.rid }/messages?count=${ limit }&offset=${ offset }&sort={"ts": 1}`); }); this.autorun(() => { diff --git a/app/livechat/imports/server/rest/rooms.js b/app/livechat/imports/server/rest/rooms.js index d2f4c6e20d63..9680b8baffce 100644 --- a/app/livechat/imports/server/rest/rooms.js +++ b/app/livechat/imports/server/rest/rooms.js @@ -21,12 +21,13 @@ API.v1.addRoute('livechat/rooms', { authRequired: true }, { get() { const { offset, count } = this.getPaginationItems(); const { sort, fields } = this.parseJsonQuery(); - const { agents, departmentId, open, tags, roomName } = this.requestParams(); + const { agents, departmentId, open, tags, roomName, onhold } = this.requestParams(); let { createdAt, customFields, closedAt } = this.requestParams(); check(agents, Match.Maybe([String])); check(roomName, Match.Maybe(String)); check(departmentId, Match.Maybe(String)); check(open, Match.Maybe(String)); + check(onhold, Match.Maybe(String)); check(tags, Match.Maybe([String])); const hasAdminAccess = hasPermission(this.userId, 'view-livechat-rooms'); @@ -51,6 +52,7 @@ API.v1.addRoute('livechat/rooms', { authRequired: true }, { closedAt, tags, customFields, + onhold, options: { offset, count, sort, fields }, }))); }, diff --git a/app/livechat/imports/server/rest/sms.js b/app/livechat/imports/server/rest/sms.js index 4f29e7e997e0..ac180f4c652e 100644 --- a/app/livechat/imports/server/rest/sms.js +++ b/app/livechat/imports/server/rest/sms.js @@ -30,7 +30,7 @@ const defineDepartment = (idOrName) => { return department && department._id; }; -const defineVisitor = (smsNumber) => { +const defineVisitor = (smsNumber, targetDepartment) => { const visitor = LivechatVisitors.findOneVisitorByPhone(smsNumber); let data = { token: (visitor && visitor.token) || Random.id(), @@ -45,9 +45,8 @@ const defineVisitor = (smsNumber) => { }); } - const department = defineDepartment(SMS.department); - if (department) { - data.department = department; + if (targetDepartment) { + data.department = targetDepartment; } const id = Livechat.registerGuest(data); @@ -70,10 +69,15 @@ API.v1.addRoute('livechat/sms-incoming/:service', { post() { const SMSService = SMS.getService(this.urlParams.service); const sms = SMSService.parse(this.bodyParams); + const { department } = this.queryParams; + let targetDepartment = defineDepartment(department || SMS.department); + if (!targetDepartment) { + targetDepartment = defineDepartment(SMS.department); + } - const visitor = defineVisitor(sms.from); + const visitor = defineVisitor(sms.from, targetDepartment); const { token } = visitor; - const room = LivechatRooms.findOneOpenByVisitorToken(token); + const room = LivechatRooms.findOneOpenByVisitorTokenAndDepartmentId(token, targetDepartment); const roomExists = !!room; const location = normalizeLocationSharing(sms); const rid = (room && room._id) || Random.id(); diff --git a/app/livechat/imports/server/rest/visitors.ts b/app/livechat/imports/server/rest/visitors.ts new file mode 100644 index 000000000000..e75d4a955a2a --- /dev/null +++ b/app/livechat/imports/server/rest/visitors.ts @@ -0,0 +1,47 @@ + +import { check } from 'meteor/check'; + +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'; +import { IMessage } from '../../../../../definition/IMessage'; + +API.v1.addRoute('livechat/:rid/messages', { authRequired: true, permissionsRequired: ['view-l-room'] }, { + async get() { + check(this.urlParams, { + rid: String, + }); + + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + + const room = LivechatRooms.findOneById(this.urlParams.rid); + + if (!room) { + throw new Error('invalid-room'); + } + + if (!canAccessRoom(room, this.user)) { + throw new Error('not-allowed'); + } + + const cursor = 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[]; + + return API.v1.success({ + messages: normalizeMessagesForUser(messages, this.userId), + offset, + count, + total, + }); + }, +}); diff --git a/app/livechat/lib/messageTypes.js b/app/livechat/lib/messageTypes.js index bde52192cbe9..fb6fa4c10160 100644 --- a/app/livechat/lib/messageTypes.js +++ b/app/livechat/lib/messageTypes.js @@ -1,4 +1,6 @@ +import formatDistance from 'date-fns/formatDistance'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import moment from 'moment'; import { MessageTypes } from '../../ui-utils'; @@ -81,6 +83,22 @@ MessageTypes.registerType({ 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 message.msg; + }, +}); + MessageTypes.registerType({ id: 'omnichannel_placed_chat_on_hold', system: true, diff --git a/app/livechat/server/api.js b/app/livechat/server/api.js index 6a13dddc86bf..7aa0ee39c4a3 100644 --- a/app/livechat/server/api.js +++ b/app/livechat/server/api.js @@ -11,6 +11,7 @@ import '../imports/server/rest/triggers.js'; import '../imports/server/rest/integrations.js'; import '../imports/server/rest/messages.js'; 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'; diff --git a/app/livechat/server/api/lib/departments.js b/app/livechat/server/api/lib/departments.js index 0a70d1b6fca4..1e70a709444f 100644 --- a/app/livechat/server/api/lib/departments.js +++ b/app/livechat/server/api/lib/departments.js @@ -71,7 +71,7 @@ export async function findDepartmentsToAutocomplete({ uid, selector, onlyMyDepar let { conditions = {} } = selector; const options = { - fields: { + projection: { _id: 1, name: 1, }, diff --git a/app/livechat/server/api/lib/livechat.js b/app/livechat/server/api/lib/livechat.js index a7a29598250f..b374e5139a99 100644 --- a/app/livechat/server/api/lib/livechat.js +++ b/app/livechat/server/api/lib/livechat.js @@ -1,7 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { LivechatRooms, LivechatVisitors, LivechatDepartment, LivechatTrigger, EmojiCustom } from '../../../../models/server'; +import { LivechatRooms, LivechatVisitors, LivechatDepartment, LivechatTrigger } from '../../../../models/server'; +import { EmojiCustom } from '../../../../models/server/raw'; import { Livechat } from '../../lib/Livechat'; import { callbacks } from '../../../../callbacks/server'; import { normalizeAgent } from '../../lib/Helper'; @@ -55,6 +57,7 @@ export function findOpenRoom(token, departmentId) { departmentId: 1, servedBy: 1, open: 1, + callStatus: 1, }, }; @@ -86,12 +89,12 @@ export function normalizeHttpHeaderData(headers = {}) { const httpHeaders = Object.assign({}, headers); return { httpHeaders }; } -export function settings() { +export async function settings() { const initSettings = Livechat.getInitSettings(); const triggers = findTriggers(); const departments = findDepartments(); const sound = `${ Meteor.absoluteUrl() }sounds/chime.mp3`; - const emojis = EmojiCustom.find().fetch(); + const emojis = await EmojiCustom.find().toArray(); return { enabled: initSettings.Livechat_enabled, settings: { @@ -100,7 +103,7 @@ export function settings() { nameFieldRegistrationForm: initSettings.Livechat_name_field_registration_form, emailFieldRegistrationForm: initSettings.Livechat_email_field_registration_form, displayOfflineForm: initSettings.Livechat_display_offline_form, - videoCall: initSettings.Livechat_videocall_enabled === true && initSettings.Jitsi_Enabled === true, + videoCall: initSettings.Omnichannel_call_provider === 'Jitsi' && initSettings.Jitsi_Enabled === true, fileUpload: initSettings.Livechat_fileupload_enabled && initSettings.FileUpload_Enabled, language: initSettings.Language, transcript: initSettings.Livechat_enable_transcript, @@ -116,10 +119,16 @@ export function settings() { color: initSettings.Livechat_title_color, offlineTitle: initSettings.Livechat_offline_title, offlineColor: initSettings.Livechat_offline_title_color, - actionLinks: [ - { icon: 'icon-videocam', i18nLabel: 'Accept', method_id: 'createLivechatCall', params: '' }, - { icon: 'icon-cancel', i18nLabel: 'Decline', method_id: 'denyLivechatCall', params: '' }, - ], + actionLinks: { + webrtc: [ + { actionLinksAlignment: 'flex-start', i18nLabel: 'Join_call', label: TAPi18n.__('Join_call'), method_id: 'joinLivechatWebRTCCall' }, + { i18nLabel: 'End_call', label: TAPi18n.__('End_call'), method_id: 'endLivechatWebRTCCall', danger: true }, + ], + jitsi: [ + { icon: 'icon-videocam', i18nLabel: 'Accept', method_id: 'createLivechatCall' }, + { icon: 'icon-cancel', i18nLabel: 'Decline', method_id: 'denyLivechatCall' }, + ], + }, }, messages: { offlineMessage: initSettings.Livechat_offline_message, diff --git a/app/livechat/server/api/lib/rooms.js b/app/livechat/server/api/lib/rooms.js index 72d84803de15..957911737f0a 100644 --- a/app/livechat/server/api/lib/rooms.js +++ b/app/livechat/server/api/lib/rooms.js @@ -9,6 +9,7 @@ export async function findRooms({ closedAt, tags, customFields, + onhold, options: { offset, count, @@ -25,6 +26,7 @@ export async function findRooms({ closedAt, tags, customFields, + onhold: ['t', 'true', '1'].includes(onhold), options: { sort: sort || { ts: -1 }, offset, diff --git a/app/livechat/server/api/lib/visitors.js b/app/livechat/server/api/lib/visitors.js index c0366bf1ac69..d03566d6da99 100644 --- a/app/livechat/server/api/lib/visitors.js +++ b/app/livechat/server/api/lib/visitors.js @@ -72,6 +72,7 @@ export async function findChatHistory({ userId, roomId, visitorId, pagination: { total, }; } + export async function searchChats({ userId, roomId, visitorId, searchText, closedChatsOnly, servedChatsOnly: served, pagination: { offset, count, sort } }) { if (!await hasPermissionAsync(userId, 'view-l-room')) { throw new Error('error-not-authorized'); @@ -111,7 +112,7 @@ export async function findVisitorsToAutocomplete({ userId, selector }) { const { exceptions = [], conditions = {} } = selector; const options = { - fields: { + projection: { _id: 1, name: 1, username: 1, diff --git a/app/livechat/server/api/v1/config.js b/app/livechat/server/api/v1/config.js index e43509d4ba3c..f1a49c1ed395 100644 --- a/app/livechat/server/api/v1/config.js +++ b/app/livechat/server/api/v1/config.js @@ -17,7 +17,7 @@ API.v1.addRoute('livechat/config', { return API.v1.success({ config: { enabled: false } }); } - const config = settings(); + const config = Promise.await(settings()); const { token, department } = this.queryParams; const status = Livechat.online(department); diff --git a/app/livechat/server/api/v1/message.js b/app/livechat/server/api/v1/message.js index 178f571da490..0fd39bc5d086 100644 --- a/app/livechat/server/api/v1/message.js +++ b/app/livechat/server/api/v1/message.js @@ -108,7 +108,7 @@ API.v1.addRoute('livechat/message/:_id', { } if (message.file) { - message = normalizeMessageFileUpload(message); + message = Promise.await(normalizeMessageFileUpload(message)); } return API.v1.success({ message }); @@ -151,7 +151,7 @@ API.v1.addRoute('livechat/message/:_id', { if (result) { let message = Messages.findOneById(_id); if (message.file) { - message = normalizeMessageFileUpload(message); + message = Promise.await(normalizeMessageFileUpload(message)); } return API.v1.success({ message }); @@ -191,7 +191,7 @@ API.v1.addRoute('livechat/message/:_id', { throw new Meteor.Error('invalid-message'); } - const result = Livechat.deleteMessage({ guest, message }); + const result = Promise.await(Livechat.deleteMessage({ guest, message })); if (result) { return API.v1.success({ message: { @@ -251,7 +251,7 @@ API.v1.addRoute('livechat/messages.history/:rid', { const messages = loadMessageHistory({ userId: guest._id, rid, end, limit, ls, sort, offset, text }) .messages - .map(normalizeMessageFileUpload); + .map((...args) => Promise.await(normalizeMessageFileUpload(...args))); return API.v1.success({ messages }); } catch (e) { return API.v1.failure(e); diff --git a/app/livechat/server/api/v1/room.js b/app/livechat/server/api/v1/room.js index 5dcbf8cf1dd0..88c9e4717110 100644 --- a/app/livechat/server/api/v1/room.js +++ b/app/livechat/server/api/v1/room.js @@ -166,7 +166,7 @@ API.v1.addRoute('livechat/room.survey', { throw new Meteor.Error('invalid-room'); } - const config = settings(); + const config = Promise.await(settings()); if (!config.survey || !config.survey.items || !config.survey.values) { throw new Meteor.Error('invalid-livechat-config'); } diff --git a/app/livechat/server/api/v1/videoCall.js b/app/livechat/server/api/v1/videoCall.js index 38b9c2d66491..6aef0c49537e 100644 --- a/app/livechat/server/api/v1/videoCall.js +++ b/app/livechat/server/api/v1/videoCall.js @@ -1,12 +1,15 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { Random } from 'meteor/random'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { Messages } from '../../../../models'; -import { settings as rcSettings } from '../../../../settings'; +import { Messages, Rooms } from '../../../../models'; +import { settings as rcSettings } from '../../../../settings/server'; import { API } from '../../../../api/server'; import { findGuest, getRoom, settings } from '../lib/livechat'; import { OmnichannelSourceType } from '../../../../../definition/IRoom'; +import { hasPermission, canSendMessage } from '../../../../authorization'; +import { Livechat } from '../../lib/Livechat'; API.v1.addRoute('livechat/video.call/:token', { get() { @@ -35,13 +38,13 @@ API.v1.addRoute('livechat/video.call/:token', { }, }; const { room } = getRoom({ guest, rid, roomInfo }); - const config = settings(); - if (!config.theme || !config.theme.actionLinks) { + 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, + actionLinks: config.theme.actionLinks.jitsi, }); let rname; if (rcSettings.get('Jitsi_URL_Room_Hash')) { @@ -63,3 +66,102 @@ API.v1.addRoute('livechat/video.call/:token', { } }, }); + +API.v1.addRoute('livechat/webrtc.call', { authRequired: true }, { + get() { + try { + check(this.queryParams, { + rid: Match.Maybe(String), + }); + + if (!hasPermission(this.userId, 'view-l-room')) { + return API.v1.unauthorized(); + } + + const room = canSendMessage(this.queryParams.rid, { + uid: this.userId, + username: this.user.username, + type: this.user.type, + }); + if (!room) { + throw new Meteor.Error('invalid-room'); + } + + const webrtcCallingAllowed = (rcSettings.get('WebRTC_Enabled') === true) && (rcSettings.get('Omnichannel_call_provider') === 'WebRTC'); + if (!webrtcCallingAllowed) { + throw new Meteor.Error('webRTC calling not enabled'); + } + + const config = Promise.await(settings()); + if (!config.theme || !config.theme.actionLinks || !config.theme.actionLinks.webrtc) { + throw new Meteor.Error('invalid-livechat-config'); + } + + let { callStatus } = room; + + if (!callStatus || callStatus === 'ended' || callStatus === 'declined') { + callStatus = 'ringing'; + Promise.await(Rooms.setCallStatusAndCallStartTime(room._id, callStatus)); + Promise.await(Messages.createWithTypeRoomIdMessageAndUser( + 'livechat_webrtc_video_call', + room._id, + TAPi18n.__('Join_my_room_to_start_the_video_call'), + this.user, + { + actionLinks: config.theme.actionLinks.webrtc, + }, + )); + } + const videoCall = { + rid: room._id, + provider: 'webrtc', + callStatus, + }; + return API.v1.success({ videoCall }); + } catch (e) { + return API.v1.failure(e); + } + }, +}); + +API.v1.addRoute('livechat/webrtc.call/:callId', { authRequired: true }, { + put() { + try { + check(this.urlParams, { + callId: String, + }); + + check(this.bodyParams, { + rid: Match.Maybe(String), + status: Match.Maybe(String), + }); + + const { callId } = this.urlParams; + const { rid, status } = this.bodyParams; + + if (!hasPermission(this.userId, 'view-l-room')) { + return API.v1.unauthorized(); + } + + const room = canSendMessage(rid, { + uid: this.userId, + username: this.user.username, + type: this.user.type, + }); + if (!room) { + throw new Meteor.Error('invalid-room'); + } + + const call = Promise.await(Messages.findOneById(callId)); + if (!call || call.t !== 'livechat_webrtc_video_call') { + throw new Meteor.Error('invalid-callId'); + } + + Livechat.updateCallStatus(callId, rid, status, this.user); + + return API.v1.success({ status }); + } catch (e) { + return API.v1.failure(e); + } + }, +}); diff --git a/app/livechat/server/api/v1/visitor.js b/app/livechat/server/api/v1/visitor.js index 98007540876c..dc4e012839ba 100644 --- a/app/livechat/server/api/v1/visitor.js +++ b/app/livechat/server/api/v1/visitor.js @@ -128,6 +128,30 @@ API.v1.addRoute('livechat/visitor/:token/room', { authRequired: true }, { }, }); +API.v1.addRoute('livechat/visitor.callStatus', { + post() { + try { + check(this.bodyParams, { + token: String, + callStatus: String, + rid: String, + callId: String, + }); + + const { token, callStatus, rid, callId } = this.bodyParams; + const guest = findGuest(token); + if (!guest) { + throw new Meteor.Error('invalid-token'); + } + const status = callStatus; + Livechat.updateCallStatus(callId, rid, status, guest); + return API.v1.success({ token, callStatus }); + } catch (e) { + return API.v1.failure(e); + } + }, +}); + API.v1.addRoute('livechat/visitor.status', { post() { try { diff --git a/app/livechat/server/business-hour/BusinessHourManager.ts b/app/livechat/server/business-hour/BusinessHourManager.ts index e1b5558d0acd..04505776f32b 100644 --- a/app/livechat/server/business-hour/BusinessHourManager.ts +++ b/app/livechat/server/business-hour/BusinessHourManager.ts @@ -1,11 +1,11 @@ import moment from 'moment'; import { ILivechatBusinessHour, LivechatBusinessHourTypes } from '../../../../definition/ILivechatBusinessHour'; -import { ICronJobs } from '../../../utils/server/lib/cron/Cronjobs'; import { IBusinessHourBehavior, IBusinessHourType } from './AbstractBusinessHour'; import { settings } from '../../../settings/server'; import { callbacks } from '../../../callbacks/server'; import { Users } from '../../../models/server/raw'; +import { ICronJobs } from '../../../../definition/ICronJobs'; const cronJobDayDict: Record = { Sunday: 0, diff --git a/app/livechat/server/config.ts b/app/livechat/server/config.ts index f0a19e3246e6..0d37ad8bf9dd 100644 --- a/app/livechat/server/config.ts +++ b/app/livechat/server/config.ts @@ -375,16 +375,6 @@ Meteor.startup(function() { enableQuery: omnichannelEnabledQuery, }); - this.add('Livechat_videocall_enabled', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'Livechat', - public: true, - i18nLabel: 'Videocall_enabled', - i18nDescription: 'Beta_feature_Depends_on_Video_Conference_to_be_enabled', - enableQuery: [{ _id: 'Jitsi_Enabled', value: true }, omnichannelEnabledQuery], - }); - this.add('Livechat_fileupload_enabled', true, { type: 'boolean', group: 'Omnichannel', @@ -616,5 +606,21 @@ Meteor.startup(function() { i18nDescription: 'Time_in_seconds', enableQuery: omnichannelEnabledQuery, }); + + this.add('Omnichannel_call_provider', 'none', { + type: 'select', + public: true, + group: 'Omnichannel', + section: 'Video_and_Audio_Call', + values: [ + { key: 'none', i18nLabel: 'None' }, + { key: 'Jitsi', i18nLabel: 'Jitsi' }, + { key: 'WebRTC', i18nLabel: 'WebRTC' }, + ], + i18nDescription: 'Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings', + i18nLabel: 'Call_provider', + alert: 'The WebRTC provider is currently in alpha!
We recommend using Firefox Browser for this feature since there are some known bugs within other browsers that still need to be fixed.
Please report bugs to github.com/RocketChat/Rocket.Chat/issues', + enableQuery: omnichannelEnabledQuery, + }); }); }); diff --git a/app/livechat/server/hooks/saveAnalyticsData.js b/app/livechat/server/hooks/saveAnalyticsData.js index 4ca8832c153d..96d336c228ca 100644 --- a/app/livechat/server/hooks/saveAnalyticsData.js +++ b/app/livechat/server/hooks/saveAnalyticsData.js @@ -14,7 +14,7 @@ callbacks.add('afterSaveMessage', function(message, room) { } if (message.file) { - message = normalizeMessageFileUpload(message); + message = Promise.await(normalizeMessageFileUpload(message)); } const now = new Date(); diff --git a/app/livechat/server/hooks/sendToCRM.js b/app/livechat/server/hooks/sendToCRM.js index ac1ec663904b..94e4cfcd22eb 100644 --- a/app/livechat/server/hooks/sendToCRM.js +++ b/app/livechat/server/hooks/sendToCRM.js @@ -84,7 +84,7 @@ function sendToCRM(type, room, includeMessages = true) { } const { u } = message; - postData.messages.push(normalizeMessageFileUpload({ u, ...msg })); + postData.messages.push(Promise.await(normalizeMessageFileUpload({ u, ...msg }))); }); } diff --git a/app/livechat/server/hooks/sendToFacebook.js b/app/livechat/server/hooks/sendToFacebook.js index 1af4767e3656..7c1b00f31211 100644 --- a/app/livechat/server/hooks/sendToFacebook.js +++ b/app/livechat/server/hooks/sendToFacebook.js @@ -29,7 +29,7 @@ callbacks.add('afterSaveMessage', function(message, room) { } if (message.file) { - message = normalizeMessageFileUpload(message); + message = Promise.await(normalizeMessageFileUpload(message)); } OmniChannel.reply({ diff --git a/app/livechat/server/lib/Analytics.js b/app/livechat/server/lib/Analytics.js index c4251fbd1bce..b4aa71af346f 100644 --- a/app/livechat/server/lib/Analytics.js +++ b/app/livechat/server/lib/Analytics.js @@ -2,6 +2,7 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import moment from 'moment'; import { LivechatRooms } from '../../../models'; +import { LivechatRooms as LivechatRoomsRaw } from '../../../models/server/raw'; import { secondsToHHMMSS } from '../../../utils/server'; import { getTimezone } from '../../../utils/server/lib/getTimezone'; import { Logger } from '../../../logger'; @@ -288,8 +289,8 @@ export const Analytics = { const totalMessagesInHour = new Map(); // total messages in hour 0, 1, ... 23 of weekday const days = to.diff(from, 'days') + 1; // total days - const summarize = (m) => ({ metrics, msgs }) => { - if (metrics && !metrics.chatDuration) { + const summarize = (m) => ({ metrics, msgs, onHold = false }) => { + if (metrics && !metrics.chatDuration && !onHold) { openConversations++; } totalMessages += msgs; @@ -337,13 +338,17 @@ export const Analytics = { to: utcBusiestHour >= 0 ? moment.utc().set({ hour: utcBusiestHour }).tz(timezone).format('hA') : '-', from: utcBusiestHour >= 0 ? moment.utc().set({ hour: utcBusiestHour }).subtract(1, 'hour').tz(timezone).format('hA') : '', }; + const onHoldConversations = Promise.await(LivechatRoomsRaw.getOnHoldConversationsBetweenDate(from, to, departmentId)); - const data = [{ + return [{ title: 'Total_conversations', value: totalConversations, }, { title: 'Open_conversations', value: openConversations, + }, { + title: 'On_Hold_conversations', + value: onHoldConversations, }, { title: 'Total_messages', value: totalMessages, @@ -357,8 +362,6 @@ export const Analytics = { title: 'Busiest_time', value: `${ busiestHour.from }${ busiestHour.to ? `- ${ busiestHour.to }` : '' }`, }]; - - return data; }, /** diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index aa2f93eb698d..78d54cd5f1e6 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -222,7 +222,7 @@ export const Livechat = { return true; }, - deleteMessage({ guest, message }) { + async deleteMessage({ guest, message }) { Livechat.logger.debug(`Attempting to delete a message by visitor ${ guest._id }`); check(message, Match.ObjectIncluding({ _id: String })); @@ -239,7 +239,7 @@ export const Livechat = { throw new Meteor.Error('error-action-not-allowed', 'Message deleting not allowed', { method: 'livechatDeleteMessage' }); } - deleteMessage(message, guest); + await deleteMessage(message, guest); return true; }, @@ -514,7 +514,7 @@ export const Livechat = { 'Livechat_offline_success_message', 'Livechat_offline_form_unavailable', 'Livechat_display_offline_form', - 'Livechat_videocall_enabled', + 'Omnichannel_call_provider', 'Jitsi_Enabled', 'Language', 'Livechat_enable_transcript', @@ -598,7 +598,7 @@ export const Livechat = { const user = Users.findOneById(userId); const { _id, username, name } = user; const transferredBy = normalizeTransferredByData({ _id, username, name }, room); - this.transfer(room, guest, { roomId: room._id, transferredBy, departmentId: guest.department }); + Promise.await(this.transfer(room, guest, { roomId: room._id, transferredBy, departmentId: guest.department })); }); }, @@ -1278,6 +1278,12 @@ export const Livechat = { }; LivechatVisitors.updateById(contactId, updateUser); }, + updateCallStatus(callId, rid, status, user) { + Rooms.setCallStatus(rid, status); + if (status === 'ended' || status === 'declined') { + return updateMessage({ _id: callId, msg: status, actionLinks: [], webRtcCallEndTs: new Date() }, user); + } + }, }; settings.watch('Livechat_history_monitor_type', (value) => { diff --git a/app/livechat/server/lib/analytics/dashboards.js b/app/livechat/server/lib/analytics/dashboards.js index 18bdca630033..70dc1c7925ff 100644 --- a/app/livechat/server/lib/analytics/dashboards.js +++ b/app/livechat/server/lib/analytics/dashboards.js @@ -25,6 +25,7 @@ const findAllChatsStatusAsync = async ({ open: await LivechatRooms.countAllOpenChatsBetweenDate({ start, end, departmentId }), closed: await LivechatRooms.countAllClosedChatsBetweenDate({ start, end, departmentId }), queued: await LivechatRooms.countAllQueuedChatsBetweenDate({ start, end, departmentId }), + onhold: await LivechatRooms.getOnHoldConversationsBetweenDate(start, end, departmentId), }; }; @@ -193,7 +194,7 @@ const getConversationsMetricsAsync = async ({ utcOffset: user.utcOffset, language: user.language || settings.get('Language') || 'en', }); - const metrics = ['Total_conversations', 'Open_conversations', 'Total_messages']; + const metrics = ['Total_conversations', 'Open_conversations', 'On_Hold_conversations', 'Total_messages']; const visitorsCount = await LivechatVisitors.getVisitorsBetweenDate({ start, end, department: departmentId }).count(); return { totalizers: [ @@ -213,13 +214,20 @@ const findAllChatMetricsByAgentAsync = async ({ } const open = await LivechatRooms.countAllOpenChatsByAgentBetweenDate({ start, end, departmentId }); const closed = await LivechatRooms.countAllClosedChatsByAgentBetweenDate({ start, end, departmentId }); + const onhold = await LivechatRooms.countAllOnHoldChatsByAgentBetweenDate({ start, end, departmentId }); const result = {}; (open || []).forEach((agent) => { - result[agent._id] = { open: agent.chats, closed: 0 }; + result[agent._id] = { open: agent.chats, closed: 0, onhold: 0 }; }); (closed || []).forEach((agent) => { result[agent._id] = { open: result[agent._id] ? result[agent._id].open : 0, closed: agent.chats }; }); + (onhold || []).forEach((agent) => { + result[agent._id] = { + ...result[agent._id], + onhold: agent.chats, + }; + }); return result; }; diff --git a/app/livechat/server/lib/stream/agentStatus.ts b/app/livechat/server/lib/stream/agentStatus.ts index ed46bacbd0f8..12985a42f58d 100644 --- a/app/livechat/server/lib/stream/agentStatus.ts +++ b/app/livechat/server/lib/stream/agentStatus.ts @@ -2,6 +2,9 @@ import { Meteor } from 'meteor/meteor'; import { Livechat } from '../Livechat'; import { settings } from '../../../../settings/server'; +import { Logger } from '../../../../logger/server'; + +const logger = new Logger('AgentStatusWatcher'); export let monitorAgents = false; let actionTimeout = 60000; @@ -64,12 +67,19 @@ export const onlineAgents = { onlineAgents.users.delete(userId); onlineAgents.queue.delete(userId); - if (action === 'close') { - return Livechat.closeOpenChats(userId, comment); - } + try { + if (action === 'close') { + return Livechat.closeOpenChats(userId, comment); + } - if (action === 'forward') { - return Livechat.forwardOpenChats(userId); + if (action === 'forward') { + return Livechat.forwardOpenChats(userId); + } + } catch (e) { + logger.error({ + msg: `Cannot perform action ${ action }`, + err: e, + }); } }), }; diff --git a/app/livechat/server/methods/getInitialData.js b/app/livechat/server/methods/getInitialData.js index 9b7a2f22a705..bac76ce8d49a 100644 --- a/app/livechat/server/methods/getInitialData.js +++ b/app/livechat/server/methods/getInitialData.js @@ -75,7 +75,7 @@ Meteor.methods({ info.offlineUnavailableMessage = initSettings.Livechat_offline_form_unavailable; info.displayOfflineForm = initSettings.Livechat_display_offline_form; info.language = initSettings.Language; - info.videoCall = initSettings.Livechat_videocall_enabled === true && initSettings.Jitsi_Enabled === true; + 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; diff --git a/app/livechat/server/methods/saveIntegration.js b/app/livechat/server/methods/saveIntegration.js index 38288eab9e0d..d9585643783e 100644 --- a/app/livechat/server/methods/saveIntegration.js +++ b/app/livechat/server/methods/saveIntegration.js @@ -3,7 +3,6 @@ import s from 'underscore.string'; import { hasPermission } from '../../../authorization'; import { Settings } from '../../../models/server'; -import { settings } from '../../../settings'; Meteor.methods({ 'livechat:saveIntegration'(values) { @@ -16,39 +15,39 @@ Meteor.methods({ } if (typeof values.Livechat_secret_token !== 'undefined') { - settings.updateValueById('Livechat_secret_token', s.trim(values.Livechat_secret_token)); + Settings.updateValueById('Livechat_secret_token', s.trim(values.Livechat_secret_token)); } if (typeof values.Livechat_webhook_on_start !== 'undefined') { - settings.updateValueById('Livechat_webhook_on_start', !!values.Livechat_webhook_on_start); + Settings.updateValueById('Livechat_webhook_on_start', !!values.Livechat_webhook_on_start); } if (typeof values.Livechat_webhook_on_close !== 'undefined') { - settings.updateValueById('Livechat_webhook_on_close', !!values.Livechat_webhook_on_close); + Settings.updateValueById('Livechat_webhook_on_close', !!values.Livechat_webhook_on_close); } if (typeof values.Livechat_webhook_on_chat_taken !== 'undefined') { - settings.updateValueById('Livechat_webhook_on_chat_taken', !!values.Livechat_webhook_on_chat_taken); + Settings.updateValueById('Livechat_webhook_on_chat_taken', !!values.Livechat_webhook_on_chat_taken); } if (typeof values.Livechat_webhook_on_chat_queued !== 'undefined') { - settings.updateValueById('Livechat_webhook_on_chat_queued', !!values.Livechat_webhook_on_chat_queued); + Settings.updateValueById('Livechat_webhook_on_chat_queued', !!values.Livechat_webhook_on_chat_queued); } if (typeof values.Livechat_webhook_on_forward !== 'undefined') { - settings.updateValueById('Livechat_webhook_on_forward', !!values.Livechat_webhook_on_forward); + Settings.updateValueById('Livechat_webhook_on_forward', !!values.Livechat_webhook_on_forward); } if (typeof values.Livechat_webhook_on_offline_msg !== 'undefined') { - settings.updateValueById('Livechat_webhook_on_offline_msg', !!values.Livechat_webhook_on_offline_msg); + Settings.updateValueById('Livechat_webhook_on_offline_msg', !!values.Livechat_webhook_on_offline_msg); } if (typeof values.Livechat_webhook_on_visitor_message !== 'undefined') { - settings.updateValueById('Livechat_webhook_on_visitor_message', !!values.Livechat_webhook_on_visitor_message); + Settings.updateValueById('Livechat_webhook_on_visitor_message', !!values.Livechat_webhook_on_visitor_message); } if (typeof values.Livechat_webhook_on_agent_message !== 'undefined') { - settings.updateValueById('Livechat_webhook_on_agent_message', !!values.Livechat_webhook_on_agent_message); + Settings.updateValueById('Livechat_webhook_on_agent_message', !!values.Livechat_webhook_on_agent_message); } }, }); diff --git a/app/livechat/server/sendMessageBySMS.js b/app/livechat/server/sendMessageBySMS.js index 6094d1dc62a3..5e3bd1f13342 100644 --- a/app/livechat/server/sendMessageBySMS.js +++ b/app/livechat/server/sendMessageBySMS.js @@ -31,7 +31,7 @@ callbacks.add('afterSaveMessage', function(message, room) { let extraData; if (message.file) { - message = normalizeMessageFileUpload(message); + message = Promise.await(normalizeMessageFileUpload(message)); const { fileUpload, rid, u: { _id: userId } = {} } = message; extraData = Object.assign({}, { rid, userId, fileUpload }); } diff --git a/app/livechat/server/statistics/LivechatAgentActivityMonitor.js b/app/livechat/server/statistics/LivechatAgentActivityMonitor.js index 1066e112f027..2c894afe4f1f 100644 --- a/app/livechat/server/statistics/LivechatAgentActivityMonitor.js +++ b/app/livechat/server/statistics/LivechatAgentActivityMonitor.js @@ -3,7 +3,8 @@ import { Meteor } from 'meteor/meteor'; import { SyncedCron } from 'meteor/littledata:synced-cron'; import { callbacks } from '../../../callbacks/server'; -import { LivechatAgentActivity, Sessions, Users } from '../../../models/server'; +import { LivechatAgentActivity, Users } from '../../../models/server'; +import { Sessions } from '../../../models/server/raw'; const formatDate = (dateTime = new Date()) => ({ date: parseInt(moment(dateTime).format('YYYYMMDD')), @@ -12,7 +13,6 @@ const formatDate = (dateTime = new Date()) => ({ export class LivechatAgentActivityMonitor { constructor() { this._started = false; - this._handleMeteorConnection = this._handleMeteorConnection.bind(this); this._handleAgentStatusChanged = this._handleAgentStatusChanged.bind(this); this._handleUserStatusLivechatChanged = this._handleUserStatusLivechatChanged.bind(this); this._name = 'Livechat Agent Activity Monitor'; @@ -41,7 +41,7 @@ export class LivechatAgentActivityMonitor { return; } this._startMonitoring(); - Meteor.onConnection(this._handleMeteorConnection); + Meteor.onConnection((connection) => this._handleMeteorConnection(connection)); callbacks.add('livechat.agentStatusChanged', this._handleAgentStatusChanged); callbacks.add('livechat.setUserStatusLivechat', this._handleUserStatusLivechatChanged); this._started = true; @@ -75,12 +75,12 @@ export class LivechatAgentActivityMonitor { } } - _handleMeteorConnection(connection) { + async _handleMeteorConnection(connection) { if (!this.isRunning()) { return; } - const session = Sessions.findOne({ sessionId: connection.id }); + const session = await Sessions.findOne({ sessionId: connection.id }); if (!session) { return; } diff --git a/app/mailer/server/api.ts b/app/mailer/server/api.ts index fe6c593e18d3..c59cd2151fd3 100644 --- a/app/mailer/server/api.ts +++ b/app/mailer/server/api.ts @@ -132,7 +132,7 @@ settings.watchMultiple(['Email_Header', 'Email_Footer'], () => { export const rfcMailPatternWithName = /^(?:.*<)?([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)(?:>?)$/; -export const checkAddressFormat = (from: string): boolean => rfcMailPatternWithName.test(from); +export const checkAddressFormat = (adresses: string | string[]): boolean => ([] as string[]).concat(adresses).every((address) => rfcMailPatternWithName.test(address)); export const sendNoWrap = ({ to, @@ -152,7 +152,7 @@ export const sendNoWrap = ({ headers?: string; }): void => { if (!checkAddressFormat(to)) { - return; + throw new Meteor.Error('invalid email'); } if (!text) { diff --git a/app/mailer/tests/api.spec.ts b/app/mailer/tests/api.spec.ts index 4c858c1b23e5..5bb1397c62e7 100644 --- a/app/mailer/tests/api.spec.ts +++ b/app/mailer/tests/api.spec.ts @@ -1,5 +1,4 @@ -/* eslint-env mocha */ -import assert from 'assert'; +import { expect } from 'chai'; import { replaceVariables } from '../server/replaceVariables'; @@ -13,55 +12,55 @@ describe('Mailer-API', function() { describe('single key', function functionName() { it(`should be equal to test ${ i18n.key }`, () => { - assert.strictEqual(`test ${ i18n.key }`, replaceVariables('test {key}', (_match, key) => i18n[key])); + expect(`test ${ i18n.key }`).to.be.equal(replaceVariables('test {key}', (_match, key) => i18n[key])); }); }); describe('multiple keys', function functionName() { it(`should be equal to test ${ i18n.key } and ${ i18n.key }`, () => { - assert.strictEqual(`test ${ i18n.key } and ${ i18n.key }`, replaceVariables('test {key} and {key}', (_match, key) => i18n[key])); + expect(`test ${ i18n.key } and ${ i18n.key }`).to.be.equal(replaceVariables('test {key} and {key}', (_match, key) => i18n[key])); }); }); describe('key with a trailing space', function functionName() { it(`should be equal to test ${ i18n.key }`, () => { - assert.strictEqual(`test ${ i18n.key }`, replaceVariables('test {key }', (_match, key) => i18n[key])); + expect(`test ${ i18n.key }`).to.be.equal(replaceVariables('test {key }', (_match, key) => i18n[key])); }); }); describe('key with a leading space', function functionName() { it(`should be equal to test ${ i18n.key }`, () => { - assert.strictEqual(`test ${ i18n.key }`, replaceVariables('test { key}', (_match, key) => i18n[key])); + expect(`test ${ i18n.key }`).to.be.equal(replaceVariables('test { key}', (_match, key) => i18n[key])); }); }); describe('key with leading and trailing spaces', function functionName() { it(`should be equal to test ${ i18n.key }`, () => { - assert.strictEqual(`test ${ i18n.key }`, replaceVariables('test { key }', (_match, key) => i18n[key])); + expect(`test ${ i18n.key }`).to.be.equal(replaceVariables('test { key }', (_match, key) => i18n[key])); }); }); describe('key with multiple words', function functionName() { it(`should be equal to test ${ i18n.key }`, () => { - assert.strictEqual(`test ${ i18n.key }`, replaceVariables('test {key ignore}', (_match, key) => i18n[key])); + expect(`test ${ i18n.key }`).to.be.equal(replaceVariables('test {key ignore}', (_match, key) => i18n[key])); }); }); describe('key with multiple opening brackets', function functionName() { it(`should be equal to test {${ i18n.key }`, () => { - assert.strictEqual(`test {${ i18n.key }`, replaceVariables('test {{key}', (_match, key) => i18n[key])); + expect(`test {${ i18n.key }`).to.be.equal(replaceVariables('test {{key}', (_match, key) => i18n[key])); }); }); describe('key with multiple closing brackets', function functionName() { it(`should be equal to test ${ i18n.key }}`, () => { - assert.strictEqual(`test ${ i18n.key }}`, replaceVariables('test {key}}', (_match, key) => i18n[key])); + expect(`test ${ i18n.key }}`).to.be.equal(replaceVariables('test {key}}', (_match, key) => i18n[key])); }); }); describe('key with multiple opening and closing brackets', function functionName() { it(`should be equal to test {${ i18n.key }}`, () => { - assert.strictEqual(`test {${ i18n.key }}`, replaceVariables('test {{key}}', (_match, key) => i18n[key])); + expect(`test {${ i18n.key }}`).to.be.equal(replaceVariables('test {{key}}', (_match, key) => i18n[key])); }); }); }); diff --git a/app/markdown/tests/client.tests.js b/app/markdown/tests/client.tests.js index 83567dff6323..8d741f696ddb 100644 --- a/app/markdown/tests/client.tests.js +++ b/app/markdown/tests/client.tests.js @@ -1,8 +1,6 @@ -/* eslint-env mocha */ -import 'babel-polyfill'; -import assert from 'assert'; - import './client.mocks.js'; + +import { expect } from 'chai'; import { escapeHTML } from '@rocket.chat/string-helpers'; import { original } from '../lib/parser/original/original'; @@ -375,7 +373,7 @@ const blockcodeFiltered = { 'Here```code```lies': 'Herecodelies', }; -const defaultObjectTest = (result, object, objectKey) => assert.equal(result.html, object[objectKey]); +const defaultObjectTest = (result, object, objectKey) => expect(result.html).to.be.equal(object[objectKey]); const testObject = (object, parser = original, test = defaultObjectTest) => { Object.keys(object).forEach((objectKey) => { @@ -435,7 +433,7 @@ describe('Filtered', function() { describe('blockcodeFilter', () => testObject(blockcodeFiltered, filtered)); }); -// describe.only('Marked', function() { +// describe('Marked', function() { // describe('Bold', () => testObject(bold, marked)); // describe('Italic', () => testObject(italic, marked)); diff --git a/app/mentions/tests/client.tests.js b/app/mentions/tests/client.tests.js index 5854ec14ba6a..e90249dcf23c 100644 --- a/app/mentions/tests/client.tests.js +++ b/app/mentions/tests/client.tests.js @@ -1,11 +1,9 @@ -/* eslint-env mocha */ -import 'babel-polyfill'; -import assert from 'assert'; +import { expect } from 'chai'; import { MentionsParser } from '../lib/MentionsParser'; let mentionsParser; -beforeEach(function functionName() { +beforeEach(() => { mentionsParser = new MentionsParser({ pattern: '[0-9a-zA-Z-_.]+', me: () => 'me', @@ -17,15 +15,15 @@ describe('Mention', function() { const regexp = '[0-9a-zA-Z-_.]+'; beforeEach(() => { mentionsParser.pattern = () => regexp; }); - describe('by function', function functionName() { + describe('by function', () => { it(`should be equal to ${ regexp }`, () => { - assert.equal(regexp, mentionsParser.pattern); + expect(regexp).to.be.equal(mentionsParser.pattern); }); }); - describe('by const', function functionName() { + describe('by const', () => { it(`should be equal to ${ regexp }`, () => { - assert.equal(regexp, mentionsParser.pattern); + expect(regexp).to.be.equal(mentionsParser.pattern); }); }); }); @@ -33,15 +31,15 @@ describe('Mention', function() { describe('get useRealName', () => { beforeEach(() => { mentionsParser.useRealName = () => true; }); - describe('by function', function functionName() { + describe('by function', () => { it('should be true', () => { - assert.equal(true, mentionsParser.useRealName); + expect(true).to.be.equal(mentionsParser.useRealName); }); }); - describe('by const', function functionName() { + describe('by const', () => { it('should be true', () => { - assert.equal(true, mentionsParser.useRealName); + expect(true).to.be.equal(mentionsParser.useRealName); }); }); }); @@ -49,24 +47,24 @@ describe('Mention', function() { describe('get me', () => { const me = 'me'; - describe('by function', function functionName() { + describe('by function', () => { beforeEach(() => { mentionsParser.me = () => me; }); it(`should be equal to ${ me }`, () => { - assert.equal(me, mentionsParser.me); + expect(me).to.be.equal(mentionsParser.me); }); }); - describe('by const', function functionName() { + describe('by const', () => { beforeEach(() => { mentionsParser.me = me; }); it(`should be equal to ${ me }`, () => { - assert.equal(me, mentionsParser.me); + expect(me).to.be.equal(mentionsParser.me); }); }); }); - describe('getUserMentions', function functionName() { + describe('getUserMentions', () => { describe('for simple text, no mentions', () => { const result = []; [ @@ -75,7 +73,7 @@ describe('Mention', function() { ] .forEach((text) => { it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { - assert.deepEqual(result, mentionsParser.getUserMentions(text)); + expect(result).to.be.deep.equal(mentionsParser.getUserMentions(text)); }); }); }); @@ -93,20 +91,20 @@ describe('Mention', function() { ] .forEach((text) => { it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { - assert.deepEqual(result, mentionsParser.getUserMentions(text)); + expect(result).to.be.deep.equal(mentionsParser.getUserMentions(text)); }); }); it.skip('should return without the "." from "@rocket.cat."', () => { - assert.deepEqual(result, mentionsParser.getUserMentions('@rocket.cat.')); + expect(result).to.be.deep.equal(mentionsParser.getUserMentions('@rocket.cat.')); }); it.skip('should return without the "_" from "@rocket.cat_"', () => { - assert.deepEqual(result, mentionsParser.getUserMentions('@rocket.cat_')); + expect(result).to.be.deep.equal(mentionsParser.getUserMentions('@rocket.cat_')); }); it.skip('should return without the "-" from "@rocket.cat-"', () => { - assert.deepEqual(result, mentionsParser.getUserMentions('@rocket.cat-')); + expect(result).to.be.deep.equal(mentionsParser.getUserMentions('@rocket.cat-')); }); }); @@ -121,13 +119,13 @@ describe('Mention', function() { ] .forEach((text) => { it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { - assert.deepEqual(result, mentionsParser.getUserMentions(text)); + expect(result).to.be.deep.equal(mentionsParser.getUserMentions(text)); }); }); }); }); - describe('getChannelMentions', function functionName() { + describe('getChannelMentions', () => { describe('for simple text, no mentions', () => { const result = []; [ @@ -136,7 +134,7 @@ describe('Mention', function() { ] .forEach((text) => { it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { - assert.deepEqual(result, mentionsParser.getChannelMentions(text)); + expect(result).to.be.deep.equal(mentionsParser.getChannelMentions(text)); }); }); }); @@ -151,20 +149,20 @@ describe('Mention', function() { 'hello #general, how are you?', ].forEach((text) => { it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { - assert.deepEqual(result, mentionsParser.getChannelMentions(text)); + expect(result).to.be.deep.equal(mentionsParser.getChannelMentions(text)); }); }); it.skip('should return without the "." from "#general."', () => { - assert.deepEqual(result, mentionsParser.getUserMentions('#general.')); + expect(result).to.be.deep.equal(mentionsParser.getUserMentions('#general.')); }); it.skip('should return without the "_" from "#general_"', () => { - assert.deepEqual(result, mentionsParser.getUserMentions('#general_')); + expect(result).to.be.deep.equal(mentionsParser.getUserMentions('#general_')); }); it.skip('should return without the "-" from "#general."', () => { - assert.deepEqual(result, mentionsParser.getUserMentions('#general-')); + expect(result).to.be.deep.equal(mentionsParser.getUserMentions('#general-')); }); }); @@ -178,7 +176,7 @@ describe('Mention', function() { 'hello #general #other, how are you?', ].forEach((text) => { it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { - assert.deepEqual(result, mentionsParser.getChannelMentions(text)); + expect(result).to.be.deep.equal(mentionsParser.getChannelMentions(text)); }); }); }); @@ -189,7 +187,7 @@ describe('Mention', function() { 'http://localhost/#general', ].forEach((text) => { it(`should return nothing from "${ text }"`, () => { - assert.deepEqual(result, mentionsParser.getChannelMentions(text)); + expect(result).to.be.deep.equal(mentionsParser.getChannelMentions(text)); }); }); }); @@ -200,7 +198,7 @@ describe('Mention', function() { 'http://localhost/#general #general', ].forEach((text) => { it(`should return "${ JSON.stringify(result) }" from "${ text }"`, () => { - assert.deepEqual(result, mentionsParser.getChannelMentions(text)); + expect(result).to.be.deep.equal(mentionsParser.getChannelMentions(text)); }); }); }); @@ -216,29 +214,29 @@ describe('replace methods', function() { describe('replaceUsers', () => { it('should render for @all', () => { const result = mentionsParser.replaceUsers('@all', message, 'me'); - assert.equal(result, 'all'); + expect(result).to.be.equal('all'); }); const str2 = 'rocket.cat'; it(`should render for "@${ str2 }"`, () => { const result = mentionsParser.replaceUsers(`@${ str2 }`, message, 'me'); - assert.equal(result, `${ str2 }`); + expect(result).to.be.equal(`${ str2 }`); }); it(`should render for "hello ${ str2 }"`, () => { const result = mentionsParser.replaceUsers(`hello @${ str2 }`, message, 'me'); - assert.equal(result, `hello ${ str2 }`); + expect(result).to.be.equal(`hello ${ str2 }`); }); it('should render for unknow/private user "hello @unknow"', () => { const result = mentionsParser.replaceUsers('hello @unknow', message, 'me'); - assert.equal(result, 'hello @unknow'); + expect(result).to.be.equal('hello @unknow'); }); it('should render for me', () => { const result = mentionsParser.replaceUsers('hello @me', message, 'me'); - assert.equal(result, 'hello me'); + expect(result).to.be.equal('hello me'); }); }); @@ -249,7 +247,7 @@ describe('replace methods', function() { it('should render for @all', () => { const result = mentionsParser.replaceUsers('@all', message, 'me'); - assert.equal(result, 'all'); + expect(result).to.be.equal('all'); }); const str2 = 'rocket.cat'; @@ -257,12 +255,12 @@ describe('replace methods', function() { it(`should render for "@${ str2 }"`, () => { const result = mentionsParser.replaceUsers(`@${ str2 }`, message, 'me'); - assert.equal(result, `${ str2Name }`); + expect(result).to.be.equal(`${ str2Name }`); }); it(`should render for "hello @${ str2 }"`, () => { const result = mentionsParser.replaceUsers(`hello @${ str2 }`, message, 'me'); - assert.equal(result, `hello ${ str2Name }`); + expect(result).to.be.equal(`hello ${ str2Name }`); }); const specialchars = 'specialchars'; @@ -270,46 +268,46 @@ describe('replace methods', function() { it(`should escape special characters in "hello @${ specialchars }"`, () => { const result = mentionsParser.replaceUsers(`hello @${ specialchars }`, message, 'me'); - assert.equal(result, `hello ${ specialcharsName }`); + expect(result).to.be.equal(`hello ${ specialcharsName }`); }); it(`should render for "hello
@${ str2 }
"`, () => { const result = mentionsParser.replaceUsers(`hello
@${ str2 }
`, message, 'me'); - assert.equal(result, `hello
${ str2Name }
`); + expect(result).to.be.equal(`hello
${ str2Name }
`); }); it('should render for unknow/private user "hello @unknow"', () => { const result = mentionsParser.replaceUsers('hello @unknow', message, 'me'); - assert.equal(result, 'hello @unknow'); + expect(result).to.be.equal('hello @unknow'); }); it('should render for me', () => { const result = mentionsParser.replaceUsers('hello @me', message, 'me'); - assert.equal(result, 'hello Me'); + expect(result).to.be.equal('hello Me'); }); }); describe('replaceChannels', () => { it('should render for #general', () => { const result = mentionsParser.replaceChannels('#general', message); - assert.equal('#general', result); + expect('<).to.be.equal(class="mention-link mention-link--room" data-channel="42">#general', result); }); const str2 = '#rocket.cat'; it(`should render for ${ str2 }`, () => { const result = mentionsParser.replaceChannels(str2, message); - assert.equal(result, `${ str2 }`); + expect(result).to.be.equal(`${ str2 }`); }); it(`should render for "hello ${ str2 }"`, () => { const result = mentionsParser.replaceChannels(`hello ${ str2 }`, message); - assert.equal(result, `hello ${ str2 }`); + expect(result).to.be.equal(`hello ${ str2 }`); }); it('should render for unknow/private channel "hello #unknow"', () => { const result = mentionsParser.replaceChannels('hello #unknow', message); - assert.equal(result, 'hello #unknow'); + expect(result).to.be.equal('hello #unknow'); }); }); @@ -317,25 +315,25 @@ describe('replace methods', function() { it('should render for #general', () => { message.html = '#general'; const result = mentionsParser.parse(message, 'me'); - assert.equal(result.html, '#general'); + expect(result.html).to.be.equal('#general'); }); it('should render for "#general and @rocket.cat', () => { message.html = '#general and @rocket.cat'; const result = mentionsParser.parse(message, 'me'); - assert.equal(result.html, '#general and rocket.cat'); + expect(result.html).to.be.equal('#general and rocket.cat'); }); it('should render for "', () => { message.html = ''; const result = mentionsParser.parse(message, 'me'); - assert.equal(result.html, ''); + expect(result.html).to.be.equal(''); }); it('should render for "simple text', () => { message.html = 'simple text'; const result = mentionsParser.parse(message, 'me'); - assert.equal(result.html, 'simple text'); + expect(result.html).to.be.equal('simple text'); }); }); @@ -347,25 +345,25 @@ describe('replace methods', function() { it('should render for #general', () => { message.html = '#general'; const result = mentionsParser.parse(message, 'me'); - assert.equal(result.html, '#general'); + expect(result.html).to.be.equal('#general'); }); it('should render for "#general and @rocket.cat', () => { message.html = '#general and @rocket.cat'; const result = mentionsParser.parse(message, 'me'); - assert.equal(result.html, '#general and Rocket.Cat'); + expect(result.html).to.be.equal('#general and Rocket.Cat'); }); it('should render for "', () => { message.html = ''; const result = mentionsParser.parse(message, 'me'); - assert.equal(result.html, ''); + expect(result.html).to.be.equal(''); }); it('should render for "simple text', () => { message.html = 'simple text'; const result = mentionsParser.parse(message, 'me'); - assert.equal(result.html, 'simple text'); + expect(result.html).to.be.equal('simple text'); }); }); }); diff --git a/app/mentions/tests/server.tests.js b/app/mentions/tests/server.tests.js index 30bc07564984..a1a77bac3058 100644 --- a/app/mentions/tests/server.tests.js +++ b/app/mentions/tests/server.tests.js @@ -1,6 +1,4 @@ -/* eslint-env mocha */ -import 'babel-polyfill'; -import assert from 'assert'; +import { expect } from 'chai'; import MentionsServer from '../server/Mentions'; @@ -43,7 +41,7 @@ describe('Mention Server', () => { }; const expected = []; const result = mention.getUsersByMentions(message); - assert.deepEqual(expected, result); + expect(expected).to.be.deep.equal(result); }); }); describe('for one user', () => { @@ -69,7 +67,7 @@ describe('Mention Server', () => { username: 'all', }]; const result = mention.getUsersByMentions(message); - assert.deepEqual(expected, result); + expect(expected).to.be.deep.equal(result); }); it('should return "here"', () => { const message = { @@ -80,7 +78,7 @@ describe('Mention Server', () => { username: 'here', }]; const result = mention.getUsersByMentions(message); - assert.deepEqual(expected, result); + expect(expected).to.be.deep.equal(result); }); it('should return "rocket.cat"', () => { const message = { @@ -91,7 +89,7 @@ describe('Mention Server', () => { username: 'rocket.cat', }]; const result = mention.getUsersByMentions(message); - assert.deepEqual(expected, result); + expect(expected).to.be.deep.equal(result); }); }); describe('for two user', () => { @@ -107,7 +105,7 @@ describe('Mention Server', () => { username: 'here', }]; const result = mention.getUsersByMentions(message); - assert.deepEqual(expected, result); + expect(expected).to.be.deep.equal(result); }); it('should return "here and rocket.cat"', () => { const message = { @@ -121,7 +119,7 @@ describe('Mention Server', () => { username: 'rocket.cat', }]; const result = mention.getUsersByMentions(message); - assert.deepEqual(expected, result); + expect(expected).to.be.deep.equal(result); }); it('should return "here, rocket.cat, jon"', () => { @@ -139,7 +137,7 @@ describe('Mention Server', () => { username: 'jon', }]; const result = mention.getUsersByMentions(message); - assert.deepEqual(expected, result); + expect(expected).to.be.deep.equal(result); }); }); @@ -150,7 +148,7 @@ describe('Mention Server', () => { }; const expected = []; const result = mention.getUsersByMentions(message); - assert.deepEqual(expected, result); + expect(expected).to.be.deep.equal(result); }); }); }); @@ -164,7 +162,7 @@ describe('Mention Server', () => { name: 'general', }]; const result = mention.getChannelbyMentions(message); - assert.deepEqual(result, expected); + expect(result).to.be.deep.equal(expected); }); it('should return nothing"', () => { const message = { @@ -172,7 +170,7 @@ describe('Mention Server', () => { }; const expected = []; const result = mention.getChannelbyMentions(message); - assert.deepEqual(result, expected); + expect(result).to.be.deep.equal(expected); }); }); describe('execute', () => { @@ -185,7 +183,7 @@ describe('Mention Server', () => { name: 'general', }]; const result = mention.getChannelbyMentions(message); - assert.deepEqual(result, expected); + expect(result).to.be.deep.equal(expected); }); it('should return nothing"', () => { const message = { @@ -197,7 +195,7 @@ describe('Mention Server', () => { channels: [], }; const result = mention.execute(message); - assert.deepEqual(result, expected); + expect(result).to.be.deep.equal(expected); }); }); @@ -207,13 +205,13 @@ describe('Mention Server', () => { describe('constant', () => { it('should return the informed value', () => { mention.messageMaxAll = 4; - assert.deepEqual(mention.messageMaxAll, 4); + expect(mention.messageMaxAll).to.be.deep.equal(4); }); }); describe('function', () => { it('should return the informed value', () => { mention.messageMaxAll = () => 4; - assert.deepEqual(mention.messageMaxAll, 4); + expect(mention.messageMaxAll).to.be.deep.equal(4); }); }); }); @@ -222,13 +220,13 @@ describe('Mention Server', () => { describe('constant', () => { it('should return the informed value', () => { mention.getUsers = 4; - assert.deepEqual(mention.getUsers(), 4); + expect(mention.getUsers()).to.be.deep.equal(4); }); }); describe('function', () => { it('should return the informed value', () => { mention.getUsers = () => 4; - assert.deepEqual(mention.getUsers(), 4); + expect(mention.getUsers()).to.be.deep.equal(4); }); }); }); @@ -237,13 +235,13 @@ describe('Mention Server', () => { describe('constant', () => { it('should return the informed value', () => { mention.getChannels = 4; - assert.deepEqual(mention.getChannels(), 4); + expect(mention.getChannels()).to.be.deep.equal(4); }); }); describe('function', () => { it('should return the informed value', () => { mention.getChannels = () => 4; - assert.deepEqual(mention.getChannels(), 4); + expect(mention.getChannels()).to.be.deep.equal(4); }); }); }); @@ -252,13 +250,13 @@ describe('Mention Server', () => { describe('constant', () => { it('should return the informed value', () => { mention.getChannel = true; - assert.deepEqual(mention.getChannel(), true); + expect(mention.getChannel()).to.be.deep.equal(true); }); }); describe('function', () => { it('should return the informed value', () => { mention.getChannel = () => true; - assert.deepEqual(mention.getChannel(), true); + expect(mention.getChannel()).to.be.deep.equal(true); }); }); }); diff --git a/app/meteor-accounts-saml/server/lib/SAML.ts b/app/meteor-accounts-saml/server/lib/SAML.ts index 5de64393352a..a0c60ff1d3a5 100644 --- a/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/app/meteor-accounts-saml/server/lib/SAML.ts @@ -8,7 +8,8 @@ import fiber from 'fibers'; import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; import { settings } from '../../../settings/server'; -import { Users, Rooms, CredentialTokens } from '../../../models/server'; +import { Users, Rooms } from '../../../models/server'; +import { CredentialTokens } from '../../../models/server/raw'; import { IUser } from '../../../../definition/IUser'; import { IIncomingMessage } from '../../../../definition/IIncomingMessage'; import { saveUserIdentity, createRoom, generateUsernameSuggestion, addUserToRoom } from '../../../lib/server/functions'; @@ -55,20 +56,20 @@ export class SAML { } } - public static hasCredential(credentialToken: string): boolean { - return CredentialTokens.findOneById(credentialToken) != null; + public static async hasCredential(credentialToken: string): Promise { + return await CredentialTokens.findOneNotExpiredById(credentialToken) != null; } - public static retrieveCredential(credentialToken: string): Record | undefined { + public static async retrieveCredential(credentialToken: string): Promise | undefined> { // The credentialToken in all these functions corresponds to SAMLs inResponseTo field and is mandatory to check. - const data = CredentialTokens.findOneById(credentialToken); + const data = await CredentialTokens.findOneNotExpiredById(credentialToken); if (data) { return data.userInfo; } } - public static storeCredential(credentialToken: string, loginResult: object): void { - CredentialTokens.create(credentialToken, loginResult); + public static async storeCredential(credentialToken: string, loginResult: {profile: Record}): Promise { + await CredentialTokens.create(credentialToken, loginResult); } public static insertOrUpdateSAMLUser(userObject: ISAMLUser): {userId: string; token: string} { @@ -380,7 +381,7 @@ export class SAML { private static processValidateAction(req: IIncomingMessage, res: ServerResponse, service: IServiceProviderOptions, _samlObject: ISAMLAction): void { const serviceProvider = new SAMLServiceProvider(service); SAMLUtils.relayState = req.body.RelayState; - serviceProvider.validateResponse(req.body.SAMLResponse, (err, profile/* , loggedOut*/) => { + serviceProvider.validateResponse(req.body.SAMLResponse, async (err, profile/* , loggedOut*/) => { try { if (err) { SAMLUtils.error(err); @@ -400,7 +401,7 @@ export class SAML { profile, }; - this.storeCredential(credentialToken, loginResult); + await this.storeCredential(credentialToken, loginResult); const url = `${ Meteor.absoluteUrl('home') }?saml_idp_credentialToken=${ credentialToken }`; res.writeHead(302, { Location: url, diff --git a/app/meteor-accounts-saml/server/lib/parsers/Response.ts b/app/meteor-accounts-saml/server/lib/parsers/Response.ts index 89bbc0758093..f92ba7fedef2 100644 --- a/app/meteor-accounts-saml/server/lib/parsers/Response.ts +++ b/app/meteor-accounts-saml/server/lib/parsers/Response.ts @@ -176,7 +176,7 @@ export class ResponseParser { if (typeof encAssertion !== 'undefined') { const options = { key: this.serviceProviderOptions.privateKey }; const encData = encAssertion.getElementsByTagNameNS('*', 'EncryptedData')[0]; - xmlenc.decrypt(encData, options, function(err: Error, result: string) { + xmlenc.decrypt(encData, options, function(err, result) { if (err) { SAMLUtils.error(err); } @@ -318,7 +318,7 @@ export class ResponseParser { if (typeof encSubject !== 'undefined') { const options = { key: this.serviceProviderOptions.privateKey }; - xmlenc.decrypt(encSubject.getElementsByTagNameNS('*', 'EncryptedData')[0], options, function(err: Error, result: string) { + xmlenc.decrypt(encSubject.getElementsByTagNameNS('*', 'EncryptedData')[0], options, (err, result) => { if (err) { SAMLUtils.error(err); } diff --git a/app/meteor-accounts-saml/server/loginHandler.ts b/app/meteor-accounts-saml/server/loginHandler.ts index 6b73c7f386ec..edb58716d974 100644 --- a/app/meteor-accounts-saml/server/loginHandler.ts +++ b/app/meteor-accounts-saml/server/loginHandler.ts @@ -17,7 +17,7 @@ Accounts.registerLoginHandler('saml', function(loginRequest) { return undefined; } - const loginResult = SAML.retrieveCredential(loginRequest.credentialToken); + const loginResult = Promise.await(SAML.retrieveCredential(loginRequest.credentialToken)); SAMLUtils.log({ msg: 'RESULT', loginResult }); if (!loginResult) { diff --git a/app/meteor-accounts-saml/tests/server.tests.ts b/app/meteor-accounts-saml/tests/server.tests.ts index d8ca3ba2f702..2b64134e22a9 100644 --- a/app/meteor-accounts-saml/tests/server.tests.ts +++ b/app/meteor-accounts-saml/tests/server.tests.ts @@ -1,7 +1,4 @@ -/* eslint-env mocha */ -import 'babel-polyfill'; - -import chai from 'chai'; +import { expect } from 'chai'; import '../../lib/tests/server.mocks.js'; import { AuthorizeRequest } from '../server/lib/generators/AuthorizeRequest'; @@ -38,9 +35,6 @@ import { privateKeyCert, privateKey, } from './data'; -import '../../../definition/xml-encryption'; - -const { expect } = chai; describe('SAML', () => { describe('[AuthorizeRequest]', () => { diff --git a/app/metrics/server/lib/collectMetrics.js b/app/metrics/server/lib/collectMetrics.js index 98267f9ca855..f72437df2f04 100644 --- a/app/metrics/server/lib/collectMetrics.js +++ b/app/metrics/server/lib/collectMetrics.js @@ -10,7 +10,7 @@ import { Facts } from 'meteor/facts-base'; import { Info, getOplogInfo } from '../../../utils/server'; import { getControl } from '../../../../server/lib/migrations'; import { settings } from '../../../settings/server'; -import { Statistics } from '../../../models/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'; @@ -42,7 +42,7 @@ const setPrometheusData = async () => { const oplogQueue = getOplogInfo().mongo._oplogHandle?._entryQueue?.length || 0; metrics.oplogQueue.set(oplogQueue); - const statistics = Statistics.findLast(); + const statistics = await Statistics.findLast(); if (!statistics) { return; } diff --git a/app/models/server/index.js b/app/models/server/index.js index 8c9094d654ef..5507cf53f0aa 100644 --- a/app/models/server/index.js +++ b/app/models/server/index.js @@ -1,31 +1,11 @@ import { Base } from './models/_Base'; import { BaseDb } from './models/_BaseDb'; -import Avatars from './models/Avatars'; -import ExportOperations from './models/ExportOperations'; import Messages from './models/Messages'; -import Reports from './models/Reports'; import Rooms from './models/Rooms'; import Settings from './models/Settings'; import Subscriptions from './models/Subscriptions'; -import Uploads from './models/Uploads'; -import UserDataFiles from './models/UserDataFiles'; import Users from './models/Users'; -import Sessions from './models/Sessions'; -import Statistics from './models/Statistics'; -import Permissions from './models/Permissions'; -import Roles from './models/Roles'; -import CustomSounds from './models/CustomSounds'; -import CustomUserStatus from './models/CustomUserStatus'; import Imports from './models/Imports'; -import Integrations from './models/Integrations'; -import IntegrationHistory from './models/IntegrationHistory'; -import Invites from './models/Invites'; -import CredentialTokens from './models/CredentialTokens'; -import EmojiCustom from './models/EmojiCustom'; -import OAuthApps from './models/OAuthApps'; -import OEmbedCache from './models/OEmbedCache'; -import SmarshHistory from './models/SmarshHistory'; -import WebdavAccounts from './models/WebdavAccounts'; import LivechatCustomField from './models/LivechatCustomField'; import LivechatDepartment from './models/LivechatDepartment'; import LivechatDepartmentAgents from './models/LivechatDepartmentAgents'; @@ -35,50 +15,24 @@ import LivechatTrigger from './models/LivechatTrigger'; import LivechatVisitors from './models/LivechatVisitors'; import LivechatAgentActivity from './models/LivechatAgentActivity'; import LivechatInquiry from './models/LivechatInquiry'; -import ReadReceipts from './models/ReadReceipts'; import LivechatExternalMessage from './models/LivechatExternalMessages'; import OmnichannelQueue from './models/OmnichannelQueue'; -import Analytics from './models/Analytics'; -import EmailInbox from './models/EmailInbox'; import ImportData from './models/ImportData'; export { AppsLogsModel } from './models/apps-logs-model'; export { AppsPersistenceModel } from './models/apps-persistence-model'; export { AppsModel } from './models/apps-model'; -export { FederationDNSCache } from './models/FederationDNSCache'; export { FederationRoomEvents } from './models/FederationRoomEvents'; -export { FederationKeys } from './models/FederationKeys'; -export { FederationServers } from './models/FederationServers'; export { Base, BaseDb, - Avatars, - ExportOperations, Messages, - Reports, Rooms, Settings, Subscriptions, - Uploads, - UserDataFiles, Users, - Sessions, - Statistics, - Permissions, - Roles, - CustomSounds, - CustomUserStatus, Imports, - Integrations, - IntegrationHistory, - Invites, - CredentialTokens, - EmojiCustom, - OAuthApps, - OEmbedCache, - SmarshHistory, - WebdavAccounts, LivechatCustomField, LivechatDepartment, LivechatDepartmentAgents, @@ -87,11 +41,8 @@ export { LivechatTrigger, LivechatVisitors, LivechatAgentActivity, - ReadReceipts, LivechatExternalMessage, LivechatInquiry, - Analytics, OmnichannelQueue, - EmailInbox, ImportData, }; diff --git a/app/models/server/models/Analytics.js b/app/models/server/models/Analytics.js deleted file mode 100644 index c521fda8923e..000000000000 --- a/app/models/server/models/Analytics.js +++ /dev/null @@ -1,11 +0,0 @@ -import { Base } from './_Base'; - -export class Analytics extends Base { - constructor() { - super('analytics'); - this.tryEnsureIndex({ date: 1 }); - this.tryEnsureIndex({ 'room._id': 1, date: 1 }, { unique: true }); - } -} - -export default new Analytics(); diff --git a/app/models/server/models/Avatars.js b/app/models/server/models/Avatars.js deleted file mode 100644 index b0e7e8cb5da1..000000000000 --- a/app/models/server/models/Avatars.js +++ /dev/null @@ -1,92 +0,0 @@ -import _ from 'underscore'; -import s from 'underscore.string'; -import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; - -import { Base } from './_Base'; - -export class Avatars extends Base { - constructor() { - super('avatars'); - - this.model.before.insert((userId, doc) => { - doc.instanceId = InstanceStatus.id(); - }); - - this.tryEnsureIndex({ name: 1 }, { sparse: true }); - this.tryEnsureIndex({ rid: 1 }, { sparse: true }); - } - - insertAvatarFileInit(name, userId, store, file, extra) { - const fileData = { - _id: name, - name, - userId, - store, - complete: false, - uploading: true, - progress: 0, - extension: s.strRightBack(file.name, '.'), - uploadedAt: new Date(), - }; - - _.extend(fileData, file, extra); - - return this.insertOrUpsert(fileData); - } - - updateFileComplete(fileId, userId, file) { - if (!fileId) { - return; - } - - const filter = { - _id: fileId, - userId, - }; - - const update = { - $set: { - complete: true, - uploading: false, - progress: 1, - }, - }; - - update.$set = _.extend(file, update.$set); - - if (this.model.direct && this.model.direct.update) { - return this.model.direct.update(filter, update); - } - return this.update(filter, update); - } - - findOneByName(name) { - return this.findOne({ name }); - } - - findOneByRoomId(rid) { - return this.findOne({ rid }); - } - - updateFileNameById(fileId, name) { - const filter = { _id: fileId }; - const update = { - $set: { - name, - }, - }; - if (this.model.direct && this.model.direct.update) { - return this.model.direct.update(filter, update); - } - return this.update(filter, update); - } - - deleteFile(fileId) { - if (this.model.direct && this.model.direct.remove) { - return this.model.direct.remove({ _id: fileId }); - } - return this.remove({ _id: fileId }); - } -} - -export default new Avatars(); diff --git a/app/models/server/models/CredentialTokens.js b/app/models/server/models/CredentialTokens.js deleted file mode 100644 index 7659538e032e..000000000000 --- a/app/models/server/models/CredentialTokens.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Base } from './_Base'; - -export class CredentialTokens extends Base { - constructor() { - super('credential_tokens'); - - this.tryEnsureIndex({ expireAt: 1 }, { sparse: 1, expireAfterSeconds: 0 }); - } - - create(_id, userInfo) { - const validForMilliseconds = 60000; // Valid for 60 seconds - const token = { - _id, - userInfo, - expireAt: new Date(Date.now() + validForMilliseconds), - }; - - this.insert(token); - return token; - } - - findOneById(_id) { - const query = { - _id, - expireAt: { $gt: new Date() }, - }; - - return this.findOne(query); - } -} - -export default new CredentialTokens(); diff --git a/app/models/server/models/CustomSounds.js b/app/models/server/models/CustomSounds.js deleted file mode 100644 index b9971b954229..000000000000 --- a/app/models/server/models/CustomSounds.js +++ /dev/null @@ -1,56 +0,0 @@ -import { Base } from './_Base'; - -class CustomSounds extends Base { - constructor() { - super('custom_sounds'); - - this.tryEnsureIndex({ name: 1 }); - } - - // find one - findOneById(_id, options) { - return this.findOne(_id, options); - } - - // find - findByName(name, options) { - const query = { - name, - }; - - return this.find(query, options); - } - - findByNameExceptId(name, except, options) { - const query = { - _id: { $nin: [except] }, - name, - }; - - return this.find(query, options); - } - - // update - setName(_id, name) { - const update = { - $set: { - name, - }, - }; - - return this.update({ _id }, update); - } - - // INSERT - create(data) { - return this.insert(data); - } - - - // REMOVE - removeById(_id) { - return this.remove(_id); - } -} - -export default new CustomSounds(); diff --git a/app/models/server/models/CustomUserStatus.js b/app/models/server/models/CustomUserStatus.js deleted file mode 100644 index eb3a586da6ba..000000000000 --- a/app/models/server/models/CustomUserStatus.js +++ /dev/null @@ -1,71 +0,0 @@ -import { Base } from './_Base'; - -class CustomUserStatus extends Base { - constructor() { - super('custom_user_status'); - - this.tryEnsureIndex({ name: 1 }); - } - - // find one - findOneById(_id, options) { - return this.findOne(_id, options); - } - - // find one by name - findOneByName(name, options) { - return this.findOne({ name }, options); - } - - // find - findByName(name, options) { - const query = { - name, - }; - - return this.find(query, options); - } - - findByNameExceptId(name, except, options) { - const query = { - _id: { $nin: [except] }, - name, - }; - - return this.find(query, options); - } - - // update - setName(_id, name) { - const update = { - $set: { - name, - }, - }; - - return this.update({ _id }, update); - } - - setStatusType(_id, statusType) { - const update = { - $set: { - statusType, - }, - }; - - return this.update({ _id }, update); - } - - // INSERT - create(data) { - return this.insert(data); - } - - - // REMOVE - removeById(_id) { - return this.remove(_id); - } -} - -export default new CustomUserStatus(); diff --git a/app/models/server/models/EmailInbox.js b/app/models/server/models/EmailInbox.js deleted file mode 100644 index 490628be3383..000000000000 --- a/app/models/server/models/EmailInbox.js +++ /dev/null @@ -1,27 +0,0 @@ -import { Base } from './_Base'; - -export class EmailInbox extends Base { - constructor() { - super('email_inbox'); - - this.tryEnsureIndex({ email: 1 }, { unique: true }); - } - - findOneById(_id, options) { - return this.findOne(_id, options); - } - - create(data) { - return this.insert(data); - } - - updateById(_id, data) { - return this.update({ _id }, data); - } - - removeById(_id) { - return this.remove(_id); - } -} - -export default new EmailInbox(); diff --git a/app/models/server/models/EmojiCustom.js b/app/models/server/models/EmojiCustom.js deleted file mode 100644 index d0cd7d7bc4cb..000000000000 --- a/app/models/server/models/EmojiCustom.js +++ /dev/null @@ -1,91 +0,0 @@ -import { Base } from './_Base'; - -class EmojiCustom extends Base { - constructor() { - super('custom_emoji'); - - this.tryEnsureIndex({ name: 1 }); - this.tryEnsureIndex({ aliases: 1 }); - this.tryEnsureIndex({ extension: 1 }); - } - - // find one - findOneById(_id, options) { - return this.findOne(_id, options); - } - - // find - findByNameOrAlias(emojiName, options) { - let name = emojiName; - - if (typeof emojiName === 'string') { - name = emojiName.replace(/:/g, ''); - } - - const query = { - $or: [ - { name }, - { aliases: name }, - ], - }; - - return this.find(query, options); - } - - findByNameOrAliasExceptID(name, except, options) { - const query = { - _id: { $nin: [except] }, - $or: [ - { name }, - { aliases: name }, - ], - }; - - return this.find(query, options); - } - - - // update - setName(_id, name) { - const update = { - $set: { - name, - }, - }; - - return this.update({ _id }, update); - } - - setAliases(_id, aliases) { - const update = { - $set: { - aliases, - }, - }; - - return this.update({ _id }, update); - } - - setExtension(_id, extension) { - const update = { - $set: { - extension, - }, - }; - - return this.update({ _id }, update); - } - - // INSERT - create(data) { - return this.insert(data); - } - - - // REMOVE - removeById(_id) { - return this.remove(_id); - } -} - -export default new EmojiCustom(); diff --git a/app/models/server/models/ExportOperations.js b/app/models/server/models/ExportOperations.js deleted file mode 100644 index fb70d38925ca..000000000000 --- a/app/models/server/models/ExportOperations.js +++ /dev/null @@ -1,108 +0,0 @@ -import _ from 'underscore'; - -import { Base } from './_Base'; - -export class ExportOperations extends Base { - constructor() { - super('export_operations'); - - this.tryEnsureIndex({ userId: 1 }); - this.tryEnsureIndex({ status: 1 }); - } - - // FIND - findById(id) { - const query = { _id: id }; - - return this.find(query); - } - - findLastOperationByUser(userId, fullExport = false, options = {}) { - const query = { - userId, - fullExport, - }; - - options.sort = { createdAt: -1 }; - return this.findOne(query, options); - } - - findPendingByUser(userId, options) { - const query = { - userId, - status: { - $nin: ['completed', 'skipped'], - }, - }; - - return this.find(query, options); - } - - findAllPending(options) { - const query = { - status: { $nin: ['completed', 'skipped'] }, - }; - - return this.find(query, options); - } - - findOnePending(options) { - const query = { - status: { $nin: ['completed', 'skipped'] }, - }; - - return this.findOne(query, options); - } - - findAllPendingBeforeMyRequest(requestDay, options) { - const query = { - status: { $nin: ['completed', 'skipped'] }, - createdAt: { $lt: requestDay }, - }; - - return this.find(query, options); - } - - // UPDATE - updateOperation(data) { - 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.update(data._id, update); - } - - - // INSERT - create(data) { - const exportOperation = { - createdAt: new Date(), - }; - - _.extend(exportOperation, data); - - this.insert(exportOperation); - - return exportOperation._id; - } - - - // REMOVE - removeById(_id) { - return this.remove(_id); - } -} - -export default new ExportOperations(); diff --git a/app/models/server/models/FederationDNSCache.js b/app/models/server/models/FederationDNSCache.js deleted file mode 100644 index 155deed53b95..000000000000 --- a/app/models/server/models/FederationDNSCache.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Base } from './_Base'; - -class FederationDNSCacheModel extends Base { - constructor() { - super('federation_dns_cache'); - } - - findOneByDomain(domain) { - return this.findOne({ domain }); - } -} - -export const FederationDNSCache = new FederationDNSCacheModel(); diff --git a/app/models/server/models/FederationKeys.js b/app/models/server/models/FederationKeys.js deleted file mode 100644 index 188f7cdc434e..000000000000 --- a/app/models/server/models/FederationKeys.js +++ /dev/null @@ -1,58 +0,0 @@ -import NodeRSA from 'node-rsa'; - -import { Base } from './_Base'; - -class FederationKeysModel extends Base { - constructor() { - super('federation_keys'); - } - - getKey(type) { - const keyResource = this.findOne({ type }); - - if (!keyResource) { return null; } - - return keyResource.key; - } - - loadKey(keyData, type) { - return new NodeRSA(keyData, `pkcs8-${ type }-pem`); - } - - generateKeys() { - const key = new NodeRSA({ b: 512 }); - - key.generateKeyPair(); - - this.update({ type: 'private' }, { type: 'private', key: key.exportKey('pkcs8-private-pem').replace(/\n|\r/g, '') }, { upsert: true }); - - this.update({ type: 'public' }, { type: 'public', key: key.exportKey('pkcs8-public-pem').replace(/\n|\r/g, '') }, { upsert: true }); - - return { - privateKey: this.getPrivateKey(), - publicKey: this.getPublicKey(), - }; - } - - getPrivateKey() { - const keyData = this.getKey('private'); - - return keyData && this.loadKey(keyData, 'private'); - } - - getPrivateKeyString() { - return this.getKey('private'); - } - - getPublicKey() { - const keyData = this.getKey('public'); - - return keyData && this.loadKey(keyData, 'public'); - } - - getPublicKeyString() { - return this.getKey('public'); - } -} - -export const FederationKeys = new FederationKeysModel(); diff --git a/app/models/server/models/FederationServers.js b/app/models/server/models/FederationServers.js deleted file mode 100644 index 9daf20d5a128..000000000000 --- a/app/models/server/models/FederationServers.js +++ /dev/null @@ -1,26 +0,0 @@ -import { Base } from './_Base'; -import { Users } from '../raw'; - -class FederationServersModel extends Base { - constructor() { - super('federation_servers'); - - this.tryEnsureIndex({ domain: 1 }); - } - - async refreshServers() { - const domains = await Users.getDistinctFederationDomains(); - - domains.forEach((domain) => { - this.update({ domain }, { - $setOnInsert: { - domain, - }, - }, { upsert: true }); - }); - - this.remove({ domain: { $nin: domains } }); - } -} - -export const FederationServers = new FederationServersModel(); diff --git a/app/models/server/models/InstanceStatus.js b/app/models/server/models/InstanceStatus.js deleted file mode 100644 index 344381e44266..000000000000 --- a/app/models/server/models/InstanceStatus.js +++ /dev/null @@ -1,7 +0,0 @@ -import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; - -import { Base } from './_Base'; - -export class InstanceStatusModel extends Base {} - -export default new InstanceStatusModel(InstanceStatus.getCollection(), { preventSetUpdatedAt: true }); diff --git a/app/models/server/models/IntegrationHistory.js b/app/models/server/models/IntegrationHistory.js deleted file mode 100644 index 817deae0d789..000000000000 --- a/app/models/server/models/IntegrationHistory.js +++ /dev/null @@ -1,43 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Base } from './_Base'; - -export class IntegrationHistory extends Base { - constructor() { - super('integration_history'); - } - - findByType(type, options) { - if (type !== 'outgoing-webhook' || type !== 'incoming-webhook') { - throw new Meteor.Error('invalid-integration-type'); - } - - return this.find({ type }, options); - } - - findByIntegrationId(id, options) { - return this.find({ 'integration._id': id }, options); - } - - findByIntegrationIdAndCreatedBy(id, creatorId, options) { - return this.find({ 'integration._id': id, 'integration._createdBy._id': creatorId }, options); - } - - findOneByIntegrationIdAndHistoryId(integrationId, historyId) { - return this.findOne({ 'integration._id': integrationId, _id: historyId }); - } - - findByEventName(event, options) { - return this.find({ event }, options); - } - - findFailed(options) { - return this.find({ error: true }, options); - } - - removeByIntegrationId(integrationId) { - return this.remove({ 'integration._id': integrationId }); - } -} - -export default new IntegrationHistory(); diff --git a/app/models/server/models/Integrations.js b/app/models/server/models/Integrations.js deleted file mode 100644 index ffbf40c1dcce..000000000000 --- a/app/models/server/models/Integrations.js +++ /dev/null @@ -1,31 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Base } from './_Base'; - -export class Integrations extends Base { - constructor() { - super('integrations'); - - this.tryEnsureIndex({ type: 1 }); - } - - findByType(type, options) { - if (type !== 'webhook-incoming' && type !== 'webhook-outgoing') { - throw new Meteor.Error('invalid-type-to-find'); - } - - return this.find({ type }, options); - } - - disableByUserId(userId) { - return this.update({ userId }, { $set: { enabled: false } }, { multi: true }); - } - - updateRoomName(oldRoomName, newRoomName) { - const hashedOldRoomName = `#${ oldRoomName }`; - const hashedNewRoomName = `#${ newRoomName }`; - return this.update({ channel: hashedOldRoomName }, { $set: { 'channel.$': hashedNewRoomName } }, { multi: true }); - } -} - -export default new Integrations(); diff --git a/app/models/server/models/Invites.js b/app/models/server/models/Invites.js deleted file mode 100644 index 517c1780f0ba..000000000000 --- a/app/models/server/models/Invites.js +++ /dev/null @@ -1,51 +0,0 @@ -import { Base } from './_Base'; - -class Invites extends Base { - constructor() { - super('invites'); - } - - findOneByUserRoomMaxUsesAndExpiration(userId, rid, maxUses, daysToExpire) { - const query = { - rid, - userId, - days: daysToExpire, - maxUses, - }; - - if (daysToExpire > 0) { - query.expires = { - $gt: new Date(), - }; - } - - if (maxUses > 0) { - query.uses = { - $lt: maxUses, - }; - } - - return this.findOne(query); - } - - // INSERT - create(data) { - return this.insert(data); - } - - // REMOVE - removeById(_id) { - return this.remove({ _id }); - } - - // UPDATE - increaseUsageById(_id, uses = 1) { - return this.update({ _id }, { - $inc: { - uses, - }, - }); - } -} - -export default new Invites(); diff --git a/app/models/server/models/LivechatRooms.js b/app/models/server/models/LivechatRooms.js index 13b4c7984769..d899284b4686 100644 --- a/app/models/server/models/LivechatRooms.js +++ b/app/models/server/models/LivechatRooms.js @@ -21,6 +21,7 @@ export class LivechatRooms extends Base { this.tryEnsureIndex({ 'v.token': 1, 'email.thread': 1 }, { sparse: true }); this.tryEnsureIndex({ 'v._id': 1 }, { sparse: true }); this.tryEnsureIndex({ t: 1, departmentId: 1, closedAt: 1 }, { partialFilterExpression: { closedAt: { $exists: true } } }); + this.tryEnsureIndex({ source: 1 }, { sparse: true }); } findLivechat(filter = {}, offset = 0, limit = 20) { @@ -280,6 +281,17 @@ export class LivechatRooms extends Base { return this.findOne(query, options); } + findOneOpenByVisitorTokenAndDepartmentId(visitorToken, departmentId, options) { + const query = { + t: 'l', + open: true, + 'v.token': visitorToken, + departmentId, + }; + + return this.findOne(query, options); + } + findOpenByVisitorTokenAndDepartmentId(visitorToken, departmentId, options) { const query = { t: 'l', @@ -564,6 +576,7 @@ export class LivechatRooms extends Base { open: '$open', servedBy: '$servedBy', metrics: '$metrics', + onHold: '$onHold', }, messagesCount: { $sum: 1, @@ -579,6 +592,7 @@ export class LivechatRooms extends Base { servedBy: '$_id.servedBy', metrics: '$_id.metrics', msgs: '$messagesCount', + onHold: '$_id.onHold', }, }, ]); diff --git a/app/models/server/models/NotificationQueue.js b/app/models/server/models/NotificationQueue.js deleted file mode 100644 index 32eb7524c2c2..000000000000 --- a/app/models/server/models/NotificationQueue.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Base } from './_Base'; - -export class NotificationQueue extends Base { - constructor() { - super('notification_queue'); - this.tryEnsureIndex({ uid: 1 }); - this.tryEnsureIndex({ ts: 1 }, { expireAfterSeconds: 2 * 60 * 60 }); - this.tryEnsureIndex({ schedule: 1 }, { sparse: true }); - this.tryEnsureIndex({ sending: 1 }, { sparse: true }); - this.tryEnsureIndex({ error: 1 }, { sparse: true }); - } -} - -export default new NotificationQueue(); diff --git a/app/models/server/models/OAuthApps.js b/app/models/server/models/OAuthApps.js deleted file mode 100644 index 6aedffb63ae0..000000000000 --- a/app/models/server/models/OAuthApps.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Base } from './_Base'; - -export class OAuthApps extends Base { - constructor() { - super('oauth_apps'); - } -} - -export default new OAuthApps(); diff --git a/app/models/server/models/OEmbedCache.js b/app/models/server/models/OEmbedCache.js deleted file mode 100644 index db4383b9cd34..000000000000 --- a/app/models/server/models/OEmbedCache.js +++ /dev/null @@ -1,39 +0,0 @@ -import { Base } from './_Base'; - -export class OEmbedCache extends Base { - constructor() { - super('oembed_cache'); - this.tryEnsureIndex({ updatedAt: 1 }); - } - - // FIND ONE - findOneById(_id, options) { - const query = { - _id, - }; - return this.findOne(query, options); - } - - // INSERT - createWithIdAndData(_id, data) { - const record = { - _id, - data, - updatedAt: new Date(), - }; - record._id = this.insert(record); - return record; - } - - // REMOVE - removeAfterDate(date) { - const query = { - updatedAt: { - $lte: date, - }, - }; - return this.remove(query); - } -} - -export default new OEmbedCache(); diff --git a/app/models/server/models/Permissions.js b/app/models/server/models/Permissions.js deleted file mode 100644 index 009f29d37f9d..000000000000 --- a/app/models/server/models/Permissions.js +++ /dev/null @@ -1,49 +0,0 @@ -import { Base } from './_Base'; - -export class Permissions extends Base { - // FIND - findByRole(role, options) { - const query = { - roles: role, - }; - - return this.find(query, options); - } - - findOneById(_id) { - return this.findOne({ _id }); - } - - createOrUpdate(name, roles) { - const exists = this.findOne({ - _id: name, - roles, - }, { fields: { _id: 1 } }); - - if (exists) { - return exists._id; - } - - this.upsert({ _id: name }, { $set: { roles } }); - } - - create(name, roles) { - const exists = this.findOneById(name, { fields: { _id: 1 } }); - - if (exists) { - return exists._id; - } - - this.upsert({ _id: name }, { $set: { roles } }); - } - - addRole(permission, role) { - this.update({ _id: permission, roles: { $ne: role } }, { $addToSet: { roles: role } }); - } - - removeRole(permission, role) { - this.update({ _id: permission, roles: role }, { $pull: { roles: role } }); - } -} - -export default new Permissions('permissions'); diff --git a/app/models/server/models/ReadReceipts.js b/app/models/server/models/ReadReceipts.js deleted file mode 100644 index d830f400669f..000000000000 --- a/app/models/server/models/ReadReceipts.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Base } from './_Base'; - -export class ReadReceipts extends Base { - constructor(...args) { - super(...args); - - this.tryEnsureIndex({ - roomId: 1, - userId: 1, - messageId: 1, - }, { - unique: 1, - }); - - this.tryEnsureIndex({ - messageId: 1, - }); - } - - findByMessageId(messageId) { - return this.find({ messageId }); - } -} - -export default new ReadReceipts('message_read_receipt'); diff --git a/app/models/server/models/Reports.js b/app/models/server/models/Reports.js deleted file mode 100644 index 4d2ab019f19b..000000000000 --- a/app/models/server/models/Reports.js +++ /dev/null @@ -1,23 +0,0 @@ -import _ from 'underscore'; - -import { Base } from './_Base'; - -export class Reports extends Base { - constructor() { - super('reports'); - } - - createWithMessageDescriptionAndUserId(message, description, userId, extraData) { - const record = { - message, - description, - ts: new Date(), - userId, - }; - _.extend(record, extraData); - record._id = this.insert(record); - return record; - } -} - -export default new Reports(); diff --git a/app/models/server/models/Roles.js b/app/models/server/models/Roles.js deleted file mode 100644 index e7576191d070..000000000000 --- a/app/models/server/models/Roles.js +++ /dev/null @@ -1,134 +0,0 @@ -import { Base } from './_Base'; -import * as Models from '..'; - - -export class Roles extends Base { - constructor(...args) { - super(...args); - this.tryEnsureIndex({ name: 1 }); - this.tryEnsureIndex({ scope: 1 }); - } - - findUsersInRole(name, scope, options) { - const role = this.findOneByName(name); - const roleScope = (role && role.scope) || 'Users'; - const model = Models[roleScope]; - - return model && model.findUsersInRoles && model.findUsersInRoles(name, scope, options); - } - - isUserInRoles(userId, roles, scope) { - roles = [].concat(roles); - return roles.some((roleName) => { - const role = this.findOneByName(roleName); - const roleScope = (role && role.scope) || 'Users'; - const model = Models[roleScope]; - - return model && model.isUserInRole && model.isUserInRole(userId, roleName, scope); - }); - } - - updateById(_id, name, scope, description, mandatory2fa) { - const queryData = { - name, - scope, - description, - mandatory2fa, - }; - - this.upsert({ _id }, { $set: queryData }); - } - - createWithRandomId(name, scope = 'Users', description = '', protectedRole = true, mandatory2fa = false) { - const role = { - name, - scope, - description, - protected: protectedRole, - mandatory2fa, - }; - - return this.insert(role); - } - - createOrUpdate(name, scope = 'Users', description = '', protectedRole = true, mandatory2fa = false) { - const queryData = { - name, - scope, - description, - protected: protectedRole, - mandatory2fa, - }; - - this.upsert({ _id: name }, { $set: queryData }); - } - - addUserRoles(userId, roles, scope) { - roles = [].concat(roles); - for (const roleName of roles) { - const role = this.findOneByName(roleName); - const roleScope = (role && role.scope) || 'Users'; - const model = Models[roleScope]; - - model && model.addRolesByUserId && model.addRolesByUserId(userId, roleName, scope); - } - return true; - } - - removeUserRoles(userId, roles, scope) { - roles = [].concat(roles); - for (const roleName of roles) { - const role = this.findOneByName(roleName); - const roleScope = (role && role.scope) || 'Users'; - const model = Models[roleScope]; - - model && model.removeRolesByUserId && model.removeRolesByUserId(userId, roleName, scope); - } - return true; - } - - findOneByIdOrName(_idOrName, options) { - const query = { - $or: [{ - _id: _idOrName, - }, { - name: _idOrName, - }], - }; - - return this.findOne(query, options); - } - - findOneByName(name, options) { - const query = { - name, - }; - - return this.findOne(query, options); - } - - findByUpdatedDate(updatedAfterDate, options) { - const query = { - _updatedAt: { $gte: new Date(updatedAfterDate) }, - }; - - return this.find(query, options); - } - - canAddUserToRole(uid, roleName, scope) { - const role = this.findOne({ name: roleName }, { fields: { scope: 1 } }); - if (!role) { - return false; - } - - const model = Models[role.scope]; - if (!model) { - return; - } - - const user = model.isUserInRoleScope(uid, scope); - return !!user; - } -} - -export default new Roles('roles'); diff --git a/app/models/server/models/Rooms.js b/app/models/server/models/Rooms.js index d884208317fc..86076aaa4368 100644 --- a/app/models/server/models/Rooms.js +++ b/app/models/server/models/Rooms.js @@ -5,7 +5,6 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Base } from './_Base'; import Messages from './Messages'; import Subscriptions from './Subscriptions'; -import { getValidRoomName } from '../../../utils'; export class Rooms extends Base { constructor(...args) { @@ -59,6 +58,35 @@ export class Rooms extends Base { return this.update(query, update); } + setCallStatus(_id, status) { + const query = { + _id, + }; + + const update = { + $set: { + callStatus: status, + }, + }; + + return this.update(query, update); + } + + setCallStatusAndCallStartTime(_id, status) { + const query = { + _id, + }; + + const update = { + $set: { + callStatus: status, + webRtcCallStartTime: new Date(), + }, + }; + + return this.update(query, update); + } + findByTokenpass(tokens) { const query = { 'tokenpass.tokens.token': { @@ -335,6 +363,7 @@ export class Rooms extends Base { let channelName = s.trim(name); try { // TODO evaluate if this function call should be here + const { getValidRoomName } = import('../../../utils/lib/getValidRoomName'); channelName = getValidRoomName(channelName, null, { allowDuplicates: true }); } catch (e) { console.error(e); diff --git a/app/models/server/models/ServerEvents.ts b/app/models/server/models/ServerEvents.ts deleted file mode 100644 index 09b17ac51067..000000000000 --- a/app/models/server/models/ServerEvents.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Base } from './_Base'; - -export class ServerEvents extends Base { - constructor() { - super('server_events'); - this.tryEnsureIndex({ t: 1, ip: 1, ts: -1 }); - this.tryEnsureIndex({ t: 1, 'u.username': 1, ts: -1 }); - } -} - -export default new ServerEvents(); diff --git a/app/models/server/models/Sessions.js b/app/models/server/models/Sessions.js deleted file mode 100644 index ffb43d6566a5..000000000000 --- a/app/models/server/models/Sessions.js +++ /dev/null @@ -1,717 +0,0 @@ -import { Base } from './_Base'; -import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; - -export const aggregates = { - dailySessionsOfYesterday(collection, { year, month, day }) { - return collection.aggregate([{ - $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 }); - }, - - getUniqueUsersOfYesterday(collection, { year, month, day }) { - 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(); - }, - - getUniqueUsersOfLastMonthOrWeek(collection, { year, month, day, type = 'month' }) { - 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' }) { - 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 }, - }], - }], - }; - }, - - getUniqueDevicesOfLastMonthOrWeek(collection, { year, month, day, type = 'month' }) { - 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, { year, month, day }) { - 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, { year, month, day, type = 'month' }) { - 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, { year, month, day }) { - 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 Sessions extends Base { - constructor(...args) { - super(...args); - - this.tryEnsureIndex({ instanceId: 1, sessionId: 1, year: 1, month: 1, day: 1 }); - this.tryEnsureIndex({ instanceId: 1, sessionId: 1, userId: 1 }); - this.tryEnsureIndex({ instanceId: 1, sessionId: 1 }); - this.tryEnsureIndex({ sessionId: 1 }); - this.tryEnsureIndex({ userId: 1 }); - this.tryEnsureIndex({ year: 1, month: 1, day: 1, type: 1 }); - this.tryEnsureIndex({ type: 1 }); - this.tryEnsureIndex({ ip: 1, loginAt: 1 }); - this.tryEnsureIndex({ _computedAt: 1 }, { expireAfterSeconds: 60 * 60 * 24 * 45 }); - - const db = this.model.rawDatabase(); - this.secondaryCollection = db.collection(this.model._name, { readPreference: readSecondaryPreferred(db) }); - } - - getUniqueUsersOfYesterday() { - 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: Promise.await(aggregates.getUniqueUsersOfYesterday(this.secondaryCollection, { year, month, day })), - }; - } - - getUniqueUsersOfLastMonth() { - 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: Promise.await(aggregates.getUniqueUsersOfLastMonthOrWeek(this.secondaryCollection, { year, month, day })), - }; - } - - getUniqueUsersOfLastWeek() { - 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: Promise.await(aggregates.getUniqueUsersOfLastMonthOrWeek(this.secondaryCollection, { year, month, day, type: 'week' })), - }; - } - - getUniqueDevicesOfYesterday() { - 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: Promise.await(aggregates.getUniqueDevicesOfYesterday(this.secondaryCollection, { year, month, day })), - }; - } - - getUniqueDevicesOfLastMonth() { - 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: Promise.await(aggregates.getUniqueDevicesOfLastMonthOrWeek(this.secondaryCollection, { year, month, day })), - }; - } - - getUniqueDevicesOfLastWeek() { - 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: Promise.await(aggregates.getUniqueDevicesOfLastMonthOrWeek(this.secondaryCollection, { year, month, day, type: 'week' })), - }; - } - - getUniqueOSOfYesterday() { - 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: Promise.await(aggregates.getUniqueOSOfYesterday(this.secondaryCollection, { year, month, day })), - }; - } - - getUniqueOSOfLastMonth() { - 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: Promise.await(aggregates.getUniqueOSOfLastMonthOrWeek(this.secondaryCollection, { year, month, day })), - }; - } - - getUniqueOSOfLastWeek() { - 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: Promise.await(aggregates.getUniqueOSOfLastMonthOrWeek(this.secondaryCollection, { year, month, day, type: 'week' })), - }; - } - - createOrUpdate(data = {}) { - const { year, month, day, sessionId, instanceId } = data; - - if (!year || !month || !day || !sessionId || !instanceId) { - return; - } - - const now = new Date(); - - return this.upsert({ instanceId, sessionId, year, month, day }, { - $set: data, - $setOnInsert: { - createdAt: now, - }, - }); - } - - closeByInstanceIdAndSessionId(instanceId, sessionId) { - const query = { - instanceId, - sessionId, - closedAt: { $exists: 0 }, - }; - - const closeTime = new Date(); - const update = { - $set: { - closedAt: closeTime, - lastActivityAt: closeTime, - }, - }; - - return this.update(query, update); - } - - updateActiveSessionsByDateAndInstanceIdAndIds({ year, month, day } = {}, instanceId, sessions, data = {}) { - const query = { - instanceId, - year, - month, - day, - sessionId: { $in: sessions }, - closedAt: { $exists: 0 }, - }; - - const update = { - $set: data, - }; - - return this.update(query, update, { multi: true }); - } - - logoutByInstanceIdAndSessionIdAndUserId(instanceId, sessionId, userId) { - const query = { - instanceId, - sessionId, - userId, - logoutAt: { $exists: 0 }, - }; - - const logoutAt = new Date(); - const update = { - $set: { - logoutAt, - }, - }; - - return this.update(query, update, { multi: true }); - } - - createBatch(sessions) { - if (!sessions || sessions.length === 0) { - return; - } - - const ops = []; - 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.model.rawCollection().bulkWrite(ops, { ordered: false }); - } -} - -export default new Sessions('sessions'); diff --git a/app/models/server/models/Sessions.mocks.js b/app/models/server/models/Sessions.mocks.js deleted file mode 100644 index ac4b22bf7780..000000000000 --- a/app/models/server/models/Sessions.mocks.js +++ /dev/null @@ -1,16 +0,0 @@ -import mock from 'mock-require'; - -mock('./_Base', { - Base: class Base { - model = { - rawDatabase() { - return { - collection() {}, - options: {}, - }; - }, - } - - tryEnsureIndex() {} - }, -}); diff --git a/app/models/server/models/SmarshHistory.js b/app/models/server/models/SmarshHistory.js deleted file mode 100644 index 9b2b7abc5043..000000000000 --- a/app/models/server/models/SmarshHistory.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Base } from './_Base'; - -export class SmarshHistory extends Base { - constructor() { - super('smarsh_history'); - } -} - -export default new SmarshHistory(); diff --git a/app/models/server/models/Statistics.js b/app/models/server/models/Statistics.js deleted file mode 100644 index 014f8c8180d2..000000000000 --- a/app/models/server/models/Statistics.js +++ /dev/null @@ -1,28 +0,0 @@ -import { Base } from './_Base'; - -export class Statistics extends Base { - constructor() { - super('statistics'); - - this.tryEnsureIndex({ createdAt: -1 }); - } - - // FIND ONE - findOneById(_id, options) { - const query = { _id }; - return this.findOne(query, options); - } - - findLast() { - const options = { - sort: { - createdAt: -1, - }, - limit: 1, - }; - const records = this.find({}, options).fetch(); - return records && records[0]; - } -} - -export default new Statistics(); diff --git a/app/models/server/models/Uploads.js b/app/models/server/models/Uploads.js deleted file mode 100644 index ce56ea6d0c0f..000000000000 --- a/app/models/server/models/Uploads.js +++ /dev/null @@ -1,146 +0,0 @@ -import _ from 'underscore'; -import s from 'underscore.string'; -import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; - -import { Base } from './_Base'; - -const fillTypeGroup = (fileData) => { - if (!fileData.type) { - return; - } - - fileData.typeGroup = fileData.type.split('/').shift(); -}; - -export class Uploads extends Base { - constructor() { - super('uploads'); - - this.model.before.insert((userId, doc) => { - doc.instanceId = InstanceStatus.id(); - }); - - this.tryEnsureIndex({ rid: 1 }); - this.tryEnsureIndex({ uploadedAt: 1 }); - this.tryEnsureIndex({ typeGroup: 1 }); - } - - findNotHiddenFilesOfRoom(roomId, searchText, fileType, limit) { - const fileQuery = { - rid: roomId, - complete: true, - uploading: false, - _hidden: { - $ne: true, - }, - }; - - if (searchText) { - fileQuery.name = { $regex: new RegExp(escapeRegExp(searchText), 'i') }; - } - - if (fileType && fileType !== 'all') { - fileQuery.typeGroup = fileType; - } - - const fileOptions = { - limit, - sort: { - uploadedAt: -1, - }, - fields: { - _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, ...args) { - fillTypeGroup(fileData); - return super.insert(fileData, ...args); - } - - update(filter, update, ...args) { - if (update.$set) { - fillTypeGroup(update.$set); - } else if (update.type) { - fillTypeGroup(update); - } - - return super.update(filter, update, ...args); - } - - insertFileInit(userId, store, file, extra) { - const fileData = { - userId, - store, - complete: false, - uploading: true, - progress: 0, - extension: s.strRightBack(file.name, '.'), - uploadedAt: new Date(), - }; - - _.extend(fileData, file, extra); - - if (this.model.direct && this.model.direct.insert != null) { - fillTypeGroup(fileData); - file = this.model.direct.insert(fileData); - } else { - file = this.insert(fileData); - } - - return file; - } - - updateFileComplete(fileId, userId, file) { - let result; - if (!fileId) { - return; - } - - const filter = { - _id: fileId, - userId, - }; - - const update = { - $set: { - complete: true, - uploading: false, - progress: 1, - }, - }; - - update.$set = _.extend(file, update.$set); - - if (this.model.direct && this.model.direct.update != null) { - fillTypeGroup(update.$set); - - result = this.model.direct.update(filter, update); - } else { - result = this.update(filter, update); - } - - return result; - } - - deleteFile(fileId) { - if (this.model.direct && this.model.direct.remove != null) { - return this.model.direct.remove({ _id: fileId }); - } - return this.remove({ _id: fileId }); - } -} - -export default new Uploads(); diff --git a/app/models/server/models/UserDataFiles.js b/app/models/server/models/UserDataFiles.js deleted file mode 100644 index a877188ff03b..000000000000 --- a/app/models/server/models/UserDataFiles.js +++ /dev/null @@ -1,44 +0,0 @@ -import _ from 'underscore'; - -import { Base } from './_Base'; - -export class UserDataFiles extends Base { - constructor() { - super('user_data_files'); - - this.tryEnsureIndex({ userId: 1 }); - } - - // FIND - findById(id) { - const query = { _id: id }; - return this.find(query); - } - - findLastFileByUser(userId, options = {}) { - const query = { - userId, - }; - - options.sort = { _updatedAt: -1 }; - return this.findOne(query, options); - } - - // INSERT - create(data) { - const userDataFile = { - createdAt: new Date(), - }; - - _.extend(userDataFile, data); - - return this.insert(userDataFile); - } - - // REMOVE - removeById(_id) { - return this.remove(_id); - } -} - -export default new UserDataFiles(); diff --git a/app/models/server/models/Users.js b/app/models/server/models/Users.js index 6b755bd01a02..f33198de0abe 100644 --- a/app/models/server/models/Users.js +++ b/app/models/server/models/Users.js @@ -1436,23 +1436,6 @@ export class Users extends Base { return this.find(query).count() !== 0; } - addBannerById(_id, banner) { - const query = { - _id, - [`banners.${ banner.id }.read`]: { - $ne: true, - }, - }; - - const update = { - $set: { - [`banners.${ banner.id }`]: banner, - }, - }; - - return this.update(query, update); - } - setBannerReadById(_id, bannerId) { const update = { $set: { diff --git a/app/models/server/models/UsersSessions.js b/app/models/server/models/UsersSessions.js deleted file mode 100644 index 43aec902d343..000000000000 --- a/app/models/server/models/UsersSessions.js +++ /dev/null @@ -1,7 +0,0 @@ -import { UsersSessions } from 'meteor/konecty:user-presence'; - -import { Base } from './_Base'; - -export class UsersSessionsModel extends Base {} - -export default new UsersSessionsModel(UsersSessions, { preventSetUpdatedAt: true }); diff --git a/app/models/server/models/WebdavAccounts.js b/app/models/server/models/WebdavAccounts.js deleted file mode 100644 index 09df0b64a3a6..000000000000 --- a/app/models/server/models/WebdavAccounts.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Webdav Accounts model - */ -import { Base } from './_Base'; - -export class WebdavAccounts extends Base { - constructor() { - super('webdav_accounts'); - - this.tryEnsureIndex({ user_id: 1 }); - } - - findWithUserId(user_id, options) { - const query = { user_id }; - return this.find(query, options); - } - - removeByUserAndId(_id, user_id) { - return this.remove({ _id, user_id }); - } - - removeById(_id) { - return this.remove({ _id }); - } -} - -export default new WebdavAccounts(); diff --git a/app/models/server/models/_BaseDb.js b/app/models/server/models/_BaseDb.js index 6c9473c81fb8..f72bb6c84557 100644 --- a/app/models/server/models/_BaseDb.js +++ b/app/models/server/models/_BaseDb.js @@ -30,31 +30,14 @@ const actions = { d: 'remove', }; -export class BaseDb extends EventEmitter { - constructor(model, baseModel, options = {}) { +export class BaseDbWatch extends EventEmitter { + constructor(collectionName) { super(); - - if (Match.test(model, String)) { - this.name = model; - this.collectionName = this.baseName + this.name; - this.model = new Mongo.Collection(this.collectionName); - } else { - this.name = model._name; - this.collectionName = this.name; - this.model = model; - } - - this.baseModel = baseModel; - - this.preventSetUpdatedAt = !!options.preventSetUpdatedAt; - - this.wrapModel(); + this.collectionName = collectionName; if (!process.env.DISABLE_DB_WATCH) { this.initDbWatch(); } - - this.tryEnsureIndex({ _updatedAt: 1 }, options._updatedAtIndexOptions); } initDbWatch() { @@ -97,6 +80,104 @@ export class BaseDb extends EventEmitter { } } + processOplogRecord({ id, op }) { + const action = actions[op.op]; + metrics.oplog.inc({ + collection: this.collectionName, + op: action, + }); + + if (action === 'insert') { + this.emit('change', { + action, + clientAction: 'inserted', + id: op.o._id, + data: op.o, + oplog: true, + }); + return; + } + + if (action === 'update') { + if (!op.o.$set && !op.o.$unset) { + this.emit('change', { + action, + clientAction: 'updated', + id, + data: op.o, + oplog: true, + }); + return; + } + + const diff = {}; + if (op.o.$set) { + for (const key in op.o.$set) { + if (op.o.$set.hasOwnProperty(key)) { + diff[key] = op.o.$set[key]; + } + } + } + const unset = {}; + if (op.o.$unset) { + for (const key in op.o.$unset) { + if (op.o.$unset.hasOwnProperty(key)) { + diff[key] = undefined; + unset[key] = 1; + } + } + } + + this.emit('change', { + action, + clientAction: 'updated', + id, + diff, + unset, + oplog: true, + }); + return; + } + + if (action === 'remove') { + this.emit('change', { + action, + clientAction: 'removed', + id, + oplog: true, + }); + } + } +} + + +export class BaseDb extends BaseDbWatch { + constructor(model, baseModel, options = {}) { + const collectionName = Match.test(model, String) ? baseName + model : model._name; + + super(collectionName); + + this.collectionName = collectionName; + + if (Match.test(model, String)) { + this.name = model; + this.collectionName = this.baseName + this.name; + this.model = new Mongo.Collection(this.collectionName); + } else { + this.name = model._name; + this.collectionName = this.name; + this.model = model; + } + + this.baseModel = baseModel; + + this.preventSetUpdatedAt = !!options.preventSetUpdatedAt; + + this.wrapModel(); + + this.tryEnsureIndex({ _updatedAt: 1 }, options._updatedAtIndexOptions); + } + get baseName() { return baseName; } @@ -204,75 +285,6 @@ export class BaseDb extends EventEmitter { ); } - processOplogRecord({ id, op }) { - const action = actions[op.op]; - metrics.oplog.inc({ - collection: this.collectionName, - op: action, - }); - - if (action === 'insert') { - this.emit('change', { - action, - clientAction: 'inserted', - id: op.o._id, - data: op.o, - oplog: true, - }); - return; - } - - if (action === 'update') { - if (!op.o.$set && !op.o.$unset) { - this.emit('change', { - action, - clientAction: 'updated', - id, - data: op.o, - oplog: true, - }); - return; - } - - const diff = {}; - if (op.o.$set) { - for (const key in op.o.$set) { - if (op.o.$set.hasOwnProperty(key)) { - diff[key] = op.o.$set[key]; - } - } - } - const unset = {}; - if (op.o.$unset) { - for (const key in op.o.$unset) { - if (op.o.$unset.hasOwnProperty(key)) { - diff[key] = undefined; - unset[key] = 1; - } - } - } - - this.emit('change', { - action, - clientAction: 'updated', - id, - diff, - unset, - oplog: true, - }); - return; - } - - if (action === 'remove') { - this.emit('change', { - action, - clientAction: 'removed', - id, - oplog: true, - }); - } - } - insert(record, ...args) { this.setUpdatedAt(record); diff --git a/app/models/server/models/_oplogHandle.ts b/app/models/server/models/_oplogHandle.ts index 997da3c34eb7..936881eeecaa 100644 --- a/app/models/server/models/_oplogHandle.ts +++ b/app/models/server/models/_oplogHandle.ts @@ -1,5 +1,4 @@ import { Meteor } from 'meteor/meteor'; -import { Promise } from 'meteor/promise'; import { MongoInternals, OplogHandle } from 'meteor/mongo'; import semver from 'semver'; import { MongoClient, Cursor, Timestamp, Db } from 'mongodb'; @@ -193,12 +192,12 @@ class CustomOplogHandle { } } -let oplogHandle: Promise; +let oplogHandle: CustomOplogHandle; if (!process.env.DISABLE_DB_WATCH) { - // @ts-ignore - // eslint-disable-next-line no-undef - if (Package['disable-oplog']) { + const disableOplog = !!(global.Package as any)['disable-oplog']; + + if (disableOplog) { try { oplogHandle = Promise.await(new CustomOplogHandle().start()); } catch (e) { diff --git a/app/models/server/models/apps-persistence-model.js b/app/models/server/models/apps-persistence-model.js index da178a390c32..dd01197abbc9 100644 --- a/app/models/server/models/apps-persistence-model.js +++ b/app/models/server/models/apps-persistence-model.js @@ -4,7 +4,7 @@ export class AppsPersistenceModel extends Base { constructor() { super('apps_persistence'); - this.tryEnsureIndex({ appId: 1 }); + this.tryEnsureIndex({ appId: 1, associations: 1 }); } // Bypass trash collection diff --git a/app/models/server/raw/Analytics.js b/app/models/server/raw/Analytics.js deleted file mode 100644 index 81e0ad335c26..000000000000 --- a/app/models/server/raw/Analytics.js +++ /dev/null @@ -1,151 +0,0 @@ -import { Random } from 'meteor/random'; - -import { BaseRaw } from './BaseRaw'; -import Analytics from '../models/Analytics'; -import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; - -export class AnalyticsRaw extends BaseRaw { - saveMessageSent({ room, date }) { - return this.update({ 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 }) { - return this.update({ date, type: 'users' }, { - $setOnInsert: { - _id: Random.id(), - date, - type: 'users', - }, - $inc: { users: 1 }, - }, { upsert: true }); - } - - saveMessageDeleted({ room, date }) { - return this.update({ date, 'room._id': room._id }, { - $inc: { messages: -1 }, - }); - } - - getMessagesSentTotalByDate({ start, end, options = {} }) { - const params = [ - { - $match: { - type: 'messages', - date: { $gte: start, $lte: end }, - }, - }, - { - $group: { - _id: '$date', - messages: { $sum: '$messages' }, - }, - }, - ]; - if (options.sort) { - params.push({ $sort: options.sort }); - } - if (options.count) { - params.push({ $limit: options.count }); - } - return this.col.aggregate(params).toArray(); - } - - getMessagesOrigin({ start, end }) { - 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).toArray(); - } - - getMostPopularChannelsByMessagesSentQuantity({ start, end, options = {} }) { - const params = [ - { - $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, - }, - }, - ]; - if (options.sort) { - params.push({ $sort: options.sort }); - } - if (options.count) { - params.push({ $limit: options.count }); - } - return this.col.aggregate(params).toArray(); - } - - getTotalOfRegisteredUsersByDate({ start, end, options = {} }) { - const params = [ - { - $match: { - type: 'users', - date: { $gte: start, $lte: end }, - }, - }, - { - $group: { - _id: '$date', - users: { $sum: '$users' }, - }, - }, - ]; - if (options.sort) { - params.push({ $sort: options.sort }); - } - if (options.count) { - params.push({ $limit: options.count }); - } - return this.col.aggregate(params).toArray(); - } - - findByTypeBeforeDate({ type, date }) { - return this.find({ type, date: { $lte: date } }); - } -} - -const db = Analytics.model.rawDatabase(); -export default new AnalyticsRaw(db.collection(Analytics.model._name, { readPreference: readSecondaryPreferred(db) })); diff --git a/app/models/server/raw/Analytics.ts b/app/models/server/raw/Analytics.ts new file mode 100644 index 000000000000..4c2b1fa46cc6 --- /dev/null +++ b/app/models/server/raw/Analytics.ts @@ -0,0 +1,166 @@ +import { Random } from 'meteor/random'; +import { AggregationCursor, Cursor, SortOptionObject, UpdateWriteOpResult } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { IAnalytic } from '../../../../definition/IAnalytic'; +import { IRoom } from '../../../../definition/IRoom'; + +type T = IAnalytic; + +export class AnalyticsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { 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/app/models/server/raw/Avatars.ts b/app/models/server/raw/Avatars.ts new file mode 100644 index 000000000000..cc9c5939f3b3 --- /dev/null +++ b/app/models/server/raw/Avatars.ts @@ -0,0 +1,73 @@ +import { DeleteWriteOpResultObject, UpdateWriteOpResult } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { IAvatar as T } from '../../../../definition/IAvatar'; + +export class AvatarsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { 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/app/models/server/raw/Banners.ts b/app/models/server/raw/Banners.ts index 301ea579bc00..a414332e2fa9 100644 --- a/app/models/server/raw/Banners.ts +++ b/app/models/server/raw/Banners.ts @@ -7,7 +7,7 @@ type T = IBanner; export class BannersRaw extends BaseRaw { constructor( public readonly col: Collection, - public readonly trash?: Collection, + trash?: Collection, ) { super(col, trash); diff --git a/app/models/server/raw/BannersDismiss.ts b/app/models/server/raw/BannersDismiss.ts index bea87b687611..6c4e69f20b96 100644 --- a/app/models/server/raw/BannersDismiss.ts +++ b/app/models/server/raw/BannersDismiss.ts @@ -6,7 +6,7 @@ import { BaseRaw } from './BaseRaw'; export class BannersDismissRaw extends BaseRaw { constructor( public readonly col: Collection, - public readonly trash?: Collection, + trash?: Collection, ) { super(col, trash); diff --git a/app/models/server/raw/BaseRaw.ts b/app/models/server/raw/BaseRaw.ts index 602d8a62542a..47fd8bbdbdd1 100644 --- a/app/models/server/raw/BaseRaw.ts +++ b/app/models/server/raw/BaseRaw.ts @@ -1,10 +1,14 @@ import { Collection, CollectionInsertOneOptions, + CommonOptions, Cursor, DeleteWriteOpResultObject, FilterQuery, + FindAndModifyWriteOpResultObject, + FindOneAndUpdateOption, FindOneOptions, + IndexSpecification, InsertOneWriteOpResult, InsertWriteOpResult, ObjectID, @@ -19,8 +23,14 @@ import { WriteOpResult, } from 'mongodb'; +import { + IRocketChatRecord, + RocketChatRecordDeleted, +} from '../../../../definition/IRocketChatRecord'; 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" @@ -37,120 +47,206 @@ type ExtractIdType = TSchema extends { _id: infer U } // user has defin : U : ObjectId; -type ModelOptionalId = EnhancedOmit & { _id?: ExtractIdType }; +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 type InsertionModel = EnhancedOmit, '_updatedAt'> & { + _updatedAt?: Date; +}; export interface IBaseRaw { col: Collection; } const baseName = 'rocketchat_'; -const isWithoutProjection = (props: T): props is WithoutProjection => !('projection' in props) && !('fields' in props); type DefaultFields = Record | Record | void; -type ResultFields = Defaults extends void ? Base : Defaults[keyof Defaults] extends 1 ? Pick : Omit; +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); } + ? (...rest: any): void => { + console.warn(...rest, new Error().stack); + } : new Function(); export class BaseRaw = undefined> implements IBaseRaw { public readonly defaultFields: C; + protected indexes?: IndexSpecification[]; + protected name: string; + private preventSetUpdatedAt: boolean; + + public readonly trash?: Collection>; + constructor( public readonly col: Collection, - public readonly trash?: Collection, + trash?: Collection, + options?: { preventSetUpdatedAt?: boolean }, ) { this.name = this.col.collectionName.replace(baseName, ''); + this.trash = trash as unknown as Collection>; + + if (this.indexes?.length) { + this.col.createIndexes(this.indexes); + } + + this.preventSetUpdatedAt = options?.preventSetUpdatedAt ?? false; + } + + 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?: undefined, + ): C extends void ? undefined : WithoutProjection>; - private ensureDefaultFields(options: WithoutProjection>): WithoutProjection>; + private ensureDefaultFields( + options: WithoutProjection>, + ): WithoutProjection>; private ensureDefaultFields

(options: FindOneOptions

): FindOneOptions

; - private ensureDefaultFields

(options?: any): FindOneOptions

| undefined | WithoutProjection> { + private ensureDefaultFields

( + options?: any, + ): FindOneOptions

| undefined | WithoutProjection> { if (this.defaultFields === undefined) { return options; } - const { fields, ...rest } = options || {}; + const { fields: deprecatedFields, projection, ...rest } = options || {}; - if (fields) { - warnFields('Using \'fields\' in models is deprecated.', options); + if (deprecatedFields) { + warnFields("Using 'fields' in models is deprecated.", options); } + const fields = { ...deprecatedFields, ...projection }; + return { projection: this.defaultFields, - ...fields && { projection: fields }, + ...fields && Object.values(fields).length && { projection: fields }, ...rest, }; } - async findOneById(_id: string, options?: WithoutProjection> | undefined): Promise; + 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: FindOneOptions

, + ): Promise

; async findOneById

(_id: string, options?: any): Promise { const query = { _id } as FilterQuery; - const optionsDef = this.ensureDefaultFields(options); + 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: WithoutProjection>, + ): Promise; - async findOne

(query: FilterQuery | string, options: FindOneOptions

): 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 q = typeof query === 'string' ? ({ _id: query } as FilterQuery) : query; - const optionsDef = this.ensureDefaultFields(options); + 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'); - } + // 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: WithoutProjection>, + ): Cursor>; - find

(query: FilterQuery, options: FindOneOptions

): Cursor

; + find

(query: FilterQuery, options: FindOneOptions

): Cursor

; find

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

| Cursor { - const optionsDef = this.ensureDefaultFields(options); + const optionsDef = this.doNotMixInclusionAndExclusionFields(options); return this.col.find(query, optionsDef); } - update(filter: FilterQuery, update: UpdateQuery | Partial, options?: UpdateOneOptions & { multi?: boolean }): Promise { - setUpdatedAt(update); + 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 { - setUpdatedAt(update); + 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 { - setUpdatedAt(update); + 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>> { + 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 }; } - setUpdatedAt(doc); + this.setUpdatedAt(doc); return doc; }); @@ -158,60 +254,187 @@ export class BaseRaw = undefined> implements IBase return this.col.insertMany(docs as unknown as Array>, options); } - insertOne(doc: InsertionModel, options?: CollectionInsertOneOptions): Promise>> { + insertOne( + doc: InsertionModel, + options?: CollectionInsertOneOptions, + ): Promise>> { if (!doc._id || typeof doc._id !== 'string') { const oid = new ObjectID(); doc = { _id: oid.toHexString(), ...doc }; } - setUpdatedAt(doc); + this.setUpdatedAt(doc); // TODO reavaluate following type casting return this.col.insertOne(doc as unknown as OptionalId, options); } removeById(_id: string): Promise { - const query: object = { _id }; - return this.col.deleteOne(query); + return this.deleteOne({ _id } as FilterQuery); } - // Trash - trashFind

(query: FilterQuery, options: FindOneOptions

): Cursor

| undefined { + async deleteOne( + filter: FilterQuery, + options?: CommonOptions & { bypassDocumentValidation?: boolean }, + ): Promise { if (!this.trash) { - return undefined; + return this.col.deleteOne(filter, options); } - const { trash } = this; - return trash.find({ - __collection__: this.name, - ...query, - }, 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; - trashFindOneById(_id: string): Promise; + const trash = { + ...record, - trashFindOneById(_id: string, options: WithoutProjection>): Promise; + _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; - trashFindOneById

(_id: string, options: FindOneOptions

): Promise

; + return trash.find( + { + __collection__: this.name, + ...query, + }, + options, + ); + } - async trashFindOneById

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

): Promise { + 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; + } as FilterQuery>; if (!this.trash) { return null; } const { trash } = this; - if (options === undefined) { - return trash.findOne(query); + return trash.findOne(query, options); + } + + private setUpdatedAt(record: UpdateQuery | InsertionModel): void { + if (this.preventSetUpdatedAt) { + return; } - if (isWithoutProjection(options)) { - return trash.findOne(query, options); + 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.findOne(query, options); + + return trash.find(q, options as any); } } diff --git a/app/models/server/raw/CredentialTokens.ts b/app/models/server/raw/CredentialTokens.ts new file mode 100644 index 000000000000..eb6db2786682 --- /dev/null +++ b/app/models/server/raw/CredentialTokens.ts @@ -0,0 +1,29 @@ +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { ICredentialToken as T } from '../../../../definition/ICredentialToken'; + +export class CredentialTokensRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { 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/app/models/server/raw/CustomSounds.js b/app/models/server/raw/CustomSounds.js deleted file mode 100644 index 54e96f064512..000000000000 --- a/app/models/server/raw/CustomSounds.js +++ /dev/null @@ -1,5 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class CustomSoundsRaw extends BaseRaw { - -} diff --git a/app/models/server/raw/CustomSounds.ts b/app/models/server/raw/CustomSounds.ts new file mode 100644 index 000000000000..c46b7f4b4141 --- /dev/null +++ b/app/models/server/raw/CustomSounds.ts @@ -0,0 +1,44 @@ +import { Cursor, FindOneOptions, InsertOneWriteOpResult, UpdateWriteOpResult, WithId, WithoutProjection } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { ICustomSound as T } from '../../../../definition/ICustomSound'; + +export class CustomSoundsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { 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/app/models/server/raw/CustomUserStatus.js b/app/models/server/raw/CustomUserStatus.js deleted file mode 100644 index 0ffc78d4b396..000000000000 --- a/app/models/server/raw/CustomUserStatus.js +++ /dev/null @@ -1,5 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class CustomUserStatusRaw extends BaseRaw { - -} diff --git a/app/models/server/raw/CustomUserStatus.ts b/app/models/server/raw/CustomUserStatus.ts new file mode 100644 index 000000000000..ad1d3df1ea10 --- /dev/null +++ b/app/models/server/raw/CustomUserStatus.ts @@ -0,0 +1,59 @@ +import { Cursor, FindOneOptions, InsertOneWriteOpResult, UpdateWriteOpResult, WithId, WithoutProjection } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { ICustomUserStatus as T } from '../../../../definition/ICustomUserStatus'; + +export class CustomUserStatusRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { 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/app/models/server/raw/EmailInbox.ts b/app/models/server/raw/EmailInbox.ts index 1d8d008242fa..53b88792392f 100644 --- a/app/models/server/raw/EmailInbox.ts +++ b/app/models/server/raw/EmailInbox.ts @@ -1,6 +1,8 @@ -import { BaseRaw } from './BaseRaw'; +import { BaseRaw, IndexSpecification } from './BaseRaw'; import { IEmailInbox } from '../../../../definition/IEmailInbox'; export class EmailInboxRaw extends BaseRaw { - // + protected indexes: IndexSpecification[] = [ + { key: { email: 1 }, unique: true }, + ] } diff --git a/app/models/server/raw/EmailMessageHistory.ts b/app/models/server/raw/EmailMessageHistory.ts index 9201d1b3a344..89c54e079ec0 100644 --- a/app/models/server/raw/EmailMessageHistory.ts +++ b/app/models/server/raw/EmailMessageHistory.ts @@ -1,10 +1,15 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { IndexSpecification, InsertOneWriteOpResult, WithId } from 'mongodb'; + import { BaseRaw } from './BaseRaw'; -import { IEmailMessageHistory } from '../../../../definition/IEmailMessageHistory'; +import { IEmailMessageHistory as T } from '../../../../definition/IEmailMessageHistory'; + +export class EmailMessageHistoryRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { createdAt: 1 }, expireAfterSeconds: 60 * 60 * 24 }, + ] -export class EmailMessageHistoryRaw extends BaseRaw { - insertOne({ _id, email }: IEmailMessageHistory) { - return this.col.insertOne({ + async create({ _id, email }: T): Promise>> { + return this.insertOne({ _id, email, createdAt: new Date(), diff --git a/app/models/server/raw/EmojiCustom.js b/app/models/server/raw/EmojiCustom.js deleted file mode 100644 index 80b81d41958b..000000000000 --- a/app/models/server/raw/EmojiCustom.js +++ /dev/null @@ -1,5 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class EmojiCustomRaw extends BaseRaw { - -} diff --git a/app/models/server/raw/EmojiCustom.ts b/app/models/server/raw/EmojiCustom.ts new file mode 100644 index 000000000000..82f5f22fc97e --- /dev/null +++ b/app/models/server/raw/EmojiCustom.ts @@ -0,0 +1,79 @@ +import { Cursor, FindOneOptions, InsertOneWriteOpResult, UpdateWriteOpResult, WithId, WithoutProjection } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { IEmojiCustom as T } from '../../../../definition/IEmojiCustom'; + +export class EmojiCustomRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { 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/app/models/server/raw/ExportOperations.ts b/app/models/server/raw/ExportOperations.ts new file mode 100644 index 000000000000..d470722d2800 --- /dev/null +++ b/app/models/server/raw/ExportOperations.ts @@ -0,0 +1,68 @@ +import { Cursor, UpdateWriteOpResult } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { IExportOperation } from '../../../../definition/IExportOperation'; + +type T = IExportOperation; + +export class ExportOperationsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { 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/app/models/server/raw/FederationKeys.ts b/app/models/server/raw/FederationKeys.ts new file mode 100644 index 000000000000..7ac06051e4fa --- /dev/null +++ b/app/models/server/raw/FederationKeys.ts @@ -0,0 +1,65 @@ +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/app/models/server/raw/FederationServers.ts b/app/models/server/raw/FederationServers.ts new file mode 100644 index 000000000000..c559b6413770 --- /dev/null +++ b/app/models/server/raw/FederationServers.ts @@ -0,0 +1,29 @@ +import { UpdateWriteOpResult } from 'mongodb'; + +import { Users } from './index'; +import { IFederationServer } from '../../../../definition/Federation'; +import { BaseRaw, IndexSpecification } from './BaseRaw'; + +export class FederationServersRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { 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/app/models/server/raw/IntegrationHistory.ts b/app/models/server/raw/IntegrationHistory.ts index 53f7167db796..923c11c6257e 100644 --- a/app/models/server/raw/IntegrationHistory.ts +++ b/app/models/server/raw/IntegrationHistory.ts @@ -1,4 +1,12 @@ import { BaseRaw } from './BaseRaw'; import { IIntegrationHistory } from '../../../../definition/IIntegrationHistory'; -export class IntegrationHistoryRaw extends BaseRaw {} +export class IntegrationHistoryRaw extends BaseRaw { + 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/app/models/server/raw/Integrations.js b/app/models/server/raw/Integrations.js deleted file mode 100644 index ab8e01a5ebae..000000000000 --- a/app/models/server/raw/Integrations.js +++ /dev/null @@ -1,14 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class IntegrationsRaw extends BaseRaw { - findOneByIdAndCreatedByIfExists({ _id, createdBy }) { - const query = { - _id, - }; - if (createdBy) { - query['_createdBy._id'] = createdBy; - } - - return this.findOne(query); - } -} diff --git a/app/models/server/raw/Integrations.ts b/app/models/server/raw/Integrations.ts new file mode 100644 index 000000000000..521507d3bfec --- /dev/null +++ b/app/models/server/raw/Integrations.ts @@ -0,0 +1,36 @@ +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { IIntegration } from '../../../../definition/IIntegration'; + +export class IntegrationsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { 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: IIntegration['_createdBy'] }): Promise { + return this.findOne({ + _id, + ...createdBy && { '_createdBy._id': createdBy }, + }); + } + + disableByUserId(userId: string): ReturnType['updateMany']> { + return this.updateMany({ userId }, { $set: { enabled: false } }); + } +} diff --git a/app/models/server/raw/Invites.ts b/app/models/server/raw/Invites.ts new file mode 100644 index 000000000000..84d21e4e3e81 --- /dev/null +++ b/app/models/server/raw/Invites.ts @@ -0,0 +1,27 @@ +import type { UpdateWriteOpResult } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; +import { IInvite } from '../../../../definition/IInvite'; + +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, + }, + }); + } +} diff --git a/app/models/server/raw/LivechatAgentActivity.js b/app/models/server/raw/LivechatAgentActivity.ts similarity index 76% rename from app/models/server/raw/LivechatAgentActivity.js rename to app/models/server/raw/LivechatAgentActivity.ts index 9d48cf45a832..7531dd30c345 100644 --- a/app/models/server/raw/LivechatAgentActivity.js +++ b/app/models/server/raw/LivechatAgentActivity.ts @@ -1,9 +1,11 @@ import moment from 'moment'; +import { AggregationCursor } from 'mongodb'; import { BaseRaw } from './BaseRaw'; +import { ILivechatAgentActivity } from '../../../../definition/ILivechatAgentActivity'; -export class LivechatAgentActivityRaw extends BaseRaw { - findAllAverageAvailableServiceTime({ date, departmentId }) { +export class LivechatAgentActivityRaw extends BaseRaw { + findAllAverageAvailableServiceTime({ date, departmentId }: { date: Date; departmentId: string }): Promise { const match = { $match: { date } }; const lookup = { $lookup: { @@ -56,7 +58,7 @@ export class LivechatAgentActivityRaw extends BaseRaw { }, }, }; - const params = [match]; + const params = [match] as object[]; if (departmentId && departmentId !== 'undefined') { params.push(lookup); params.push(unwind); @@ -67,7 +69,19 @@ export class LivechatAgentActivityRaw extends BaseRaw { return this.col.aggregate(params).toArray(); } - findAvailableServiceTimeHistory({ start, end, fullReport, onlyCount = false, options = {} }) { + findAvailableServiceTimeHistory({ + start, + end, + fullReport, + onlyCount = false, + options = {}, + }: { + start: string; + end: string; + fullReport: boolean; + onlyCount: boolean; + options: any; + }): AggregationCursor { const match = { $match: { date: { @@ -101,13 +115,12 @@ export class LivechatAgentActivityRaw extends BaseRaw { _id: 0, username: '$_id.username', availableTimeInSeconds: 1, + ...fullReport && { serviceHistory: 1 }, }, }; - if (fullReport) { - project.$project.serviceHistory = 1; - } + const sort = { $sort: options.sort || { username: 1 } }; - const params = [match, lookup, unwind, group, project, sort]; + const params = [match, lookup, unwind, group, project, sort] as object[]; if (onlyCount) { params.push({ $count: 'total' }); return this.col.aggregate(params); diff --git a/app/models/server/raw/LivechatCustomField.js b/app/models/server/raw/LivechatCustomField.js deleted file mode 100644 index 2e3ee77e85a7..000000000000 --- a/app/models/server/raw/LivechatCustomField.js +++ /dev/null @@ -1,5 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class LivechatCustomFieldRaw extends BaseRaw { - -} diff --git a/app/models/server/raw/LivechatCustomField.ts b/app/models/server/raw/LivechatCustomField.ts new file mode 100644 index 000000000000..6ca1ca5b0e23 --- /dev/null +++ b/app/models/server/raw/LivechatCustomField.ts @@ -0,0 +1,6 @@ +import { BaseRaw } from './BaseRaw'; +import { ILivechatCustomField } from '../../../../definition/ILivechatCustomField'; + +export class LivechatCustomFieldRaw extends BaseRaw { + +} diff --git a/app/models/server/raw/LivechatDepartment.js b/app/models/server/raw/LivechatDepartment.ts similarity index 51% rename from app/models/server/raw/LivechatDepartment.js rename to app/models/server/raw/LivechatDepartment.ts index 7915f57724c3..af4da4397da9 100644 --- a/app/models/server/raw/LivechatDepartment.js +++ b/app/models/server/raw/LivechatDepartment.ts @@ -1,14 +1,16 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { FindOneOptions, Cursor, FilterQuery, WriteOpResult } from 'mongodb'; import { BaseRaw } from './BaseRaw'; +import { ILivechatDepartmentRecord } from '../../../../definition/ILivechatDepartmentRecord'; -export class LivechatDepartmentRaw extends BaseRaw { - findInIds(departmentsIds, options) { +export class LivechatDepartmentRaw extends BaseRaw { + findInIds(departmentsIds: string[], options: FindOneOptions): Cursor { const query = { _id: { $in: departmentsIds } }; return this.find(query, options); } - findByNameRegexWithExceptionsAndConditions(searchTerm, exceptions = [], conditions = {}, options = {}) { + findByNameRegexWithExceptionsAndConditions(searchTerm: string, exceptions: string[] = [], conditions: FilterQuery = {}, options: FindOneOptions = {}): Cursor { if (!Array.isArray(exceptions)) { exceptions = [exceptions]; } @@ -26,17 +28,17 @@ export class LivechatDepartmentRaw extends BaseRaw { return this.find(query, options); } - findByBusinessHourId(businessHourId, options) { + findByBusinessHourId(businessHourId: string, options: FindOneOptions): Cursor { const query = { businessHourId }; return this.find(query, options); } - findEnabledByBusinessHourId(businessHourId, options) { + findEnabledByBusinessHourId(businessHourId: string, options: FindOneOptions): Cursor { const query = { businessHourId, enabled: true }; return this.find(query, options); } - addBusinessHourToDepartmentsByIds(ids = [], businessHourId) { + addBusinessHourToDepartmentsByIds(ids: string[] = [], businessHourId: string): Promise { const query = { _id: { $in: ids }, }; @@ -50,7 +52,7 @@ export class LivechatDepartmentRaw extends BaseRaw { return this.col.update(query, update, { multi: true }); } - removeBusinessHourFromDepartmentsByIdsAndBusinessHourId(ids = [], businessHourId) { + removeBusinessHourFromDepartmentsByIdsAndBusinessHourId(ids: string[] = [], businessHourId: string): Promise { const query = { _id: { $in: ids }, businessHourId, @@ -65,7 +67,7 @@ export class LivechatDepartmentRaw extends BaseRaw { return this.col.update(query, update, { multi: true }); } - removeBusinessHourFromDepartmentsByBusinessHourId(businessHourId) { + removeBusinessHourFromDepartmentsByBusinessHourId(businessHourId: string): Promise { const query = { businessHourId, }; diff --git a/app/models/server/raw/LivechatInquiry.js b/app/models/server/raw/LivechatInquiry.js deleted file mode 100644 index 5a3f4971786b..000000000000 --- a/app/models/server/raw/LivechatInquiry.js +++ /dev/null @@ -1,22 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class LivechatInquiryRaw extends BaseRaw { - findOneQueuedByRoomId(rid) { - const query = { - rid, - status: 'queued', - }; - return this.findOne(query); - } - - findOneByRoomId(rid, options) { - const query = { - rid, - }; - return this.findOne(query, options); - } - - getDistinctQueuedDepartments() { - return this.col.distinct('department', { status: 'queued' }); - } -} diff --git a/app/models/server/raw/LivechatInquiry.ts b/app/models/server/raw/LivechatInquiry.ts new file mode 100644 index 000000000000..cae073044f89 --- /dev/null +++ b/app/models/server/raw/LivechatInquiry.ts @@ -0,0 +1,25 @@ +import { FindOneOptions, MongoDistinctPreferences } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; +import { ILivechatInquiryRecord, LivechatInquiryStatus } from '../../../../definition/IInquiry'; + +export class LivechatInquiryRaw extends BaseRaw { + findOneQueuedByRoomId(rid: string): Promise { + 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); + } +} diff --git a/app/models/server/raw/LivechatRooms.js b/app/models/server/raw/LivechatRooms.js index 0c07e12add2b..c73a15e3ad5c 100644 --- a/app/models/server/raw/LivechatRooms.js +++ b/app/models/server/raw/LivechatRooms.js @@ -479,6 +479,16 @@ export class LivechatRoomsRaw extends BaseRaw { 'metrics.chatDuration': { $exists: false, }, + $or: [{ + onHold: { + $exists: false, + }, + }, { + onHold: { + $exists: true, + $eq: false, + }, + }], servedBy: { $exists: true }, ts: { $gte: new Date(start), $lte: new Date(end) }, }; @@ -494,7 +504,6 @@ export class LivechatRoomsRaw extends BaseRaw { 'metrics.chatDuration': { $exists: true, }, - servedBy: { $exists: true }, ts: { $gte: new Date(start), $lte: new Date(end) }, }; if (departmentId && departmentId !== 'undefined') { @@ -507,6 +516,7 @@ export class LivechatRoomsRaw extends BaseRaw { const query = { t: 'l', servedBy: { $exists: false }, + open: true, ts: { $gte: new Date(start), $lte: new Date(end) }, }; if (departmentId && departmentId !== 'undefined') { @@ -521,6 +531,41 @@ export class LivechatRoomsRaw extends BaseRaw { t: 'l', 'servedBy.username': { $exists: true }, open: true, + $or: [{ + onHold: { + $exists: false, + }, + }, { + onHold: { + $exists: true, + $eq: false, + }, + }], + ts: { $gte: new Date(start), $lte: new Date(end) }, + }, + }; + const group = { + $group: { + _id: '$servedBy.username', + chats: { $sum: 1 }, + }, + }; + if (departmentId && departmentId !== 'undefined') { + match.$match.departmentId = departmentId; + } + return this.col.aggregate([match, group]).toArray(); + } + + countAllOnHoldChatsByAgentBetweenDate({ start, end, departmentId }) { + const match = { + $match: { + t: 'l', + 'servedBy.username': { $exists: true }, + open: true, + onHold: { + $exists: true, + $eq: true, + }, ts: { $gte: new Date(start), $lte: new Date(end) }, }, }; @@ -896,7 +941,7 @@ export class LivechatRoomsRaw extends BaseRaw { return this.col.aggregate(params); } - findRoomsWithCriteria({ agents, roomName, departmentId, open, served, createdAt, closedAt, tags, customFields, visitorId, roomIds, options = {} }) { + findRoomsWithCriteria({ agents, roomName, departmentId, open, served, createdAt, closedAt, tags, customFields, visitorId, roomIds, onhold, options = {} }) { const query = { t: 'l', }; @@ -911,6 +956,7 @@ export class LivechatRoomsRaw extends BaseRaw { } if (open !== undefined) { query.open = { $exists: open }; + query.onHold = { $ne: true }; } if (served !== undefined) { query.servedBy = { $exists: served }; @@ -947,9 +993,35 @@ export class LivechatRoomsRaw extends BaseRaw { query._id = { $in: roomIds }; } + if (onhold) { + query.onHold = { + $exists: true, + $eq: onhold, + }; + } + return this.find(query, { sort: options.sort || { name: 1 }, skip: options.offset, limit: options.count }); } + getOnHoldConversationsBetweenDate(from, to, departmentId) { + const query = { + onHold: { + $exists: true, + $eq: true, + }, + ts: { + $gte: new Date(from), // ISO Date, ts >= date.gte + $lt: new Date(to), // ISODate, ts < date.lt + }, + }; + + if (departmentId && departmentId !== 'undefined') { + query.departmentId = departmentId; + } + + return this.find(query).count(); + } + findAllServiceTimeByAgent({ start, end, onlyCount = false, options = {} }) { const match = { $match: { diff --git a/app/models/server/raw/LivechatTrigger.js b/app/models/server/raw/LivechatTrigger.js deleted file mode 100644 index af9bfbdcce74..000000000000 --- a/app/models/server/raw/LivechatTrigger.js +++ /dev/null @@ -1,5 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class LivechatTriggerRaw extends BaseRaw { - -} diff --git a/app/models/server/raw/LivechatTrigger.ts b/app/models/server/raw/LivechatTrigger.ts new file mode 100644 index 000000000000..71035b1db111 --- /dev/null +++ b/app/models/server/raw/LivechatTrigger.ts @@ -0,0 +1,6 @@ +import { BaseRaw } from './BaseRaw'; +import { ILivechatTrigger } from '../../../../definition/ILivechatTrigger'; + +export class LivechatTriggerRaw extends BaseRaw { + +} diff --git a/app/models/server/raw/LivechatVisitors.js b/app/models/server/raw/LivechatVisitors.ts similarity index 51% rename from app/models/server/raw/LivechatVisitors.js rename to app/models/server/raw/LivechatVisitors.ts index dabd7124625c..ff0f0ba5319f 100644 --- a/app/models/server/raw/LivechatVisitors.js +++ b/app/models/server/raw/LivechatVisitors.ts @@ -1,9 +1,11 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { AggregationCursor, Cursor, FilterQuery, FindOneOptions } from 'mongodb'; import { BaseRaw } from './BaseRaw'; +import { ILivechatVisitor } from '../../../../definition/ILivechatVisitor'; -export class LivechatVisitorsRaw extends BaseRaw { - getVisitorsBetweenDate({ start, end, department }) { +export class LivechatVisitorsRaw extends BaseRaw { + getVisitorsBetweenDate({ start, end, department }: { start: Date; end: Date; department: string }): Cursor { const query = { _updatedAt: { $gte: new Date(start), @@ -12,10 +14,12 @@ export class LivechatVisitorsRaw extends BaseRaw { ...department && department !== 'undefined' && { department }, }; - return this.find(query, { fields: { _id: 1 } }); + return this.find(query, { projection: { _id: 1 } }); } - findByNameRegexWithExceptionsAndConditions(searchTerm, exceptions = [], conditions = {}, options = {}) { + findByNameRegexWithExceptionsAndConditions

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

= {}): AggregationCursor

{ if (!Array.isArray(exceptions)) { exceptions = [exceptions]; } @@ -32,24 +36,22 @@ export class LivechatVisitorsRaw extends BaseRaw { }, }; - const { fields, sort, offset, count } = options; + const { projection, sort, skip, limit } = options; const project = { - $project: { + $project: { // TODO: move this logic to client + // eslint-disable-next-line @typescript-eslint/camelcase custom_name: { $concat: ['$username', ' - ', '$name'] }, - ...fields, + ...projection, }, }; const order = { $sort: sort || { name: 1 } }; - const params = [match, project, order]; - - if (offset) { - params.push({ $skip: offset }); - } - - if (count) { - params.push({ $limit: count }); - } + const params: Record[] = [ + match, + order, + skip && { $skip: skip }, + limit && { $limit: limit }, + project].filter(Boolean) as Record[]; return this.col.aggregate(params); } @@ -58,7 +60,7 @@ export class LivechatVisitorsRaw extends BaseRaw { * Find visitors by their email or phone or username or name * @return [{object}] List of Visitors from db */ - findVisitorsByEmailOrPhoneOrNameOrUsername(_emailOrPhoneOrNameOrUsername, options) { + findVisitorsByEmailOrPhoneOrNameOrUsername(_emailOrPhoneOrNameOrUsername: string, options: FindOneOptions): Cursor { const filter = new RegExp(_emailOrPhoneOrNameOrUsername, 'i'); const query = { $or: [{ diff --git a/app/models/server/raw/Messages.js b/app/models/server/raw/Messages.js index 06addaca1cdb..f3704d6a53b7 100644 --- a/app/models/server/raw/Messages.js +++ b/app/models/server/raw/Messages.js @@ -184,4 +184,17 @@ export class MessagesRaw extends BaseRaw { } return this.col.aggregate(params).toArray(); } + + findLivechatClosedMessages(rid, options) { + return this.find( + { + rid, + $or: [ + { t: { $exists: false } }, + { t: 'livechat-close' }, + ], + }, + options, + ); + } } diff --git a/app/models/server/raw/NotificationQueue.ts b/app/models/server/raw/NotificationQueue.ts index 9aedb9680902..cf80da0b747d 100644 --- a/app/models/server/raw/NotificationQueue.ts +++ b/app/models/server/raw/NotificationQueue.ts @@ -1,25 +1,27 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { - Collection, - ObjectId, -} from 'mongodb'; +import { UpdateWriteOpResult } from 'mongodb'; -import { BaseRaw } from './BaseRaw'; +import { BaseRaw, IndexSpecification } from './BaseRaw'; import { INotification } from '../../../../definition/INotification'; export class NotificationQueueRaw extends BaseRaw { - public readonly col!: Collection; + protected indexes: IndexSpecification[] = [ + { 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) { - return this.col.updateOne({ _id }, { + unsetSendingById(_id: string): Promise { + return this.updateOne({ _id }, { $unset: { sending: 1, }, }); } - setErrorById(_id: string, error: any) { - return this.col.updateOne({ + setErrorById(_id: string, error: any): Promise { + return this.updateOne({ _id, }, { $set: { @@ -31,12 +33,8 @@ export class NotificationQueueRaw extends BaseRaw { }); } - removeById(_id: string) { - return this.col.deleteOne({ _id }); - } - - clearScheduleByUserId(uid: string) { - return this.col.updateMany({ + clearScheduleByUserId(uid: string): Promise { + return this.updateMany({ uid, schedule: { $exists: true }, }, { @@ -47,7 +45,7 @@ export class NotificationQueueRaw extends BaseRaw { } async clearQueueByUserId(uid: string): Promise { - const op = await this.col.deleteMany({ + const op = await this.deleteMany({ uid, }); @@ -83,11 +81,4 @@ export class NotificationQueueRaw extends BaseRaw { return result.value; } - - insertOne(data: Omit) { - return this.col.insertOne({ - _id: new ObjectId().toHexString(), - ...data, - }); - } } diff --git a/app/models/server/raw/Nps.ts b/app/models/server/raw/Nps.ts index 715628e7146e..f77e0b61bde3 100644 --- a/app/models/server/raw/Nps.ts +++ b/app/models/server/raw/Nps.ts @@ -7,7 +7,7 @@ type T = INps; export class NpsRaw extends BaseRaw { constructor( public readonly col: Collection, - public readonly trash?: Collection, + trash?: Collection, ) { super(col, trash); diff --git a/app/models/server/raw/NpsVote.ts b/app/models/server/raw/NpsVote.ts index f6ebb6dcc34e..e215e1a92530 100644 --- a/app/models/server/raw/NpsVote.ts +++ b/app/models/server/raw/NpsVote.ts @@ -7,7 +7,7 @@ type T = INpsVote; export class NpsVoteRaw extends BaseRaw { constructor( public readonly col: Collection, - public readonly trash?: Collection, + trash?: Collection, ) { super(col, trash); diff --git a/app/models/server/raw/OAuthApps.js b/app/models/server/raw/OAuthApps.js deleted file mode 100644 index 68c77a772cdd..000000000000 --- a/app/models/server/raw/OAuthApps.js +++ /dev/null @@ -1,14 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class OAuthAppsRaw extends BaseRaw { - findOneAuthAppByIdOrClientId({ clientId, appId }) { - const query = {}; - if (clientId) { - query.clientId = clientId; - } - if (appId) { - query._id = appId; - } - return this.findOne(query); - } -} diff --git a/app/models/server/raw/OAuthApps.ts b/app/models/server/raw/OAuthApps.ts new file mode 100644 index 000000000000..f70d88616b34 --- /dev/null +++ b/app/models/server/raw/OAuthApps.ts @@ -0,0 +1,11 @@ +import { IOAuthApps as T } from '../../../../definition/IOAuthApps'; +import { BaseRaw } from './BaseRaw'; + +export class OAuthAppsRaw extends BaseRaw { + findOneAuthAppByIdOrClientId({ clientId, appId }: {clientId: string; appId: string}): ReturnType['findOne']> { + return this.findOne({ + ...appId && { _id: appId }, + ...clientId && { clientId }, + }); + } +} diff --git a/app/models/server/raw/OEmbedCache.ts b/app/models/server/raw/OEmbedCache.ts new file mode 100644 index 000000000000..586fb1d7040c --- /dev/null +++ b/app/models/server/raw/OEmbedCache.ts @@ -0,0 +1,31 @@ +import { DeleteWriteOpResultObject } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { IOEmbedCache } from '../../../../definition/IOEmbedCache'; + +type T = IOEmbedCache; + +export class OEmbedCacheRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { 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/app/models/server/raw/Permissions.ts b/app/models/server/raw/Permissions.ts index d5321c82c80b..1b0bacacc53b 100644 --- a/app/models/server/raw/Permissions.ts +++ b/app/models/server/raw/Permissions.ts @@ -2,4 +2,39 @@ import { BaseRaw } from './BaseRaw'; import { IPermission } from '../../../../definition/IPermission'; export class PermissionsRaw extends BaseRaw { + async createOrUpdate(name: string, roles: string[]): 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: string[]): 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: string): Promise { + await this.update({ _id: permission, roles: { $ne: role } }, { $addToSet: { roles: role } }); + } + + async setRoles(permission: string, roles: string[]): Promise { + await this.update({ _id: permission }, { $set: { roles } }); + } + + async removeRole(permission: string, role: string): Promise { + await this.update({ _id: permission, roles: role }, { $pull: { roles: role } }); + } } diff --git a/app/models/server/raw/ReadReceipts.ts b/app/models/server/raw/ReadReceipts.ts new file mode 100644 index 000000000000..12763332ca3e --- /dev/null +++ b/app/models/server/raw/ReadReceipts.ts @@ -0,0 +1,15 @@ +import { Cursor } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { ReadReceipt } from '../../../../definition/ReadReceipt'; + +export class ReadReceiptsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { roomId: 1, userId: 1, messageId: 1 }, unique: true }, + { key: { messageId: 1 } }, + ]; + + findByMessageId(messageId: string): Cursor { + return this.find({ messageId }); + } +} diff --git a/app/models/server/raw/Reports.ts b/app/models/server/raw/Reports.ts new file mode 100644 index 000000000000..9b47fdb8fe9f --- /dev/null +++ b/app/models/server/raw/Reports.ts @@ -0,0 +1,15 @@ +import { BaseRaw } from './BaseRaw'; +import { IReport } from '../../../../definition/IReport'; +import { IMessage } from '../../../../definition/IMessage'; + +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/app/models/server/raw/Roles.js b/app/models/server/raw/Roles.js deleted file mode 100644 index 7e06551fde56..000000000000 --- a/app/models/server/raw/Roles.js +++ /dev/null @@ -1,31 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class RolesRaw extends BaseRaw { - constructor(col, trash, models) { - super(col, trash); - - this.models = models; - } - - async isUserInRoles(userId, roles, scope) { - if (!Array.isArray(roles)) { - roles = [roles]; - } - - for (let i = 0, total = roles.length; i < total; i++) { - const roleName = roles[i]; - - // eslint-disable-next-line no-await-in-loop - const role = await this.findOne({ name: roleName }, { scope: 1 }); - const roleScope = (role && role.scope) || 'Users'; - const model = this.models[roleScope]; - - // eslint-disable-next-line no-await-in-loop - const permitted = await (model && model.isUserInRole && model.isUserInRole(userId, roleName, scope)); - if (permitted) { - return true; - } - } - return false; - } -} diff --git a/app/models/server/raw/Roles.ts b/app/models/server/raw/Roles.ts new file mode 100644 index 000000000000..3d776945677e --- /dev/null +++ b/app/models/server/raw/Roles.ts @@ -0,0 +1,200 @@ +import type { Collection, Cursor, FilterQuery, FindOneOptions, InsertOneWriteOpResult, UpdateWriteOpResult, WithId, WithoutProjection } from 'mongodb'; + +import { IRole, IUser } from '../../../../definition/IUser'; +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); + } + + + createOrUpdate(name: IRole['name'], scope: 'Users' | 'Subscriptions' = 'Users', description = '', protectedRole = true, mandatory2fa = false): Promise { + const queryData = { + name, + scope, + description, + protected: protectedRole, + mandatory2fa, + }; + + return this.updateOne({ _id: name }, { $set: queryData }, { upsert: true }); + } + + async addUserRoles(userId: IUser['_id'], roles: IRole['_id'][], scope?: string): Promise { + if (!Array.isArray(roles)) { + roles = [roles]; + process.env.NODE_ENV === 'development' && console.warn('[WARN] RolesRaw.addUserRoles: roles should be an array'); + } + + for await (const name of roles) { + const role = await this.findOne({ name }, { scope: 1 } as FindOneOptions); + + if (!role) { + process.env.NODE_ENV === 'development' && console.warn(`[WARN] RolesRaw.addUserRoles: role: ${ name } not found`); + continue; + } + switch (role.scope) { + case 'Subscriptions': + await this.models.Subscriptions.addRolesByUserId(userId, [name], scope); + break; + case 'Users': + default: + await this.models.Users.addRolesByUserId(userId, [name]); + } + } + return true; + } + + + async isUserInRoles(userId: IUser['_id'], roles: IRole['_id'][], scope?: string): Promise { + if (!Array.isArray(roles)) { // TODO: remove this check + roles = [roles]; + process.env.NODE_ENV === 'development' && console.warn('[WARN] RolesRaw.isUserInRoles: roles should be an array'); + } + + for await (const roleName of roles) { + const role = await this.findOne({ name: roleName }, { scope: 1 } as FindOneOptions); + + if (!role) { + continue; + } + + switch (role.scope) { + case 'Subscriptions': + if (await this.models.Subscriptions.isUserInRole(userId, roleName, scope)) { + return true; + } + break; + case 'Users': + default: + if (await this.models.Users.isUserInRole(userId, roleName)) { + return true; + } + } + } + return false; + } + + async removeUserRoles(userId: IUser['_id'], roles: IRole['_id'][], scope?: string): Promise { + if (!Array.isArray(roles)) { // TODO: remove this check + roles = [roles]; + process.env.NODE_ENV === 'development' && console.warn('[WARN] RolesRaw.removeUserRoles: roles should be an array'); + } + for await (const roleName of roles) { + const role = await this.findOne({ name: roleName }, { scope: 1 } as FindOneOptions); + + if (!role) { + continue; + } + + switch (role.scope) { + case 'Subscriptions': + scope && await this.models.Subscriptions.removeRolesByUserId(userId, [roleName], scope); + break; + case 'Users': + default: + await this.models.Users.removeRolesByUserId(userId, [roleName]); + } + } + 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); + } + + 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(name: IRole['name'], scope?: string): Promise>; + + findUsersInRole(name: IRole['name'], scope: string | undefined, options: WithoutProjection>): Promise>; + + findUsersInRole

(name: IRole['name'], scope: string | undefined, options: FindOneOptions

): Promise>; + + async findUsersInRole

(name: IRole['name'], scope: string | undefined, options?: any | undefined): Promise | Cursor

> { + const role = await this.findOne({ name }, { scope: 1 } as FindOneOptions); + + if (!role) { + throw new Error('RolesRaw.findUsersInRole: role not found'); + } + + switch (role.scope) { + case 'Subscriptions': + return this.models.Subscriptions.findUsersInRoles([name], scope, options); + case 'Users': + default: + return this.models.Users.findUsersInRoles([name], options); + } + } + + + createWithRandomId(name: IRole['name'], scope: 'Users' | 'Subscriptions' = 'Users', description = '', protectedRole = true, mandatory2fa = false): Promise>> { + const role = { + name, + scope, + description, + protected: protectedRole, + mandatory2fa, + }; + + return this.insertOne(role); + } + + + async canAddUserToRole(uid: IUser['_id'], name: IRole['name'], scope?: string): Promise { + const role = await this.findOne({ name }, { 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/app/models/server/raw/Rooms.js b/app/models/server/raw/Rooms.js index 0da61636ef86..a4d83368a8b4 100644 --- a/app/models/server/raw/Rooms.js +++ b/app/models/server/raw/Rooms.js @@ -181,6 +181,23 @@ export class RoomsRaw extends BaseRaw { return this.find(query, options); } + findRoomsByNameOrFnameStarting(name, options) { + const nameRegex = new RegExp(`^${ escapeRegExp(name).trim() }`, 'i'); + + const query = { + t: { + $in: ['c', 'p'], + }, + $or: [{ + name: nameRegex, + }, { + fname: nameRegex, + }], + }; + + return this.find(query, options); + } + findRoomsWithoutDiscussionsByRoomIds(name, roomIds, options) { const nameRegex = new RegExp(`^${ escapeRegExp(name).trim() }`, 'i'); @@ -350,18 +367,20 @@ export class RoomsRaw extends BaseRaw { const firstParams = [lookup, messagesProject, messagesUnwind, messagesGroup, lastWeekMessagesUnwind, lastWeekMessagesGroup, presentationProject]; const sort = { $sort: options.sort || { messages: -1 } }; const params = [...firstParams, 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).toArray(); + return this.col.aggregate(params); } findOneByName(name, options = {}) { @@ -397,4 +416,23 @@ export class RoomsRaw extends BaseRaw { findOneByNameOrFname(name, options = {}) { return this.col.findOne({ $or: [{ name }, { fname: name }] }, options); } + + allRoomSourcesCount() { + return this.col.aggregate([ + { + $match: { + source: { + $exists: true, + }, + t: 'l', + }, + }, + { + $group: { + _id: '$source', + count: { $sum: 1 }, + }, + }, + ]); + } } diff --git a/app/models/server/raw/ServerEvents.ts b/app/models/server/raw/ServerEvents.ts index f36b44983e19..1bb1342ed885 100644 --- a/app/models/server/raw/ServerEvents.ts +++ b/app/models/server/raw/ServerEvents.ts @@ -1,38 +1,28 @@ -import { Collection, ObjectId } from 'mongodb'; - -import { BaseRaw } from './BaseRaw'; +import { BaseRaw, IndexSpecification } from './BaseRaw'; import { IServerEvent, IServerEventType } from '../../../../definition/IServerEvent'; -import { IUser } from '../../../../definition/IUser'; export class ServerEventsRaw extends BaseRaw { - public readonly col!: Collection; - - async insertOne(data: Omit): Promise { - if (data.u) { - data.u = { _id: data.u._id, username: data.u.username } as IUser; - } - return this.col.insertOne({ - _id: new ObjectId().toHexString(), - ...data, - }); - } + protected indexes: IndexSpecification[] = [ + { key: { t: 1, ip: 1, ts: -1 } }, + { key: { t: 1, 'u.username': 1, ts: -1 } }, + ] async findLastFailedAttemptByIp(ip: string): Promise { - return this.col.findOne({ + return this.findOne({ ip, t: IServerEventType.FAILED_LOGIN_ATTEMPT, }, { sort: { ts: -1 } }); } async findLastFailedAttemptByUsername(username: string): Promise { - return this.col.findOne({ + return this.findOne({ 'u.username': username, t: IServerEventType.FAILED_LOGIN_ATTEMPT, }, { sort: { ts: -1 } }); } async countFailedAttemptsByUsernameSince(username: string, since: Date): Promise { - return this.col.find({ + return this.find({ 'u.username': username, t: IServerEventType.FAILED_LOGIN_ATTEMPT, ts: { @@ -42,7 +32,7 @@ export class ServerEventsRaw extends BaseRaw { } countFailedAttemptsByIpSince(ip: string, since: Date): Promise { - return this.col.find({ + return this.find({ ip, t: IServerEventType.FAILED_LOGIN_ATTEMPT, ts: { @@ -52,14 +42,14 @@ export class ServerEventsRaw extends BaseRaw { } countFailedAttemptsByIp(ip: string): Promise { - return this.col.find({ + return this.find({ ip, t: IServerEventType.FAILED_LOGIN_ATTEMPT, }).count(); } countFailedAttemptsByUsername(username: string): Promise { - return this.col.find({ + return this.find({ 'u.username': username, t: IServerEventType.FAILED_LOGIN_ATTEMPT, }).count(); diff --git a/app/models/server/raw/Sessions.js b/app/models/server/raw/Sessions.js deleted file mode 100644 index 965604fcd0b5..000000000000 --- a/app/models/server/raw/Sessions.js +++ /dev/null @@ -1,285 +0,0 @@ -import { BaseRaw } from './BaseRaw'; -import Sessions from '../models/Sessions'; - -const matchBasedOnDate = (start, end) => { - 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) => { - 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: -1, - month: -1, - day: -1, -}); - -const getProjectionByFullDate = () => ({ - day: '$_id.day', - month: '$_id.month', - year: '$_id.year', -}); - -export class SessionsRaw extends BaseRaw { - getActiveUsersBetweenDates({ start, end }) { - return this.col.aggregate([ - { - $match: { - ...matchBasedOnDate(start, end), - type: 'user_daily', - }, - }, - { - $group: { - _id: '$userId', - }, - }, - ]).toArray(); - } - - async findLastLoginByIp(ip) { - return (await this.col.find({ - ip, - }, { - sort: { loginAt: -1 }, - limit: 1, - }).toArray())[0]; - } - - getActiveUsersOfPeriodByDayBetweenDates({ start, end }) { - return this.col.aggregate([ - { - $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(); - } - - getBusiestTimeWithinHoursPeriod({ start, end, groupSize }) { - 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([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort]).toArray(); - } - - getTotalOfSessionsByDayBetweenDates({ start, end }) { - return this.col.aggregate([ - { - $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(); - } - - getTotalOfSessionByHourAndDayBetweenDates({ start, end }) { - 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([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort]).toArray(); - } -} - -export default new SessionsRaw(Sessions.model.rawCollection()); diff --git a/app/models/server/models/Sessions.tests.js b/app/models/server/raw/Sessions.tests.js similarity index 93% rename from app/models/server/models/Sessions.tests.js rename to app/models/server/raw/Sessions.tests.js index ba0b94c60892..8b284009c5a4 100644 --- a/app/models/server/models/Sessions.tests.js +++ b/app/models/server/raw/Sessions.tests.js @@ -1,11 +1,6 @@ -/* eslint-env mocha */ - -import assert from 'assert'; - +import { expect } from 'chai'; import { MongoMemoryServer } from 'mongodb-memory-server'; -import './Sessions.mocks.js'; - const { MongoClient } = require('mongodb'); const { aggregates } = require('./Sessions'); @@ -282,14 +277,14 @@ describe('Sessions Aggregates', () => { it('should have sessions_dates data saved', () => { const collection = db.collection('sessions_dates'); return collection.find().toArray() - .then((docs) => assert.strictEqual(docs.length, DATA.sessions_dates.length)); + .then((docs) => expect(docs.length).to.be.equal(DATA.sessions_dates.length)); }); it('should match sessions between 2018-12-11 and 2019-1-10', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 1, day: 10 }); - assert.deepStrictEqual($match, { + expect($match).to.be.deep.equal({ $and: [{ $or: [ { year: { $gt: 2018 } }, @@ -309,8 +304,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.strictEqual(docs.length, 31); - assert.deepStrictEqual(docs, [ + expect(docs.length).to.be.equal(31); + expect(docs).to.be.deep.equal([ { _id: '2018-12-11', year: 2018, month: 12, day: 11 }, { _id: '2018-12-12', year: 2018, month: 12, day: 12 }, { _id: '2018-12-13', year: 2018, month: 12, day: 13 }, @@ -350,7 +345,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 2, day: 10 }); - assert.deepStrictEqual($match, { + expect($match).to.be.deep.equal({ year: 2019, $and: [{ $or: [ @@ -369,8 +364,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.strictEqual(docs.length, 31); - assert.deepStrictEqual(docs, [ + expect(docs.length).to.be.deep.equal(31); + expect(docs).to.be.deep.equal([ { _id: '2019-1-11', year: 2019, month: 1, day: 11 }, { _id: '2019-1-12', year: 2019, month: 1, day: 12 }, { _id: '2019-1-13', year: 2019, month: 1, day: 13 }, @@ -410,7 +405,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 5, day: 31 }); - assert.deepStrictEqual($match, { + expect($match).to.be.deep.equal({ year: 2019, month: 5, day: { $gte: 1, $lte: 31 }, @@ -420,8 +415,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.strictEqual(docs.length, 31); - assert.deepStrictEqual(docs, [ + expect(docs.length).to.be.equal(31); + expect(docs).to.be.deep.equal([ { _id: '2019-5-1', year: 2019, month: 5, day: 1 }, { _id: '2019-5-2', year: 2019, month: 5, day: 2 }, { _id: '2019-5-3', year: 2019, month: 5, day: 3 }, @@ -461,7 +456,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 4, day: 30 }); - assert.deepStrictEqual($match, { + expect($match).to.be.deep.equal({ year: 2019, month: 4, day: { $gte: 1, $lte: 30 }, @@ -471,8 +466,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.strictEqual(docs.length, 30); - assert.deepStrictEqual(docs, [ + expect(docs.length).to.be.equal(30); + expect(docs).to.be.deep.equal([ { _id: '2019-4-1', year: 2019, month: 4, day: 1 }, { _id: '2019-4-2', year: 2019, month: 4, day: 2 }, { _id: '2019-4-3', year: 2019, month: 4, day: 3 }, @@ -511,7 +506,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 2, day: 28 }); - assert.deepStrictEqual($match, { + expect($match).to.be.deep.equal({ year: 2019, month: 2, day: { $gte: 1, $lte: 28 }, @@ -521,8 +516,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.strictEqual(docs.length, 28); - assert.deepStrictEqual(docs, [ + expect(docs.length).to.be.equal(28); + expect(docs).to.be.deep.equal([ { _id: '2019-2-1', year: 2019, month: 2, day: 1 }, { _id: '2019-2-2', year: 2019, month: 2, day: 2 }, { _id: '2019-2-3', year: 2019, month: 2, day: 3 }, @@ -559,7 +554,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 2, day: 27 }); - assert.deepStrictEqual($match, { + expect($match).to.be.deep.equal({ year: 2019, $and: [{ $or: [ @@ -578,8 +573,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.strictEqual(docs.length, 31); - assert.deepStrictEqual(docs, [ + expect(docs.length).to.be.equal(31); + expect(docs).to.be.deep.equal([ { _id: '2019-1-28', year: 2019, month: 1, day: 28 }, { _id: '2019-1-29', year: 2019, month: 1, day: 29 }, { _id: '2019-1-30', year: 2019, month: 1, day: 30 }, @@ -618,7 +613,7 @@ describe('Sessions Aggregates', () => { it('should have sessions data saved', () => { const collection = db.collection('sessions'); return collection.find().toArray() - .then((docs) => assert.strictEqual(docs.length, DATA.sessions.length)); + .then((docs) => expect(docs.length).to.be.equal(DATA.sessions.length)); }); it('should generate daily sessions', () => { @@ -631,8 +626,8 @@ describe('Sessions Aggregates', () => { await collection.insertMany(docs); - assert.strictEqual(docs.length, 3); - assert.deepStrictEqual(docs, [{ + expect(docs.length).to.be.equal(3); + expect(docs).to.be.deep.equal([{ _id: 'xPZXw9xqM3kKshsse-2019-5-2', time: 5814, sessions: 3, @@ -728,8 +723,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueUsersOfLastMonthOrWeek(collection, { year: 2019, month: 5, day: 31 }) .then((docs) => { - assert.strictEqual(docs.length, 1); - assert.deepStrictEqual(docs, [{ + expect(docs.length).to.be.equal(1); + expect(docs).to.be.deep.equal([{ count: 2, roles: [{ count: 1, @@ -752,8 +747,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueUsersOfYesterday(collection, { year: 2019, month: 5, day: 1 }) .then((docs) => { - assert.strictEqual(docs.length, 1); - assert.deepStrictEqual(docs, [{ + expect(docs.length).to.be.equal(1); + expect(docs).to.be.deep.equal([{ count: 1, roles: [{ count: 1, @@ -771,8 +766,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueUsersOfYesterday(collection, { year: 2019, month: 5, day: 2 }) .then((docs) => { - assert.strictEqual(docs.length, 1); - assert.deepStrictEqual(docs, [{ + expect(docs.length).to.be.equal(1); + expect(docs).to.be.deep.equal([{ count: 1, roles: [{ count: 1, @@ -790,8 +785,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueDevicesOfLastMonthOrWeek(collection, { year: 2019, month: 5, day: 31 }) .then((docs) => { - assert.strictEqual(docs.length, 2); - assert.deepStrictEqual(docs, [{ + expect(docs.length).to.be.equal(2); + expect(docs).to.be.deep.equal([{ count: 3, time: 9695, type: 'browser', @@ -811,8 +806,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueDevicesOfYesterday(collection, { year: 2019, month: 5, day: 2 }) .then((docs) => { - assert.strictEqual(docs.length, 2); - assert.deepStrictEqual(docs, [{ + expect(docs.length).to.be.equal(2); + expect(docs).to.be.deep.equal([{ count: 2, time: 5528, type: 'browser', @@ -832,8 +827,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueOSOfLastMonthOrWeek(collection, { year: 2019, month: 5, day: 31 }) .then((docs) => { - assert.strictEqual(docs.length, 2); - assert.deepStrictEqual(docs, [{ + expect(docs.length).to.be.equal(2); + expect(docs).to.be.deep.equal([{ count: 3, time: 9695, name: 'Mac OS', @@ -851,8 +846,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueOSOfYesterday(collection, { year: 2019, month: 5, day: 2 }) .then((docs) => { - assert.strictEqual(docs.length, 2); - assert.deepStrictEqual(docs, [{ + expect(docs.length).to.be.equal(2); + expect(docs).to.be.deep.equal([{ count: 2, time: 5528, name: 'Mac OS', @@ -870,7 +865,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 1, day: 4, type: 'week' }); - assert.deepStrictEqual($match, { + expect($match).to.be.deep.equal({ $and: [{ $or: [ { year: { $gt: 2018 } }, @@ -890,8 +885,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.strictEqual(docs.length, 7); - assert.deepStrictEqual(docs, [ + expect(docs.length).to.be.equal(7); + expect(docs).to.be.deep.equal([ { _id: '2018-12-29', year: 2018, month: 12, day: 29 }, { _id: '2018-12-30', year: 2018, month: 12, day: 30 }, { _id: '2018-12-31', year: 2018, month: 12, day: 31 }, @@ -907,7 +902,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 2, day: 4, type: 'week' }); - assert.deepStrictEqual($match, { + expect($match).to.be.deep.equal({ year: 2019, $and: [{ $or: [ @@ -926,8 +921,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.strictEqual(docs.length, 7); - assert.deepStrictEqual(docs, [ + expect(docs.length).to.be.equal(7); + expect(docs).to.be.deep.equal([ { _id: '2019-1-29', year: 2019, month: 1, day: 29 }, { _id: '2019-1-30', year: 2019, month: 1, day: 30 }, { _id: '2019-1-31', year: 2019, month: 1, day: 31 }, @@ -943,7 +938,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 5, day: 7, type: 'week' }); - assert.deepStrictEqual($match, { + expect($match).to.be.deep.equal({ year: 2019, month: 5, day: { $gte: 1, $lte: 7 }, @@ -953,8 +948,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.strictEqual(docs.length, 7); - assert.deepStrictEqual(docs, [ + expect(docs.length).to.be.equal(7); + expect(docs).to.be.deep.equal([ { _id: '2019-5-1', year: 2019, month: 5, day: 1 }, { _id: '2019-5-2', year: 2019, month: 5, day: 2 }, { _id: '2019-5-3', year: 2019, month: 5, day: 3 }, @@ -970,7 +965,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 5, day: 14, type: 'week' }); - assert.deepStrictEqual($match, { + expect($match).to.be.deep.equal({ year: 2019, month: 5, day: { $gte: 8, $lte: 14 }, @@ -980,8 +975,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.strictEqual(docs.length, 7); - assert.deepStrictEqual(docs, [ + expect(docs.length).to.be.equal(7); + expect(docs).to.be.deep.equal([ { _id: '2019-5-8', year: 2019, month: 5, day: 8 }, { _id: '2019-5-9', year: 2019, month: 5, day: 9 }, { _id: '2019-5-10', year: 2019, month: 5, day: 10 }, @@ -997,7 +992,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueUsersOfLastMonthOrWeek(collection, { year: 2019, month: 5, day: 31, type: 'week' }) .then((docs) => { - assert.strictEqual(docs.length, 0); + expect(docs.length).to.be.equal(0); }); }); @@ -1005,8 +1000,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueUsersOfLastMonthOrWeek(collection, { year: 2019, month: 5, day: 7, type: 'week' }) .then((docs) => { - assert.strictEqual(docs.length, 1); - assert.deepStrictEqual(docs, [{ + expect(docs.length).to.be.equal(1); + expect(docs).to.be.deep.equal([{ count: 2, roles: [{ count: 1, @@ -1029,8 +1024,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueDevicesOfLastMonthOrWeek(collection, { year: 2019, month: 5, day: 7, type: 'week' }) .then((docs) => { - assert.strictEqual(docs.length, 2); - assert.deepStrictEqual(docs, [{ + expect(docs.length).to.be.equal(2); + expect(docs).to.be.deep.equal([{ count: 3, time: 9695, type: 'browser', @@ -1050,8 +1045,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueOSOfLastMonthOrWeek(collection, { year: 2019, month: 5, day: 7 }) .then((docs) => { - assert.strictEqual(docs.length, 2); - assert.deepStrictEqual(docs, [{ + expect(docs.length).to.be.equal(2); + expect(docs).to.be.deep.equal([{ count: 3, time: 9695, name: 'Mac OS', diff --git a/app/models/server/raw/Sessions.ts b/app/models/server/raw/Sessions.ts new file mode 100644 index 000000000000..64d0c5cca7ff --- /dev/null +++ b/app/models/server/raw/Sessions.ts @@ -0,0 +1,1061 @@ +import { AggregationCursor, BulkWriteOperation, BulkWriteOpResultObject, Collection, IndexSpecification, UpdateWriteOpResult, FilterQuery } from 'mongodb'; + +import type { ISession } from '../../../../definition/ISession'; +import { BaseRaw, ModelOptionalId } from './BaseRaw'; +import type { IUser } from '../../../../definition/IUser'; + +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}; +type FullReturn = { year: number; month: number; day: number; data: ISession[] }; + +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 & { + time: number; + sessions: number; + devices: ISession['device'][]; + _computedAt: string; + }> { + return collection.aggregate & { + 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 { + protected indexes: IndexSpecification[] = [ + { 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 }, + ] + + private secondaryCollection: Collection; + + constructor( + public readonly col: Collection, + public readonly colSecondary: Collection, + trash?: Collection, + ) { + super(col, trash); + + this.secondaryCollection = colSecondary; + } + + 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, + }); + } + + 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: ISession): 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 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/app/models/server/raw/Settings.ts b/app/models/server/raw/Settings.ts index dd475ed9a131..7e84d539b05e 100644 --- a/app/models/server/raw/Settings.ts +++ b/app/models/server/raw/Settings.ts @@ -1,18 +1,29 @@ -import { Cursor, WriteOpResult } from 'mongodb'; +import { Cursor, FilterQuery, UpdateQuery, WriteOpResult } from 'mongodb'; import { BaseRaw } from './BaseRaw'; -import { ISetting } from '../../../../definition/ISetting'; +import { ISetting, ISettingColor, ISettingSelectOption } from '../../../../definition/ISetting'; -type T = ISetting; -export class SettingsRaw extends BaseRaw { +export class SettingsRaw extends BaseRaw { async getValueById(_id: string): Promise { const setting = await this.findOne>({ _id }, { projection: { value: 1 } }); return setting?.value; } - findOneNotHiddenById(_id: string): Promise { + 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 }, @@ -21,7 +32,7 @@ export class SettingsRaw extends BaseRaw { return this.findOne(query); } - findByIds(_id: string[] | string = []): Cursor { + findByIds(_id: string[] | string = []): Cursor { if (typeof _id === 'string') { _id = [_id]; } @@ -35,7 +46,50 @@ export class SettingsRaw extends BaseRaw { return this.find(query); } - updateValueById(_id: string, value: any): Promise { + 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 }, @@ -45,9 +99,58 @@ export class SettingsRaw extends BaseRaw { 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/app/models/server/raw/SmarshHistory.ts b/app/models/server/raw/SmarshHistory.ts new file mode 100644 index 000000000000..70c2e3df482d --- /dev/null +++ b/app/models/server/raw/SmarshHistory.ts @@ -0,0 +1,8 @@ +import { BaseRaw } from './BaseRaw'; +import { ISmarshHistory } from '../../../../definition/ISmarshHistory'; + +type T = ISmarshHistory; + +export class SmarshHistoryRaw extends BaseRaw { + +} diff --git a/app/models/server/raw/Statistics.js b/app/models/server/raw/Statistics.js deleted file mode 100644 index 15b3cf39404a..000000000000 --- a/app/models/server/raw/Statistics.js +++ /dev/null @@ -1,14 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class StatisticsRaw extends BaseRaw { - async findLast() { - const options = { - sort: { - createdAt: -1, - }, - limit: 1, - }; - const records = await this.find({}, options).toArray(); - return records && records[0]; - } -} diff --git a/app/models/server/raw/Statistics.ts b/app/models/server/raw/Statistics.ts new file mode 100644 index 000000000000..b3b915a9ebce --- /dev/null +++ b/app/models/server/raw/Statistics.ts @@ -0,0 +1,21 @@ +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { IStatistic } from '../../../../definition/IStatistic'; + +type T = IStatistic; + +export class StatisticsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { createdAt: -1 } }, + ] + + async findLast(): Promise { + const options = { + sort: { + createdAt: -1, + }, + limit: 1, + }; + const records = await this.find({}, options).toArray(); + return records && records[0]; + } +} diff --git a/app/models/server/raw/Subscriptions.ts b/app/models/server/raw/Subscriptions.ts index 544af563af91..2877c147312e 100644 --- a/app/models/server/raw/Subscriptions.ts +++ b/app/models/server/raw/Subscriptions.ts @@ -1,10 +1,20 @@ -import { FindOneOptions, Cursor, UpdateQuery, FilterQuery } from 'mongodb'; +import { FindOneOptions, Cursor, UpdateQuery, FilterQuery, UpdateWriteOpResult, Collection, WithoutProjection } from 'mongodb'; +import { compact } from 'lodash'; import { BaseRaw } from './BaseRaw'; import { ISubscription } from '../../../../definition/ISubscription'; +import { IRole, IUser } from '../../../../definition/IUser'; +import { IRoom } from '../../../../definition/IRoom'; +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); + } + findOneByRoomIdAndUserId(rid: string, uid: string, options: FindOneOptions = {}): Promise { const query = { rid, @@ -36,7 +46,18 @@ export class SubscriptionsRaw extends BaseRaw { return this.find(query, options); } - countByRoomIdAndUserId(rid: string, uid: string): Promise { + 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, @@ -47,7 +68,7 @@ export class SubscriptionsRaw extends BaseRaw { return cursor.count(); } - async isUserInRole(uid: string, roleName: string, rid: string): Promise { + async isUserInRole(uid: IUser['_id'], roleName: IRole['name'], rid?: IRoom['_id']): Promise { if (rid == null) { return null; } @@ -80,4 +101,77 @@ export class SubscriptionsRaw extends BaseRaw { return this.update(query, update, options); } + + removeRolesByUserId(uid: IUser['_id'], roles: IRole['name'][], rid: IRoom['_id']): Promise { + const query = { + 'u._id': uid, + rid, + }; + + const update = { + $pullAll: { + roles, + }, + }; + + return this.updateOne(query, update); + } + + + findUsersInRoles(name: IRole['name'][], rid: string | undefined): Promise>; + + findUsersInRoles(name: IRole['name'][], rid: string | undefined, options: WithoutProjection>): Promise>; + + findUsersInRoles

(name: IRole['name'][], rid: string | undefined, options: FindOneOptions

): Promise>; + + async findUsersInRoles

(roles: IRole['name'][], 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['name'][], 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/app/models/server/raw/Team.ts b/app/models/server/raw/Team.ts index 8c8d51b724fc..f8d1874797cb 100644 --- a/app/models/server/raw/Team.ts +++ b/app/models/server/raw/Team.ts @@ -6,7 +6,7 @@ import { ITeam, TEAM_TYPE } from '../../../../definition/ITeam'; export class TeamRaw extends BaseRaw { constructor( public readonly col: Collection, - public readonly trash?: Collection, + trash?: Collection, ) { super(col, trash); diff --git a/app/models/server/raw/TeamMember.ts b/app/models/server/raw/TeamMember.ts index e411980a8627..c65fcea6e533 100644 --- a/app/models/server/raw/TeamMember.ts +++ b/app/models/server/raw/TeamMember.ts @@ -8,7 +8,7 @@ type T = ITeamMember; export class TeamMemberRaw extends BaseRaw { constructor( public readonly col: Collection, - public readonly trash?: Collection, + trash?: Collection, ) { super(col, trash); diff --git a/app/models/server/raw/Uploads.ts b/app/models/server/raw/Uploads.ts new file mode 100644 index 000000000000..ad2fd67247c9 --- /dev/null +++ b/app/models/server/raw/Uploads.ts @@ -0,0 +1,116 @@ +// 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 { BaseRaw, IndexSpecification, InsertionModel } from './BaseRaw'; +import { IUpload as T } from '../../../../definition/IUpload'; + +const fillTypeGroup = (fileData: Partial): void => { + if (!fileData.type) { + return; + } + + fileData.typeGroup = fileData.type.split('/').shift(); +}; + +export class UploadsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { 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/app/models/server/raw/UserDataFiles.ts b/app/models/server/raw/UserDataFiles.ts new file mode 100644 index 000000000000..684135c9d57a --- /dev/null +++ b/app/models/server/raw/UserDataFiles.ts @@ -0,0 +1,29 @@ +import { FindOneOptions, InsertOneWriteOpResult, WithId, WithoutProjection } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { IUserDataFile as T } from '../../../../definition/IUserDataFile'; + +export class UserDataFilesRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { 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/app/models/server/raw/Users.js b/app/models/server/raw/Users.js index 6de2fb2f7e56..dc2cab7b6232 100644 --- a/app/models/server/raw/Users.js +++ b/app/models/server/raw/Users.js @@ -11,6 +11,24 @@ export class UsersRaw extends BaseRaw { }; } + addRolesByUserId(uid, roles) { + if (!Array.isArray(roles)) { + roles = [roles]; + process.env.NODE_ENV === 'development' && console.warn('[WARN] Users.addRolesByUserId: roles should be an array'); + } + + const query = { + _id: uid, + }; + + const update = { + $addToSet: { + roles: { $each: roles }, + }, + }; + return this.updateOne(query, update); + } + findUsersInRoles(roles, scope, options) { roles = [].concat(roles); @@ -706,4 +724,48 @@ export class UsersRaw extends BaseRaw { $pullAll: { __rooms: rids }, }, { multi: true }); } + + removeRolesByUserId(uid, roles) { + const query = { + _id: uid, + }; + + const update = { + $pullAll: { + roles, + }, + }; + + return this.updateOne(query, update); + } + + async isUserInRoleScope(uid) { + const query = { + _id: uid, + }; + + const options = { + fields: { _id: 1 }, + }; + + const found = await this.findOne(query, options); + return !!found; + } + + addBannerById(_id, banner) { + const query = { + _id, + [`banners.${ banner.id }.read`]: { + $ne: true, + }, + }; + + const update = { + $set: { + [`banners.${ banner.id }`]: banner, + }, + }; + + return this.updateOne(query, update); + } } diff --git a/app/models/server/raw/UsersSessions.ts b/app/models/server/raw/UsersSessions.ts index b89feaa9deb3..3560f1e175d1 100644 --- a/app/models/server/raw/UsersSessions.ts +++ b/app/models/server/raw/UsersSessions.ts @@ -1,4 +1,16 @@ import { BaseRaw } from './BaseRaw'; import { IUserSession } from '../../../../definition/IUserSession'; -export class UsersSessionsRaw extends BaseRaw {} +export class UsersSessionsRaw extends BaseRaw { + clearConnectionsFromInstanceId(instanceId: string[]): ReturnType['updateMany']> { + return this.col.updateMany({}, { + $pull: { + connections: { + instanceId: { + $nin: instanceId, + }, + }, + }, + }); + } +} diff --git a/app/models/server/raw/WebdavAccounts.js b/app/models/server/raw/WebdavAccounts.js deleted file mode 100644 index bcd87761c267..000000000000 --- a/app/models/server/raw/WebdavAccounts.js +++ /dev/null @@ -1,8 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class WebdavAccountsRaw extends BaseRaw { - findWithUserId(user_id, options) { - const query = { user_id }; - return this.find(query, options); - } -} diff --git a/app/models/server/raw/WebdavAccounts.ts b/app/models/server/raw/WebdavAccounts.ts new file mode 100644 index 000000000000..1a7fea7114e6 --- /dev/null +++ b/app/models/server/raw/WebdavAccounts.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/camelcase */ +/** + * Webdav Accounts model + */ +import type { Collection, FindOneOptions, Cursor, DeleteWriteOpResultObject } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; +import { IWebdavAccount } from '../../../../definition/IWebdavAccount'; + +type T = IWebdavAccount; + +export class WebdavAccountsRaw extends BaseRaw { + constructor( + public readonly col: Collection, + trash?: Collection, + ) { + super(col, trash); + + this.col.createIndex({ user_id: 1 }); + } + + findOneByIdAndUserId(_id: string, user_id: string, options: FindOneOptions): Promise { + return this.findOne({ _id, user_id }, options); + } + + findOneByUserIdServerUrlAndUsername({ + user_id, + server_url, + username, + }: { + user_id: string; + server_url: string; + username: string; + }, options: FindOneOptions): Promise { + return this.findOne({ user_id, server_url, username }, options); + } + + findWithUserId(user_id: string, options: FindOneOptions): Cursor { + const query = { user_id }; + return this.find(query, options); + } + + removeByUserAndId(_id: string, user_id: string): Promise { + return this.deleteOne({ _id, user_id }); + } +} diff --git a/app/models/server/raw/_Users.d.ts b/app/models/server/raw/_Users.d.ts new file mode 100644 index 000000000000..891392ee3f7e --- /dev/null +++ b/app/models/server/raw/_Users.d.ts @@ -0,0 +1,14 @@ +import { UpdateWriteOpResult } from 'mongodb'; + +import { IRole, IUser } from '../../../../definition/IUser'; +import { BaseRaw } from './BaseRaw'; + +export interface IUserRaw extends BaseRaw { + isUserInRole(uid: IUser['_id'], name: IRole['name']): Promise; + removeRolesByUserId(uid: IUser['_id'], roles: IRole['name'][]): Promise; + findUsersInRoles(roles: IRole['name'][]): Promise; + addRolesByUserId(uid: IUser['_id'], roles: IRole['name'][]): Promise; + isUserInRoleScope(uid: IUser['_id']): Promise; + new(...args: any): IUser; +} +export const UsersRaw: IUserRaw; diff --git a/app/models/server/raw/index.ts b/app/models/server/raw/index.ts index 605218c636be..34789c62ce2f 100644 --- a/app/models/server/raw/index.ts +++ b/app/models/server/raw/index.ts @@ -1,83 +1,81 @@ -import PermissionsModel from '../models/Permissions'; +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 { LivechatExternalMessageRaw } from './LivechatExternalMessages'; +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 RolesModel from '../models/Roles'; +import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; +import { ReadReceiptsRaw } from './ReadReceipts'; +import { ReportsRaw } from './Reports'; import { RolesRaw } from './Roles'; -import SubscriptionsModel from '../models/Subscriptions'; -import { SubscriptionsRaw } from './Subscriptions'; -import SettingsModel from '../models/Settings'; +import { RoomsRaw } from './Rooms'; +import { ServerEventsRaw } from './ServerEvents'; +import { SessionsRaw } from './Sessions'; import { SettingsRaw } from './Settings'; -import UsersModel from '../models/Users'; +import { SmarshHistoryRaw } from './SmarshHistory'; +import { StatisticsRaw } from './Statistics'; +import { SubscriptionsRaw } from './Subscriptions'; import { UsersRaw } from './Users'; -import SessionsModel from '../models/Sessions'; -import { SessionsRaw } from './Sessions'; -import RoomsModel from '../models/Rooms'; -import { RoomsRaw } from './Rooms'; +import { UsersSessionsRaw } from './UsersSessions'; +import { UserDataFilesRaw } from './UserDataFiles'; +import { UploadsRaw } from './Uploads'; +import { WebdavAccountsRaw } from './WebdavAccounts'; +import ImportDataModel from '../models/ImportData'; +import LivechatAgentActivityModel from '../models/LivechatAgentActivity'; +import LivechatBusinessHoursModel from '../models/LivechatBusinessHours'; import LivechatCustomFieldModel from '../models/LivechatCustomField'; -import { LivechatCustomFieldRaw } from './LivechatCustomField'; -import LivechatTriggerModel from '../models/LivechatTrigger'; -import { LivechatTriggerRaw } from './LivechatTrigger'; -import LivechatDepartmentModel from '../models/LivechatDepartment'; -import { LivechatDepartmentRaw } from './LivechatDepartment'; import LivechatDepartmentAgentsModel from '../models/LivechatDepartmentAgents'; -import { LivechatDepartmentAgentsRaw } from './LivechatDepartmentAgents'; -import LivechatRoomsModel from '../models/LivechatRooms'; -import { LivechatRoomsRaw } from './LivechatRooms'; -import MessagesModel from '../models/Messages'; -import { MessagesRaw } from './Messages'; +import LivechatDepartmentModel from '../models/LivechatDepartment'; import LivechatExternalMessagesModel from '../models/LivechatExternalMessages'; -import { LivechatExternalMessageRaw } from './LivechatExternalMessages'; -import LivechatVisitorsModel from '../models/LivechatVisitors'; -import { LivechatVisitorsRaw } from './LivechatVisitors'; import LivechatInquiryModel from '../models/LivechatInquiry'; -import { LivechatInquiryRaw } from './LivechatInquiry'; -import IntegrationsModel from '../models/Integrations'; -import { IntegrationsRaw } from './Integrations'; -import EmojiCustomModel from '../models/EmojiCustom'; -import { EmojiCustomRaw } from './EmojiCustom'; -import WebdavAccountsModel from '../models/WebdavAccounts'; -import { WebdavAccountsRaw } from './WebdavAccounts'; -import OAuthAppsModel from '../models/OAuthApps'; -import { OAuthAppsRaw } from './OAuthApps'; -import CustomSoundsModel from '../models/CustomSounds'; -import { CustomSoundsRaw } from './CustomSounds'; -import CustomUserStatusModel from '../models/CustomUserStatus'; -import { CustomUserStatusRaw } from './CustomUserStatus'; -import LivechatAgentActivityModel from '../models/LivechatAgentActivity'; -import { LivechatAgentActivityRaw } from './LivechatAgentActivity'; -import StatisticsModel from '../models/Statistics'; -import { StatisticsRaw } from './Statistics'; -import NotificationQueueModel from '../models/NotificationQueue'; -import { NotificationQueueRaw } from './NotificationQueue'; -import LivechatBusinessHoursModel from '../models/LivechatBusinessHours'; -import { LivechatBusinessHoursRaw } from './LivechatBusinessHours'; -import ServerEventModel from '../models/ServerEvents'; -import { UsersSessionsRaw } from './UsersSessions'; -import UsersSessionsModel from '../models/UsersSessions'; -import { ServerEventsRaw } from './ServerEvents'; -import { trash } from '../models/_BaseDb'; +import LivechatRoomsModel from '../models/LivechatRooms'; +import LivechatTriggerModel from '../models/LivechatTrigger'; +import LivechatVisitorsModel from '../models/LivechatVisitors'; import LoginServiceConfigurationModel from '../models/LoginServiceConfiguration'; -import { LoginServiceConfigurationRaw } from './LoginServiceConfiguration'; -import { InstanceStatusRaw } from './InstanceStatus'; -import InstanceStatusModel from '../models/InstanceStatus'; -import { IntegrationHistoryRaw } from './IntegrationHistory'; -import IntegrationHistoryModel from '../models/IntegrationHistory'; +import MessagesModel from '../models/Messages'; import OmnichannelQueueModel from '../models/OmnichannelQueue'; -import { OmnichannelQueueRaw } from './OmnichannelQueue'; -import EmailInboxModel from '../models/EmailInbox'; -import { EmailInboxRaw } from './EmailInbox'; -import EmailMessageHistoryModel from '../models/EmailMessageHistory'; -import { EmailMessageHistoryRaw } from './EmailMessageHistory'; -import { api } from '../../../../server/sdk/api'; -import { initWatchers } from '../../../../server/modules/watchers/watchers.module'; -import ImportDataModel from '../models/ImportData'; -import { ImportDataRaw } from './ImportData'; +import RoomsModel from '../models/Rooms'; +import SettingsModel from '../models/Settings'; +import SubscriptionsModel from '../models/Subscriptions'; +import UsersModel from '../models/Users'; const trashCollection = trash.rawCollection(); -export const Permissions = new PermissionsRaw(PermissionsModel.model.rawCollection(), trashCollection); -export const Subscriptions = new SubscriptionsRaw(SubscriptionsModel.model.rawCollection(), trashCollection); -export const Settings = new SettingsRaw(SettingsModel.model.rawCollection(), trashCollection); 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 LivechatTrigger = new LivechatTriggerRaw(LivechatTriggerModel.model.rawCollection(), trashCollection); @@ -88,44 +86,56 @@ export const Messages = new MessagesRaw(MessagesModel.model.rawCollection(), tra export const LivechatExternalMessage = new LivechatExternalMessageRaw(LivechatExternalMessagesModel.model.rawCollection(), trashCollection); export const LivechatVisitors = new LivechatVisitorsRaw(LivechatVisitorsModel.model.rawCollection(), trashCollection); export const LivechatInquiry = new LivechatInquiryRaw(LivechatInquiryModel.model.rawCollection(), trashCollection); -export const Integrations = new IntegrationsRaw(IntegrationsModel.model.rawCollection(), trashCollection); -export const EmojiCustom = new EmojiCustomRaw(EmojiCustomModel.model.rawCollection(), trashCollection); -export const WebdavAccounts = new WebdavAccountsRaw(WebdavAccountsModel.model.rawCollection(), trashCollection); -export const OAuthApps = new OAuthAppsRaw(OAuthAppsModel.model.rawCollection(), trashCollection); -export const CustomSounds = new CustomSoundsRaw(CustomSoundsModel.model.rawCollection(), trashCollection); -export const CustomUserStatus = new CustomUserStatusRaw(CustomUserStatusModel.model.rawCollection(), trashCollection); export const LivechatAgentActivity = new LivechatAgentActivityRaw(LivechatAgentActivityModel.model.rawCollection(), trashCollection); -export const Statistics = new StatisticsRaw(StatisticsModel.model.rawCollection(), trashCollection); -export const NotificationQueue = new NotificationQueueRaw(NotificationQueueModel.model.rawCollection(), trashCollection); export const LivechatBusinessHours = new LivechatBusinessHoursRaw(LivechatBusinessHoursModel.model.rawCollection(), trashCollection); -export const ServerEvents = new ServerEventsRaw(ServerEventModel.model.rawCollection(), trashCollection); -export const Roles = new RolesRaw(RolesModel.model.rawCollection(), trashCollection, { Users, Subscriptions }); -export const UsersSessions = new UsersSessionsRaw(UsersSessionsModel.model.rawCollection(), trashCollection); +// export const Roles = new RolesRaw(RolesModel.model.rawCollection(), { Users, Subscriptions }, trashCollection); export const LoginServiceConfiguration = new LoginServiceConfigurationRaw(LoginServiceConfigurationModel.model.rawCollection(), trashCollection); -export const InstanceStatus = new InstanceStatusRaw(InstanceStatusModel.model.rawCollection(), trashCollection); -export const IntegrationHistory = new IntegrationHistoryRaw(IntegrationHistoryModel.model.rawCollection(), trashCollection); -export const Sessions = new SessionsRaw(SessionsModel.model.rawCollection(), trashCollection); export const OmnichannelQueue = new OmnichannelQueueRaw(OmnichannelQueueModel.model.rawCollection(), trashCollection); -export const EmailInbox = new EmailInboxRaw(EmailInboxModel.model.rawCollection(), trashCollection); -export const EmailMessageHistory = new EmailMessageHistoryRaw(EmailMessageHistoryModel.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 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); + const map = { [Messages.col.collectionName]: MessagesModel, [Users.col.collectionName]: UsersModel, [Subscriptions.col.collectionName]: SubscriptionsModel, [Settings.col.collectionName]: SettingsModel, - [Roles.col.collectionName]: RolesModel, - [Permissions.col.collectionName]: PermissionsModel, [LivechatInquiry.col.collectionName]: LivechatInquiryModel, [LivechatDepartmentAgents.col.collectionName]: LivechatDepartmentAgentsModel, - [UsersSessions.col.collectionName]: UsersSessionsModel, [Rooms.col.collectionName]: RoomsModel, [LoginServiceConfiguration.col.collectionName]: LoginServiceConfigurationModel, - [InstanceStatus.col.collectionName]: InstanceStatusModel, - [IntegrationHistory.col.collectionName]: IntegrationHistoryModel, - [Integrations.col.collectionName]: IntegrationsModel, - [EmailInbox.col.collectionName]: EmailInboxModel, }; if (!process.env.DISABLE_DB_WATCH) { @@ -148,7 +158,7 @@ if (!process.env.DISABLE_DB_WATCH) { }; initWatchers(models, api.broadcastLocal.bind(api), (model, fn) => { - const meteorModel = map[model.col.collectionName]; + const meteorModel = map[model.col.collectionName] || new BaseDbWatch(model.col.collectionName); if (!meteorModel) { return; } diff --git a/app/notifications/client/lib/Notifications.js b/app/notifications/client/lib/Notifications.js index 15cd4374fe96..bf601bb7878d 100644 --- a/app/notifications/client/lib/Notifications.js +++ b/app/notifications/client/lib/Notifications.js @@ -78,9 +78,9 @@ class Notifications { return this.streamRoom.on(`${ room }/${ eventName }`, callback); } - async onUser(eventName, callback) { - await this.streamUser.on(`${ Meteor.userId() }/${ eventName }`, callback); - return () => this.unUser(eventName, callback); + async onUser(eventName, callback, visitorId = null) { + await this.streamUser.on(`${ Meteor.userId() || visitorId }/${ eventName }`, callback); + return () => this.unUser(eventName, callback, visitorId); } unAll(callback) { @@ -95,8 +95,8 @@ class Notifications { return this.streamRoom.removeListener(`${ room }/${ eventName }`, callback); } - unUser(eventName, callback) { - return this.streamUser.removeListener(`${ Meteor.userId() }/${ eventName }`, callback); + unUser(eventName, callback, visitorId = null) { + return this.streamUser.removeListener(`${ Meteor.userId() || visitorId }/${ eventName }`, callback); } } diff --git a/app/notifications/server/lib/Notifications.ts b/app/notifications/server/lib/Notifications.ts index 6653cd98c829..214d642d5f41 100644 --- a/app/notifications/server/lib/Notifications.ts +++ b/app/notifications/server/lib/Notifications.ts @@ -1,5 +1,4 @@ import { Meteor } from 'meteor/meteor'; -import { Promise } from 'meteor/promise'; import { DDPCommon } from 'meteor/ddp-common'; import { NotificationsModule } from '../../../../server/modules/notifications/notifications.module'; diff --git a/app/notifications/server/lib/Presence.ts b/app/notifications/server/lib/Presence.ts index 549c06af110e..19ccba63461a 100644 --- a/app/notifications/server/lib/Presence.ts +++ b/app/notifications/server/lib/Presence.ts @@ -1,7 +1,7 @@ import { Emitter } from '@rocket.chat/emitter'; +import type { IPublication, IStreamerConstructor, Connection, IStreamer } from 'meteor/rocketchat:streamer'; -import { IUser } from '../../../../definition/IUser'; -import { IPublication, IStreamerConstructor, Connection, IStreamer } from '../../../../server/modules/streamer/streamer.module'; +import type { IUser } from '../../../../definition/IUser'; export type UserPresenseStreamProps = { added: IUser['_id'][]; diff --git a/app/oauth2-server-config/server/admin/methods/addOAuthApp.js b/app/oauth2-server-config/server/admin/methods/addOAuthApp.js index cb4d73e19f27..688409e5ddf4 100644 --- a/app/oauth2-server-config/server/admin/methods/addOAuthApp.js +++ b/app/oauth2-server-config/server/admin/methods/addOAuthApp.js @@ -3,11 +3,12 @@ import { Random } from 'meteor/random'; import _ from 'underscore'; import { hasPermission } from '../../../../authorization'; -import { Users, OAuthApps } from '../../../../models'; +import { Users } from '../../../../models/server'; +import { OAuthApps } from '../../../../models/server/raw'; import { parseUriList } from '../functions/parseUriList'; Meteor.methods({ - addOAuthApp(application) { + async addOAuthApp(application) { if (!hasPermission(this.userId, 'manage-oauth-apps')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'addOAuthApp' }); } @@ -31,7 +32,7 @@ Meteor.methods({ application.clientSecret = Random.secret(); application._createdAt = new Date(); application._createdBy = Users.findOne(this.userId, { fields: { username: 1 } }); - application._id = OAuthApps.insert(application); + application._id = (await OAuthApps.insertOne(application)).insertedId; return application; }, }); diff --git a/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.js b/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.js index 6c0b1e665de6..d1df82d95704 100644 --- a/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.js +++ b/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.js @@ -1,18 +1,18 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../../authorization'; -import { OAuthApps } from '../../../../models'; +import { OAuthApps } from '../../../../models/server/raw'; Meteor.methods({ - deleteOAuthApp(applicationId) { + async deleteOAuthApp(applicationId) { if (!hasPermission(this.userId, 'manage-oauth-apps')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'deleteOAuthApp' }); } - const application = OAuthApps.findOne(applicationId); + const application = await OAuthApps.findOneById(applicationId); if (application == null) { throw new Meteor.Error('error-application-not-found', 'Application not found', { method: 'deleteOAuthApp' }); } - OAuthApps.remove({ _id: applicationId }); + await OAuthApps.deleteOne({ _id: applicationId }); return true; }, }); diff --git a/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js b/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js index 007f5be2e95c..3a7f88dda09e 100644 --- a/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js +++ b/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js @@ -2,11 +2,12 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { hasPermission } from '../../../../authorization'; -import { OAuthApps, Users } from '../../../../models'; +import { OAuthApps } from '../../../../models/server/raw'; +import { Users } from '../../../../models/server'; import { parseUriList } from '../functions/parseUriList'; Meteor.methods({ - updateOAuthApp(applicationId, application) { + async updateOAuthApp(applicationId, application) { if (!hasPermission(this.userId, 'manage-oauth-apps')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'updateOAuthApp' }); } @@ -19,7 +20,7 @@ Meteor.methods({ if (!_.isBoolean(application.active)) { throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { method: 'updateOAuthApp' }); } - const currentApplication = OAuthApps.findOne(applicationId); + const currentApplication = await OAuthApps.findOneById(applicationId); if (currentApplication == null) { throw new Meteor.Error('error-application-not-found', 'Application not found', { method: 'updateOAuthApp' }); } @@ -30,7 +31,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', { method: 'updateOAuthApp' }); } - OAuthApps.update(applicationId, { + await OAuthApps.updateOne({ _id: applicationId }, { $set: { name: application.name, active: application.active, @@ -43,6 +44,6 @@ Meteor.methods({ }), }, }); - return OAuthApps.findOne(applicationId); + return OAuthApps.findOneById(applicationId); }, }); diff --git a/app/oauth2-server-config/server/oauth/default-services.js b/app/oauth2-server-config/server/oauth/default-services.js deleted file mode 100644 index d39489c9ec85..000000000000 --- a/app/oauth2-server-config/server/oauth/default-services.js +++ /dev/null @@ -1,17 +0,0 @@ -import { OAuthApps } from '../../../models'; - -if (!OAuthApps.findOne('zapier')) { - OAuthApps.insert({ - _id: 'zapier', - name: 'Zapier', - active: true, - clientId: 'zapier', - clientSecret: 'RTK6TlndaCIolhQhZ7_KHIGOKj41RnlaOq_o-7JKwLr', - redirectUri: 'https://zapier.com/dashboard/auth/oauth/return/RocketChatDevAPI/', - _createdAt: new Date(), - _createdBy: { - _id: 'system', - username: 'system', - }, - }); -} diff --git a/app/oauth2-server-config/server/oauth/default-services.ts b/app/oauth2-server-config/server/oauth/default-services.ts new file mode 100644 index 000000000000..05fd8f5c5d35 --- /dev/null +++ b/app/oauth2-server-config/server/oauth/default-services.ts @@ -0,0 +1,20 @@ +import { OAuthApps } from '../../../models/server/raw'; + +async function run(): Promise { + if (!await OAuthApps.findOneById('zapier')) { + await OAuthApps.insertOne({ + _id: 'zapier', + name: 'Zapier', + active: true, + clientId: 'zapier', + clientSecret: 'RTK6TlndaCIolhQhZ7_KHIGOKj41RnlaOq_o-7JKwLr', + redirectUri: 'https://zapier.com/dashboard/auth/oauth/return/RocketChatDevAPI/', + _createdAt: new Date(), + _createdBy: { + _id: 'system', + username: 'system', + }, + }); + } +} +run(); diff --git a/app/oauth2-server-config/server/oauth/oauth2-server.js b/app/oauth2-server-config/server/oauth/oauth2-server.js index 438aaaa2e0e6..c801074db4d5 100644 --- a/app/oauth2-server-config/server/oauth/oauth2-server.js +++ b/app/oauth2-server-config/server/oauth/oauth2-server.js @@ -1,15 +1,18 @@ import { Meteor } from 'meteor/meteor'; +import { Mongo } from 'meteor/mongo'; import { WebApp } from 'meteor/webapp'; import { OAuth2Server } from 'meteor/rocketchat:oauth2-server'; -import { OAuthApps, Users } from '../../../models'; +import { Users } from '../../../models/server'; +import { OAuthApps } from '../../../models/server/raw'; import { API } from '../../../api/server'; const oauth2server = new OAuth2Server({ accessTokensCollectionName: 'rocketchat_oauth_access_tokens', refreshTokensCollectionName: 'rocketchat_oauth_refresh_tokens', authCodesCollectionName: 'rocketchat_oauth_auth_codes', - clientsCollection: OAuthApps.model, + // TODO: Remove workaround. Used to pass meteor collection reference to a package + clientsCollection: new Mongo.Collection(OAuthApps.col.collectionName), debug: true, }); diff --git a/app/oembed/server/server.js b/app/oembed/server/server.js index 05f95cfbc0dc..a3b32443fd1d 100644 --- a/app/oembed/server/server.js +++ b/app/oembed/server/server.js @@ -10,7 +10,8 @@ import ipRangeCheck from 'ip-range-check'; import he from 'he'; import jschardet from 'jschardet'; -import { OEmbedCache, Messages } from '../../models'; +import { Messages } from '../../models/server'; +import { OEmbedCache } from '../../models/server/raw'; import { callbacks } from '../../callbacks'; import { settings } from '../../settings'; import { isURL } from '../../utils/lib/isURL'; @@ -214,8 +215,8 @@ OEmbed.getUrlMeta = function(url, withFragment) { }); }; -OEmbed.getUrlMetaWithCache = function(url, withFragment) { - const cache = OEmbedCache.findOneById(url); +OEmbed.getUrlMetaWithCache = async function(url, withFragment) { + const cache = await OEmbedCache.findOneById(url); if (cache != null) { return cache.data; @@ -223,7 +224,7 @@ OEmbed.getUrlMetaWithCache = function(url, withFragment) { const data = OEmbed.getUrlMeta(url, withFragment); if (data != null) { try { - OEmbedCache.createWithIdAndData(url, data); + await OEmbedCache.createWithIdAndData(url, data); } catch (_error) { SystemLogger.error('OEmbed duplicated record', url); } @@ -262,21 +263,21 @@ const getRelevantMetaTags = function(metaObj) { const insertMaxWidthInOembedHtml = (oembedHtml) => oembedHtml?.replace('iframe', 'iframe style=\"max-width: 100%;width:400px;height:225px\"'); -OEmbed.rocketUrlParser = function(message) { +OEmbed.rocketUrlParser = async function(message) { if (Array.isArray(message.urls)) { - let attachments = []; + const attachments = []; let changed = false; - message.urls.forEach(function(item) { + for await (const item of message.urls) { if (item.ignoreParse === true) { return; } if (!isURL(item.url)) { return; } - const data = OEmbed.getUrlMetaWithCache(item.url); + const data = await OEmbed.getUrlMetaWithCache(item.url); if (data != null) { if (data.attachments) { - attachments = _.union(attachments, data.attachments); + attachments.push(...data.attachments); return; } if (data.meta != null) { @@ -291,7 +292,7 @@ OEmbed.rocketUrlParser = function(message) { item.parsedUrl = data.parsedUrl; changed = true; } - }); + } if (attachments.length) { Messages.setMessageAttachments(message._id, attachments); } @@ -304,7 +305,7 @@ OEmbed.rocketUrlParser = function(message) { settings.watch('API_Embed', function(value) { if (value) { - return callbacks.add('afterSaveMessage', OEmbed.rocketUrlParser, callbacks.priority.LOW, 'API_Embed'); + return callbacks.add('afterSaveMessage', (message) => Promise.await(OEmbed.rocketUrlParser(message)), callbacks.priority.LOW, 'API_Embed'); } return callbacks.remove('afterSaveMessage', 'API_Embed'); }); diff --git a/app/reactions/server/setReaction.js b/app/reactions/server/setReaction.js index 279e4e813d88..e5f2a885b308 100644 --- a/app/reactions/server/setReaction.js +++ b/app/reactions/server/setReaction.js @@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import _ from 'underscore'; -import { Messages, EmojiCustom, Rooms } from '../../models/server'; +import { Messages, Rooms } from '../../models/server'; +import { EmojiCustom } from '../../models/server/raw'; import { callbacks } from '../../callbacks/server'; import { emoji } from '../../emoji/server'; import { isTheLastMessage, msgStream } from '../../lib/server'; @@ -20,7 +21,7 @@ const removeUserReaction = (message, reaction, username) => { async function setReaction(room, user, message, reaction, shouldReact) { reaction = `:${ reaction.replace(/:/g, '') }:`; - if (!emoji.list[reaction] && EmojiCustom.findByNameOrAlias(reaction).count() === 0) { + if (!emoji.list[reaction] && await EmojiCustom.findByNameOrAlias(reaction).count() === 0) { throw new Meteor.Error('error-not-allowed', 'Invalid emoji provided.', { method: 'setReaction' }); } diff --git a/app/settings/server/functions/getSettingDefaults.tests.ts b/app/settings/server/functions/getSettingDefaults.tests.ts index 35d5f3f5c029..63581c43ce69 100644 --- a/app/settings/server/functions/getSettingDefaults.tests.ts +++ b/app/settings/server/functions/getSettingDefaults.tests.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/camelcase */ -/* eslint-env mocha */ import { expect } from 'chai'; import { getSettingDefaults } from './getSettingDefaults'; diff --git a/app/settings/server/functions/overrideGenerator.tests.ts b/app/settings/server/functions/overrideGenerator.tests.ts index a1881527ed84..776cf8ce2cf9 100644 --- a/app/settings/server/functions/overrideGenerator.tests.ts +++ b/app/settings/server/functions/overrideGenerator.tests.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/camelcase */ -/* eslint-env mocha */ import { expect } from 'chai'; import { getSettingDefaults } from './getSettingDefaults'; diff --git a/app/settings/server/functions/settings.tests.ts b/app/settings/server/functions/settings.tests.ts index 4028486beb2a..55692c7dc6d5 100644 --- a/app/settings/server/functions/settings.tests.ts +++ b/app/settings/server/functions/settings.tests.ts @@ -1,14 +1,10 @@ /* eslint-disable @typescript-eslint/camelcase */ -/* eslint-env mocha */ -import chai, { expect } from 'chai'; -import spies from 'chai-spies'; +import { expect, spy } from 'chai'; import { Settings } from './settings.mocks'; import { SettingsRegistry } from '../SettingsRegistry'; import { CachedSettings } from '../CachedSettings'; -chai.use(spies); - describe('Settings', () => { beforeEach(() => { Settings.insertCalls = 0; @@ -306,8 +302,8 @@ describe('Settings', () => { settings.initilized(); const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); - const spy = chai.spy(); - const spy2 = chai.spy(); + const spiedCallback1 = spy(); + const spiedCallback2 = spy(); settingsRegistry.addGroup('group', function() { this.section('section', function() { @@ -317,27 +313,27 @@ describe('Settings', () => { }); }); - settings.watch('setting_callback', spy, { debounce: 10 }); - settings.watchByRegex(/setting_callback/, spy2, { debounce: 10 }); + settings.watch('setting_callback', spiedCallback1, { debounce: 10 }); + settings.watchByRegex(/setting_callback/, spiedCallback2, { debounce: 10 }); setTimeout(() => { - expect(spy).to.have.been.called.exactly(1); - expect(spy2).to.have.been.called.exactly(1); - expect(spy).to.have.been.called.always.with('value1'); - expect(spy2).to.have.been.called.always.with('setting_callback', 'value1'); + expect(spiedCallback1).to.have.been.called.exactly(1); + expect(spiedCallback2).to.have.been.called.exactly(1); + expect(spiedCallback1).to.have.been.called.always.with('value1'); + expect(spiedCallback2).to.have.been.called.always.with('setting_callback', 'value1'); done(); }, settings.getConfig({ debounce: 10 }).debounce); }); it('should call `settings.watch` callback on setting changed registering before initialized', (done) => { - const spy = chai.spy(); - const spy2 = chai.spy(); + const spiedCallback1 = spy(); + const spiedCallback2 = spy(); const settings = new CachedSettings(); Settings.settings = settings; const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); - settings.watch('setting_callback', spy, { debounce: 1 }); - settings.watchByRegex(/setting_callback/ig, spy2, { debounce: 1 }); + settings.watch('setting_callback', spiedCallback1, { debounce: 1 }); + settings.watchByRegex(/setting_callback/ig, spiedCallback2, { debounce: 1 }); settings.initilized(); settingsRegistry.addGroup('group', function() { @@ -350,10 +346,10 @@ describe('Settings', () => { setTimeout(() => { Settings.updateValueById('setting_callback', 'value3'); setTimeout(() => { - expect(spy).to.have.been.called.exactly(2); - expect(spy2).to.have.been.called.exactly(2); - expect(spy).to.have.been.called.with('value2'); - expect(spy).to.have.been.called.with('value3'); + expect(spiedCallback1).to.have.been.called.exactly(2); + expect(spiedCallback2).to.have.been.called.exactly(2); + expect(spiedCallback1).to.have.been.called.with('value2'); + expect(spiedCallback1).to.have.been.called.with('value3'); done(); }, settings.getConfig({ debounce: 10 }).debounce); }, settings.getConfig({ debounce: 10 }).debounce); diff --git a/app/settings/server/functions/validateSettings.tests.ts b/app/settings/server/functions/validateSettings.tests.ts index 8891afcf6947..ba7d28f793f8 100644 --- a/app/settings/server/functions/validateSettings.tests.ts +++ b/app/settings/server/functions/validateSettings.tests.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/camelcase */ -/* eslint-env mocha */ import { expect } from 'chai'; import { validateSetting } from './validateSetting'; diff --git a/app/settings/server/raw.tests.js b/app/settings/server/raw.tests.js index 7a9d6bccd6cf..ecd1aeada491 100644 --- a/app/settings/server/raw.tests.js +++ b/app/settings/server/raw.tests.js @@ -1,22 +1,18 @@ -/* eslint-env mocha */ -import chai, { expect } from 'chai'; -import spies from 'chai-spies'; +import { expect, spy } from 'chai'; import rewire from 'rewire'; -chai.use(spies); - describe('Raw Settings', () => { let rawModule; const cache = new Map(); before('rewire deps', () => { - const spy = chai.spy(async (id) => { + const spied = spy(async (id) => { if (id === '1') { return 'some-setting-value'; } return null; }); rawModule = rewire('./raw'); - rawModule.__set__('setFromDB', spy); + rawModule.__set__('setFromDB', spied); rawModule.__set__('cache', cache); }); diff --git a/app/smarsh-connector/server/functions/generateEml.js b/app/smarsh-connector/server/functions/generateEml.js index 8633fd1ca0ca..f828ce310387 100644 --- a/app/smarsh-connector/server/functions/generateEml.js +++ b/app/smarsh-connector/server/functions/generateEml.js @@ -4,7 +4,8 @@ import _ from 'underscore'; import moment from 'moment'; import { settings } from '../../../settings'; -import { Rooms, Messages, Users, SmarshHistory } from '../../../models'; +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'; @@ -31,8 +32,8 @@ smarsh.generateEml = () => { const smarshMissingEmail = settings.get('Smarsh_MissingEmail_Email'); const timeZone = settings.get('Smarsh_Timezone'); - Rooms.find().forEach((room) => { - const smarshHistory = SmarshHistory.findOne({ _id: room._id }); + Rooms.find().forEach(async (room) => { + const smarshHistory = await SmarshHistory.findOne({ _id: room._id }); const query = { rid: room._id }; if (smarshHistory) { diff --git a/app/smarsh-connector/server/functions/sendEmail.js b/app/smarsh-connector/server/functions/sendEmail.js index 9b69b05b3ac1..67fcfde02e67 100644 --- a/app/smarsh-connector/server/functions/sendEmail.js +++ b/app/smarsh-connector/server/functions/sendEmail.js @@ -4,19 +4,18 @@ // subject: 'Rocket.Chat, 17 Users, 24 Messages, 1 File, 799504 Minutes, in #random', // files: ['i3nc9l3mn'] // } -import _ from 'underscore'; import { UploadFS } from 'meteor/jalik:ufs'; import * as Mailer from '../../../mailer'; -import { Uploads } from '../../../models'; +import { Uploads } from '../../../models/server/raw'; import { settings } from '../../../settings'; import { smarsh } from '../lib/rocketchat'; -smarsh.sendEmail = (data) => { +smarsh.sendEmail = async (data) => { const attachments = []; - _.each(data.files, (fileId) => { - const file = Uploads.findOneById(fileId); + for await (const fileId of data.files) { + const file = await Uploads.findOneById(fileId); if (file.store === 'rocketchat_uploads' || file.store === 'fileSystem') { const rs = UploadFS.getStore(file.store).getReadStream(fileId, file); attachments.push({ @@ -24,8 +23,7 @@ smarsh.sendEmail = (data) => { streamSource: rs, }); } - }); - + } Mailer.sendNoWrap({ to: settings.get('Smarsh_Email'), diff --git a/app/statistics/server/lib/SAUMonitor.js b/app/statistics/server/lib/SAUMonitor.js index 36b7036fb5a0..9ffa5479a9e2 100644 --- a/app/statistics/server/lib/SAUMonitor.js +++ b/app/statistics/server/lib/SAUMonitor.js @@ -4,9 +4,9 @@ import { SyncedCron } from 'meteor/littledata:synced-cron'; import UAParser from 'ua-parser-js'; import { UAParserMobile, UAParserDesktop } from './UAParserCustom'; -import { Sessions } from '../../../models/server'; +import { Sessions } from '../../../models/server/raw'; +import { aggregates } from '../../../models/server/raw/Sessions'; import { Logger } from '../../../logger'; -import { aggregates } from '../../../models/server/models/Sessions'; import { getMostImportantRole } from './getMostImportantRole'; const getDateObj = (dateTime = new Date()) => ({ @@ -32,7 +32,7 @@ export class SAUMonitorClass { this._jobName = 'aggregate-sessions'; } - start(instanceId) { + async start(instanceId) { if (this.isRunning()) { return; } @@ -44,7 +44,7 @@ export class SAUMonitorClass { return; } - this._startMonitoring(() => { + await this._startMonitoring(() => { this._started = true; logger.debug(`[start] - InstanceId: ${ this._instanceId }`); }); @@ -70,12 +70,12 @@ export class SAUMonitorClass { return this._started === true; } - _startMonitoring(callback) { + async _startMonitoring(callback) { try { this._handleAccountEvents(); this._handleOnConnection(); this._startSessionControl(); - this._initActiveServerSessions(); + await this._initActiveServerSessions(); this._startAggregation(); if (callback) { callback(); @@ -94,8 +94,8 @@ export class SAUMonitorClass { return; } - this._timer = Meteor.setInterval(() => { - this._updateActiveSessions(); + this._timer = Meteor.setInterval(async () => { + await this._updateActiveSessions(); }, this._monitorTime); } @@ -110,8 +110,8 @@ export class SAUMonitorClass { } // this._handleSession(connection, getDateObj()); - connection.onClose(() => { - Sessions.closeByInstanceIdAndSessionId(this._instanceId, connection.id); + connection.onClose(async () => { + await Sessions.closeByInstanceIdAndSessionId(this._instanceId, connection.id); }); }); } @@ -121,7 +121,7 @@ export class SAUMonitorClass { return; } - Accounts.onLogin((info) => { + Accounts.onLogin(async (info) => { if (!this.isRunning()) { return; } @@ -132,11 +132,11 @@ export class SAUMonitorClass { const loginAt = new Date(); const params = { userId, roles, mostImportantRole, loginAt, ...getDateObj() }; - this._handleSession(info.connection, params); + await this._handleSession(info.connection, params); this._updateConnectionInfo(info.connection.id, { loginAt }); }); - Accounts.onLogout((info) => { + Accounts.onLogout(async (info) => { if (!this.isRunning()) { return; } @@ -144,17 +144,17 @@ export class SAUMonitorClass { const sessionId = info.connection.id; if (info.user) { const userId = info.user._id; - Sessions.logoutByInstanceIdAndSessionIdAndUserId(this._instanceId, sessionId, userId); + await Sessions.logoutByInstanceIdAndSessionIdAndUserId(this._instanceId, sessionId, userId); } }); } - _handleSession(connection, params) { + async _handleSession(connection, params) { const data = this._getConnectionInfo(connection, params); - Sessions.createOrUpdate(data); + await Sessions.createOrUpdate(data); } - _updateActiveSessions() { + async _updateActiveSessions() { if (!this.isRunning()) { return; } @@ -167,8 +167,8 @@ export class SAUMonitorClass { const beforeDateTime = new Date(this._today.year, this._today.month - 1, this._today.day, 23, 59, 59, 999); const nextDateTime = new Date(currentDay.year, currentDay.month - 1, currentDay.day); - const createSessions = (objects, ids) => { - Sessions.createBatch(objects); + const createSessions = async (objects, ids) => { + await Sessions.createBatch(objects); Meteor.defer(() => { Sessions.updateActiveSessionsByDateAndInstanceIdAndIds({ year, month, day }, this._instanceId, ids, { lastActivityAt: beforeDateTime }); @@ -180,8 +180,8 @@ export class SAUMonitorClass { } // Otherwise, just update the lastActivityAt field - this._applyAllServerSessionsIds((sessions) => { - Sessions.updateActiveSessionsByDateAndInstanceIdAndIds({ year, month, day }, this._instanceId, sessions, { lastActivityAt: currentDateTime }); + await this._applyAllServerSessionsIds(async (sessions) => { + await Sessions.updateActiveSessionsByDateAndInstanceIdAndIds({ year, month, day }, this._instanceId, sessions, { lastActivityAt: currentDateTime }); }); } @@ -266,32 +266,38 @@ export class SAUMonitorClass { }; } - _initActiveServerSessions() { - this._applyAllServerSessions((connectionHandle) => { - this._handleSession(connectionHandle, getDateObj()); + async _initActiveServerSessions() { + await this._applyAllServerSessions(async (connectionHandle) => { + await this._handleSession(connectionHandle, getDateObj()); }); } - _applyAllServerSessions(callback) { + async _applyAllServerSessions(callback) { if (!callback || typeof callback !== 'function') { return; } const sessions = Object.values(Meteor.server.sessions).filter((session) => session.userId); - sessions.forEach((session) => { - callback(session.connectionHandle); - }); + for await (const session of sessions) { + await callback(session.connectionHandle); + } } - _applyAllServerSessionsIds(callback) { + async recursive(callback, sessionIds) { + await callback(sessionIds.splice(0, 500)); + + if (sessionIds.length) { + await this.recursive(callback, sessionIds); + } + } + + async _applyAllServerSessionsIds(callback) { if (!callback || typeof callback !== 'function') { return; } const sessionIds = Object.values(Meteor.server.sessions).filter((session) => session.userId).map((s) => s.id); - while (sessionIds.length) { - callback(sessionIds.splice(0, 500)); - } + await this.recursive(callback, sessionIds); } _updateConnectionInfo(sessionId, data = {}) { @@ -315,8 +321,8 @@ export class SAUMonitorClass { return Promise.all(arr.splice(0, limit).map((item) => { ids.push(item.id); return this._getConnectionInfo(item.connectionHandle, params); - })).then((data) => { - callback(data, ids); + })).then(async (data) => { + await callback(data, ids); return batch(arr, limit); }).catch((e) => { logger.debug(`Error: ${ e.message }`); @@ -333,13 +339,13 @@ export class SAUMonitorClass { SyncedCron.add({ name: this._jobName, schedule: (parser) => parser.text('at 2:00 am'), - job: () => { - this.aggregate(); + job: async () => { + await this.aggregate(); }, }); } - aggregate() { + async aggregate() { if (!this.isRunning()) { return; } @@ -357,16 +363,16 @@ export class SAUMonitorClass { day: { $lte: yesterday.day }, }; - aggregates.dailySessionsOfYesterday(Sessions.model.rawCollection(), yesterday).forEach(Meteor.bindEnvironment((record) => { + await aggregates.dailySessionsOfYesterday(Sessions.col, yesterday).forEach(async (record) => { record._id = `${ record.userId }-${ record.year }-${ record.month }-${ record.day }`; - Sessions.upsert({ _id: record._id }, record); - })); + await Sessions.updateOne({ _id: record._id }, { $set: record }, { upsert: true }); + }); - Sessions.update(match, { + await Sessions.updateMany(match, { $set: { type: 'computed-session', _computedAt: new Date(), }, - }, { multi: true }); + }); } } diff --git a/app/statistics/server/lib/UAParserCustom.tests.js b/app/statistics/server/lib/UAParserCustom.tests.js index 0d7b2da16f3d..18338bc05104 100644 --- a/app/statistics/server/lib/UAParserCustom.tests.js +++ b/app/statistics/server/lib/UAParserCustom.tests.js @@ -1,5 +1,3 @@ -/* eslint-env mocha */ - import { expect } from 'chai'; import { UAParserMobile, UAParserDesktop } from './UAParserCustom'; diff --git a/app/statistics/server/lib/getMostImportantRole.tests.js b/app/statistics/server/lib/getMostImportantRole.tests.js index 464f14b0e574..5d3c7a6efa84 100644 --- a/app/statistics/server/lib/getMostImportantRole.tests.js +++ b/app/statistics/server/lib/getMostImportantRole.tests.js @@ -1,5 +1,3 @@ -/* eslint-env mocha */ - import { expect } from 'chai'; import { getMostImportantRole } from './getMostImportantRole'; diff --git a/app/statistics/server/lib/getServicesStatistics.ts b/app/statistics/server/lib/getServicesStatistics.ts index faf9a07928c3..0eb444940b3a 100644 --- a/app/statistics/server/lib/getServicesStatistics.ts +++ b/app/statistics/server/lib/getServicesStatistics.ts @@ -11,7 +11,7 @@ function getCustomOAuthServices(): Record(`Accounts_OAuth_Custom-${ name }-merge_roles`), users: Users.countActiveUsersByService(name), }]; })); diff --git a/app/statistics/server/lib/statistics.js b/app/statistics/server/lib/statistics.js index b4ffb9f7dd25..3fa09484ed4d 100644 --- a/app/statistics/server/lib/statistics.js +++ b/app/statistics/server/lib/statistics.js @@ -3,24 +3,21 @@ import os from 'os'; import _ from 'underscore'; import { Meteor } from 'meteor/meteor'; import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; +import { MongoInternals } from 'meteor/mongo'; import { - Sessions, Settings, Users, Rooms, Subscriptions, - Uploads, Messages, LivechatVisitors, - Integrations, - Statistics, } 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 } from '../../../models/server/raw'; +import { NotificationQueue, Users as UsersRaw, Rooms as RoomsRaw, Statistics, Sessions, Integrations, Uploads } from '../../../models/server/raw'; import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { getAppsStatistics } from './getAppsStatistics'; import { getServicesStatistics } from './getServicesStatistics'; @@ -55,9 +52,11 @@ const getUserLanguages = (totalUsers) => { return languages; }; +const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; + export const statistics = { get: function _getStatistics() { - const readPreference = readSecondaryPreferred(Uploads.model.rawDatabase()); + const readPreference = readSecondaryPreferred(db); const statistics = {}; @@ -117,6 +116,17 @@ export const statistics = { // livechat enabled statistics.livechatEnabled = settings.get('Livechat_enabled'); + // Count and types of omnichannel rooms + statistics.omnichannelSources = Promise.await(RoomsRaw.allRoomSourcesCount().toArray()).map(({ + _id: { id, alias, type }, + count, + }) => ({ + id, + alias, + type, + count, + })); + // Message statistics statistics.totalChannelMessages = _.reduce(Rooms.findByType('c', { fields: { msgs: 1 } }).fetch(), function _countChannelMessages(num, room) { return num + room.msgs; }, 0); statistics.totalPrivateGroupMessages = _.reduce(Rooms.findByType('p', { fields: { msgs: 1 } }).fetch(), function _countPrivateGroupMessages(num, room) { return num + room.msgs; }, 0); @@ -162,8 +172,8 @@ export const statistics = { statistics.enterpriseReady = true; - statistics.uploadsTotal = Uploads.find().count(); - const [result] = Promise.await(Uploads.model.rawCollection().aggregate([{ + statistics.uploadsTotal = Promise.await(Uploads.find().count()); + const [result] = Promise.await(Uploads.col.aggregate([{ $group: { _id: 'total', total: { $sum: '$size' } }, }], { readPreference }).toArray()); statistics.uploadsTotalSize = result ? result.total : 0; @@ -176,20 +186,20 @@ export const statistics = { statistics.mongoVersion = mongoVersion; statistics.mongoStorageEngine = mongoStorageEngine; - statistics.uniqueUsersOfYesterday = Sessions.getUniqueUsersOfYesterday(); - statistics.uniqueUsersOfLastWeek = Sessions.getUniqueUsersOfLastWeek(); - statistics.uniqueUsersOfLastMonth = Sessions.getUniqueUsersOfLastMonth(); - statistics.uniqueDevicesOfYesterday = Sessions.getUniqueDevicesOfYesterday(); - statistics.uniqueDevicesOfLastWeek = Sessions.getUniqueDevicesOfLastWeek(); - statistics.uniqueDevicesOfLastMonth = Sessions.getUniqueDevicesOfLastMonth(); - statistics.uniqueOSOfYesterday = Sessions.getUniqueOSOfYesterday(); - statistics.uniqueOSOfLastWeek = Sessions.getUniqueOSOfLastWeek(); - statistics.uniqueOSOfLastMonth = Sessions.getUniqueOSOfLastMonth(); + statistics.uniqueUsersOfYesterday = Promise.await(Sessions.getUniqueUsersOfYesterday()); + statistics.uniqueUsersOfLastWeek = Promise.await(Sessions.getUniqueUsersOfLastWeek()); + statistics.uniqueUsersOfLastMonth = Promise.await(Sessions.getUniqueUsersOfLastMonth()); + statistics.uniqueDevicesOfYesterday = Promise.await(Sessions.getUniqueDevicesOfYesterday()); + statistics.uniqueDevicesOfLastWeek = Promise.await(Sessions.getUniqueDevicesOfLastWeek()); + statistics.uniqueDevicesOfLastMonth = Promise.await(Sessions.getUniqueDevicesOfLastMonth()); + statistics.uniqueOSOfYesterday = Promise.await(Sessions.getUniqueOSOfYesterday()); + statistics.uniqueOSOfLastWeek = Promise.await(Sessions.getUniqueOSOfLastWeek()); + statistics.uniqueOSOfLastMonth = Promise.await(Sessions.getUniqueOSOfLastMonth()); statistics.apps = getAppsStatistics(); statistics.services = getServicesStatistics(); - const integrations = Promise.await(Integrations.model.rawCollection().find({}, { + const integrations = Promise.await(Integrations.find({}, { projection: { _id: 0, type: 1, @@ -215,10 +225,10 @@ export const statistics = { return statistics; }, - save() { + async save() { const rcStatistics = statistics.get(); rcStatistics.createdAt = new Date(); - Statistics.insert(rcStatistics); + await Statistics.insertOne(rcStatistics); return rcStatistics; }, }; diff --git a/app/ui-master/client/main.html b/app/ui-master/client/main.html index f226be71093a..587453a41936 100644 --- a/app/ui-master/client/main.html +++ b/app/ui-master/client/main.html @@ -35,7 +35,6 @@ {{/if}} {{/unless}} {{ CustomScriptLoggedIn }} - {{> photoswipe}} {{/unless}} {{else}} {{> loading}} diff --git a/app/ui-message/client/messageBox/messageBox.js b/app/ui-message/client/messageBox/messageBox.js index 8d67e3466bdc..d8cf0424deff 100644 --- a/app/ui-message/client/messageBox/messageBox.js +++ b/app/ui-message/client/messageBox/messageBox.js @@ -477,6 +477,7 @@ Template.messageBox.events({ data: { rid: this.rid, tmid: this.tmid, + prid: this.subscription.prid, messageBox: instance.firstNode, }, activeElement: event.currentTarget, @@ -494,6 +495,7 @@ Template.messageBox.events({ rid: this.rid, tmid: this.tmid, messageBox: instance.firstNode, + prid: this.subscription.prid, event, }); }); diff --git a/app/ui-utils/client/lib/SideNav.js b/app/ui-utils/client/lib/SideNav.js index fe0b5bab0c10..c6deaa23087a 100644 --- a/app/ui-utils/client/lib/SideNav.js +++ b/app/ui-utils/client/lib/SideNav.js @@ -84,23 +84,6 @@ export const SideNav = new class { return AccountBox.toggle(); } - focusInput() { - const sideNavDivs = Array.from(this.sideNav[0].children).filter((el) => el.tagName === 'DIV' && !el.classList.contains('hidden')); - let highestZidx = 0; - let highestZidxElem; - sideNavDivs.forEach((el) => { - const zIndex = Number(window.getComputedStyle(el).zIndex); - if (zIndex > highestZidx) { - highestZidx = zIndex; - highestZidxElem = el; - } - }); - setTimeout(() => { - const ref = highestZidxElem && highestZidxElem.querySelector('input'); - return ref && ref.focus(); - }, 200); - } - validate() { const invalid = []; this.sideNav.find('input.required').each(function() { @@ -125,7 +108,6 @@ export const SideNav = new class { return; } this.toggleFlex(1, callback); - return this.focusInput(); } init() { diff --git a/app/ui-utils/tests/server.tests.js b/app/ui-utils/tests/server.tests.js index a1592b60602a..5f2dfee9e891 100644 --- a/app/ui-utils/tests/server.tests.js +++ b/app/ui-utils/tests/server.tests.js @@ -1,6 +1,4 @@ -/* eslint-env mocha */ -import 'babel-polyfill'; -import assert from 'assert'; +import { expect } from 'chai'; import './server.mocks.js'; import { messageProperties } from '../lib/MessageProperties'; @@ -16,7 +14,7 @@ describe('Message Properties', () => { describe('Check Message Length', () => { Object.keys(messages).forEach((objectKey) => { it('should treat emojis as single characters', () => { - assert.equal(messageProperties.length(objectKey), messages[objectKey]); + expect(messageProperties.length(objectKey)).to.be.equal(messages[objectKey]); }); }); }); diff --git a/app/ui/client/index.js b/app/ui/client/index.ts similarity index 94% rename from app/ui/client/index.js rename to app/ui/client/index.ts index c30bf6f34370..c577eba51959 100644 --- a/app/ui/client/index.js +++ b/app/ui/client/index.ts @@ -17,7 +17,6 @@ import './views/app/pageCustomContainer.html'; import './views/app/roomSearch.html'; import './views/app/secretURL.html'; import './views/app/userSearch.html'; -import './views/app/photoswipe.html'; import './views/cmsPage'; import './views/404/roomNotFound'; import './views/app/burger'; @@ -25,7 +24,7 @@ import './views/app/home'; import './views/app/roomSearch'; import './views/app/secretURL'; import './views/app/invite'; -import './views/app/photoswipe'; +import './views/app/photoswipeContent.ts'; // without the *.ts extension, *.html gets loaded first import './components/icon'; import './components/table.html'; import './components/table'; diff --git a/app/ui/client/lib/UserAction.ts b/app/ui/client/lib/UserAction.ts index 847de8e19079..8925e0351b99 100644 --- a/app/ui/client/lib/UserAction.ts +++ b/app/ui/client/lib/UserAction.ts @@ -1,7 +1,5 @@ import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; import { ReactiveDict } from 'meteor/reactive-dict'; -import { Session } from 'meteor/session'; import { debounce } from 'lodash'; import { settings } from '../../../settings/client'; @@ -66,10 +64,6 @@ function handleStreamAction(rid: string, username: string, activityTypes: string performingUsers.set(rid, roomActivities); } export const UserAction = new class { - constructor() { - Tracker.autorun(() => Session.get('openedRoom') && this.addStream(Session.get('openedRoom'))); - } - addStream(rid: string): void { if (rooms.get(rid)) { return; diff --git a/app/ui/client/views/app/photoswipe.js b/app/ui/client/views/app/photoswipe.js deleted file mode 100644 index e42ccdfe9495..000000000000 --- a/app/ui/client/views/app/photoswipe.js +++ /dev/null @@ -1,103 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Blaze } from 'meteor/blaze'; -import { Template } from 'meteor/templating'; -import { escapeHTML } from '@rocket.chat/string-helpers'; - -Meteor.startup(() => { - let currentGallery = null; - const initGallery = async (items, options) => { - Blaze.render(Template.photoswipeContent, document.body); - const [PhotoSwipeImport, PhotoSwipeUI_DefaultImport] = await Promise.all([import('photoswipe'), import('photoswipe/dist/photoswipe-ui-default'), import('photoswipe/dist/photoswipe.css')]); - if (!currentGallery) { - const PhotoSwipe = PhotoSwipeImport.default; - const PhotoSwipeUI_Default = PhotoSwipeUI_DefaultImport.default; - currentGallery = new PhotoSwipe(document.getElementById('pswp'), PhotoSwipeUI_Default, items, options); - currentGallery.listen('destroy', () => { - currentGallery = null; - }); - currentGallery.init(); - } - }; - - const defaultGalleryOptions = { - bgOpacity: 0.7, - showHideOpacity: true, - counterEl: false, - shareEl: false, - clickToCloseNonZoomable: false, - }; - - const createEventListenerFor = (className) => (event) => { - event.preventDefault(); - event.stopPropagation(); - - const galleryOptions = { - ...defaultGalleryOptions, - index: 0, - addCaptionHTMLFn(item, captionEl) { - captionEl.children[0].innerHTML = `${ escapeHTML(item.title) }
${ escapeHTML(item.description) }`; - return true; - }, - }; - - const items = Array.from(document.querySelectorAll(className)) - .map((element, i) => { - if (element === event.currentTarget) { - galleryOptions.index = i; - } - - const item = { - src: element.src, - w: element.naturalWidth, - h: element.naturalHeight, - title: element.dataset.title || element.title, - description: element.dataset.description, - }; - - if (element.dataset.src || element.href) { - // use stored sizes if available - if (element.dataset.width && element.dataset.height) { - return { - ...item, - h: element.dataset.height, - w: element.dataset.width, - src: element.dataset.src || element.href, - }; - } - - const img = new Image(); - - img.addEventListener('load', () => { - if (!currentGallery) { - return; - } - - // stores loaded sizes on original image element - element.dataset.width = img.naturalWidth; - element.dataset.height = img.naturalHeight; - - delete currentGallery.items[i].html; - currentGallery.items[i].src = img.src; - currentGallery.items[i].w = img.naturalWidth; - currentGallery.items[i].h = img.naturalHeight; - currentGallery.invalidateCurrItems(); - currentGallery.updateSize(true); - }); - - img.src = element.dataset.src || element.href; - - return { - ...item, - msrc: element.src, - src: element.dataset.src || element.href, - }; - } - - return item; - }); - - initGallery(items, galleryOptions); - }; - - $(document).on('click', '.gallery-item', createEventListenerFor('.gallery-item')); -}); diff --git a/app/ui/client/views/app/photoswipe.html b/app/ui/client/views/app/photoswipeContent.html similarity index 95% rename from app/ui/client/views/app/photoswipe.html rename to app/ui/client/views/app/photoswipeContent.html index d4ee792610e0..0eb80ecbd7fa 100644 --- a/app/ui/client/views/app/photoswipe.html +++ b/app/ui/client/views/app/photoswipeContent.html @@ -1,5 +1,3 @@ - - \ No newline at end of file + diff --git a/app/ui/client/views/app/photoswipeContent.ts b/app/ui/client/views/app/photoswipeContent.ts new file mode 100644 index 000000000000..7e6ee33b9176 --- /dev/null +++ b/app/ui/client/views/app/photoswipeContent.ts @@ -0,0 +1,158 @@ +import { Meteor } from 'meteor/meteor'; +import { Blaze } from 'meteor/blaze'; +import { Template } from 'meteor/templating'; +import { escapeHTML } from '@rocket.chat/string-helpers'; +import type PhotoSwipe from 'photoswipe'; +import type PhotoSwipeUiDefault from 'photoswipe/dist/photoswipe-ui-default'; + +const parseLength = (x: unknown): number | undefined => { + const length = typeof x === 'string' ? parseInt(x, 10) : undefined; + return Number.isFinite(length) ? length : undefined; +}; + +const getImageSize = (src: string): Promise<[w: number, h: number]> => new Promise((resolve, reject) => { + const img = new Image(); + + img.addEventListener('load', () => { + resolve([img.naturalWidth, img.naturalHeight]); + }); + + img.addEventListener('error', (error) => { + reject(error.error); + }); + + img.src = src; +}); + +type Slide = PhotoSwipeUiDefault.Item & { description?: string }; + +const fromElementToSlide = async (element: Element): Promise => { + if (!(element instanceof HTMLElement)) { + return null; + } + + const title = element.dataset.title || element.title; + const { description } = element.dataset; + + if (element instanceof HTMLAnchorElement) { + const src = element.dataset.src || element.href; + let w = parseLength(element.dataset.width); + let h = parseLength(element.dataset.height); + + if (w === undefined || h === undefined) { + [w, h] = await getImageSize(src); + } + + return { src, w, h, title, description }; + } + + if (element instanceof HTMLImageElement) { + let msrc: string | undefined; + let { src } = element; + let w: number | undefined = element.naturalWidth; + let h: number | undefined = element.naturalHeight; + + if (element.dataset.src) { + msrc = element.src; + src = element.dataset.src; + w = parseLength(element.dataset.width); + h = parseLength(element.dataset.height); + + if (w === undefined || h === undefined) { + [w, h] = await getImageSize(src); + } + } + + return { msrc, src, w, h, title, description }; + } + + return null; +}; + +let currentGallery: PhotoSwipe | null = null; + +const initGallery = async (items: Slide[], options: PhotoSwipeUiDefault.Options): Promise => { + const [ + { default: PhotoSwipe }, + { default: PhotoSwipeUiDefault }, // eslint-disable-line @typescript-eslint/camelcase + ] = await Promise.all([ + import('photoswipe'), + import('photoswipe/dist/photoswipe-ui-default'), + // @ts-ignore + import('photoswipe/dist/photoswipe.css'), + // @ts-ignore + import('./photoswipeContent.html'), + ]); + + Blaze.render(Template.photoswipeContent, document.body); + + if (!currentGallery) { + const container = document.getElementById('pswp'); + + if (!container) { + throw new Error('Photoswipe container element not found'); + } + + currentGallery = new PhotoSwipe(container, PhotoSwipeUiDefault, items, options); + + currentGallery.listen('destroy', () => { + currentGallery = null; + }); + + currentGallery.init(); + } +}; + +const defaultGalleryOptions: PhotoSwipeUiDefault.Options = { + bgOpacity: 0.7, + showHideOpacity: true, + counterEl: false, + shareEl: false, + clickToCloseNonZoomable: false, + index: 0, + addCaptionHTMLFn(item: Slide, captionEl: HTMLElement): boolean { + captionEl.children[0].innerHTML = ` + ${ escapeHTML(item.title ?? '') }
+ ${ escapeHTML(item.description ?? '') } + `; + return true; + }, +}; + +const createEventListenerFor = (className: string) => (event: JQuery.ClickEvent): void => { + event.preventDefault(); + event.stopPropagation(); + + const { currentTarget } = event; + + Array.from(document.querySelectorAll(className)) + .sort((a, b) => { + if (a === currentTarget) { + return -1; + } + + if (b === currentTarget) { + return 1; + } + + return 0; + }) + .map((element) => fromElementToSlide(element)) + .reduce((p, curr) => p.then(() => curr).then(async (slide) => { + if (!slide) { + return; + } + + if (!currentGallery) { + return initGallery([slide], defaultGalleryOptions); + } + + currentGallery.items.push(slide); + currentGallery.invalidateCurrItems(); + currentGallery.updateSize(true); + }), Promise.resolve()); +}; + +Meteor.startup(() => { + $(document).on('click', '.gallery-item', createEventListenerFor('.gallery-item')); +}); diff --git a/app/ui/client/views/app/tests/helpers.tests.js b/app/ui/client/views/app/tests/helpers.tests.js index 110a050454eb..77487ec1ea4c 100644 --- a/app/ui/client/views/app/tests/helpers.tests.js +++ b/app/ui/client/views/app/tests/helpers.tests.js @@ -1,6 +1,4 @@ -/* eslint-env mocha */ -import 'babel-polyfill'; -import assert from 'assert'; +import { expect } from 'chai'; import { timeAgo } from '../helpers'; @@ -16,9 +14,9 @@ describe('Helpers', () => { const func = (a) => a; - assert.equal(timeAgo(t1, func, now), '1:00 AM'); - assert.equal(timeAgo(t2, func, now), '10:00 AM'); - assert.equal(timeAgo(t3, func, now), '2:30 PM'); + expect(timeAgo(t1, func, now)).to.be.equal('1:00 AM'); + expect(timeAgo(t2, func, now)).to.be.equal('10:00 AM'); + expect(timeAgo(t3, func, now)).to.be.equal('2:30 PM'); }); it('returns "yesterday" when the passed value is on the day before', () => { @@ -30,9 +28,9 @@ describe('Helpers', () => { const func = (a) => a; - assert.equal(timeAgo(t1, func, now), 'yesterday'); - assert.equal(timeAgo(t2, func, now), 'yesterday'); - assert.equal(timeAgo(t3, func, now), 'yesterday'); + expect(timeAgo(t1, func, now)).to.be.equal('yesterday'); + expect(timeAgo(t2, func, now)).to.be.equal('yesterday'); + expect(timeAgo(t3, func, now)).to.be.equal('yesterday'); }); it('returns formated date when the passed value two or more days before', () => { @@ -46,11 +44,11 @@ describe('Helpers', () => { const func = () => 'should not be called'; - assert.equal(timeAgo(t1, func, now), 'Jun 18, 2018'); - assert.equal(timeAgo(t2, func, now), 'Jun 10, 2018'); - assert.equal(timeAgo(t3, func, now), 'May 10, 2018'); - assert.equal(timeAgo(t4, func, now), 'May 20, 2018'); - assert.equal(timeAgo(t5, func, now), 'Nov 10, 2017'); + expect(timeAgo(t1, func, now)).to.be.equal('Jun 18, 2018'); + expect(timeAgo(t2, func, now)).to.be.equal('Jun 10, 2018'); + expect(timeAgo(t3, func, now)).to.be.equal('May 10, 2018'); + expect(timeAgo(t4, func, now)).to.be.equal('May 20, 2018'); + expect(timeAgo(t5, func, now)).to.be.equal('Nov 10, 2017'); }); }); }); diff --git a/app/ui/index.js b/app/ui/index.ts similarity index 100% rename from app/ui/index.js rename to app/ui/index.ts diff --git a/app/user-data-download/server/cronProcessDownloads.js b/app/user-data-download/server/cronProcessDownloads.js index 8fb19b86b08c..35c8de44af20 100644 --- a/app/user-data-download/server/cronProcessDownloads.js +++ b/app/user-data-download/server/cronProcessDownloads.js @@ -10,7 +10,8 @@ import moment from 'moment'; import { v4 as uuidv4 } from 'uuid'; import { settings } from '../../settings/server'; -import { Subscriptions, Rooms, Users, Uploads, Messages, UserDataFiles, ExportOperations, Avatars } from '../../models/server'; +import { Subscriptions, Rooms, Users, Messages } from '../../models/server'; +import { Avatars, ExportOperations, UserDataFiles, Uploads } from '../../models/server/raw'; import { FileUpload } from '../../file-upload/server'; import { DataExport } from './DataExport'; import * as Mailer from '../../mailer'; @@ -186,8 +187,8 @@ const getMessageData = function(msg, hideUsers, userData, usersMap) { return messageObject; }; -export const copyFile = function(attachmentData, assetsPath) { - const file = Uploads.findOneById(attachmentData._id); +export const copyFile = async function(attachmentData, assetsPath) { + const file = await Uploads.findOneById(attachmentData._id); if (!file) { return; } @@ -439,12 +440,12 @@ const generateUserFile = function(exportOperation, userData) { } }; -const generateUserAvatarFile = function(exportOperation, userData) { +const generateUserAvatarFile = async function(exportOperation, userData) { if (!userData) { return; } - const file = Avatars.findOneByName(userData.username); + const file = await Avatars.findOneByName(userData.username); if (!file) { return; } @@ -478,7 +479,7 @@ const continueExportOperation = async function(exportOperation) { } if (!exportOperation.generatedAvatar) { - generateUserAvatarFile(exportOperation, exportOperation.userData); + await generateUserAvatarFile(exportOperation, exportOperation.userData); } if (exportOperation.status === 'exporting-rooms') { @@ -511,9 +512,9 @@ const continueExportOperation = async function(exportOperation) { const generatedFileName = uuidv4(); if (exportOperation.status === 'downloading') { - exportOperation.fileList.forEach((attachmentData) => { - copyFile(attachmentData, exportOperation.assetsPath); - }); + for await (const attachmentData of exportOperation.fileList) { + await copyFile(attachmentData, exportOperation.assetsPath); + } const targetFile = joinPath(zipFolder, `${ generatedFileName }.zip`); if (await fsExists(targetFile)) { @@ -539,17 +540,17 @@ const continueExportOperation = async function(exportOperation) { exportOperation.fileId = fileId; exportOperation.status = 'completed'; - ExportOperations.updateOperation(exportOperation); + await ExportOperations.updateOperation(exportOperation); } - ExportOperations.updateOperation(exportOperation); + await ExportOperations.updateOperation(exportOperation); } catch (e) { console.error(e); } }; async function processDataDownloads() { - const operation = ExportOperations.findOnePending(); + const operation = await ExportOperations.findOnePending(); if (!operation) { return; } @@ -571,7 +572,7 @@ async function processDataDownloads() { await ExportOperations.updateOperation(operation); if (operation.status === 'completed') { - const file = operation.fileId ? UserDataFiles.findOneById(operation.fileId) : UserDataFiles.findLastFileByUser(operation.userId); + const file = operation.fileId ? await UserDataFiles.findOneById(operation.fileId) : await UserDataFiles.findLastFileByUser(operation.userId); if (!file) { return; } diff --git a/app/user-data-download/server/exportDownload.js b/app/user-data-download/server/exportDownload.js index d99eda5c88e6..6ea406de307c 100644 --- a/app/user-data-download/server/exportDownload.js +++ b/app/user-data-download/server/exportDownload.js @@ -1,12 +1,12 @@ import { WebApp } from 'meteor/webapp'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { UserDataFiles } from '../../models'; +import { UserDataFiles } from '../../models/server/raw'; import { DataExport } from './DataExport'; import { settings } from '../../settings/server'; -WebApp.connectHandlers.use(DataExport.getPath(), function(req, res, next) { +WebApp.connectHandlers.use(DataExport.getPath(), async function(req, res, next) { const match = /^\/([^\/]+)/.exec(req.url); if (!settings.get('UserData_EnableDownload')) { @@ -16,7 +16,7 @@ WebApp.connectHandlers.use(DataExport.getPath(), function(req, res, next) { } if (match && match[1]) { - const file = UserDataFiles.findOneById(match[1]); + const file = await UserDataFiles.findOneById(match[1]); if (file) { if (!DataExport.requestCanAccessFiles(req, file.userId)) { res.setHeader('Content-Type', 'text/html; charset=UTF-8'); diff --git a/app/user-status/server/methods/deleteCustomUserStatus.js b/app/user-status/server/methods/deleteCustomUserStatus.js index e81a8140d718..6935266a7475 100644 --- a/app/user-status/server/methods/deleteCustomUserStatus.js +++ b/app/user-status/server/methods/deleteCustomUserStatus.js @@ -1,21 +1,21 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization/server'; -import { CustomUserStatus } from '../../../models/server'; +import { CustomUserStatus } from '../../../models/server/raw'; import { api } from '../../../../server/sdk/api'; Meteor.methods({ - deleteCustomUserStatus(userStatusID) { + async deleteCustomUserStatus(userStatusID) { if (!hasPermission(this.userId, 'manage-user-status')) { throw new Meteor.Error('not_authorized'); } - const userStatus = CustomUserStatus.findOneById(userStatusID); + const userStatus = await CustomUserStatus.findOneById(userStatusID); if (userStatus == null) { throw new Meteor.Error('Custom_User_Status_Error_Invalid_User_Status', 'Invalid user status', { method: 'deleteCustomUserStatus' }); } - CustomUserStatus.removeById(userStatusID); + await CustomUserStatus.removeById(userStatusID); api.broadcast('user.deleteCustomStatus', userStatus); return true; diff --git a/app/user-status/server/methods/insertOrUpdateUserStatus.js b/app/user-status/server/methods/insertOrUpdateUserStatus.js index a01cc751c525..ebc0e918acba 100644 --- a/app/user-status/server/methods/insertOrUpdateUserStatus.js +++ b/app/user-status/server/methods/insertOrUpdateUserStatus.js @@ -2,11 +2,11 @@ import { Meteor } from 'meteor/meteor'; import s from 'underscore.string'; import { hasPermission } from '../../../authorization'; -import { CustomUserStatus } from '../../../models'; +import { CustomUserStatus } from '../../../models/server/raw'; import { api } from '../../../../server/sdk/api'; Meteor.methods({ - insertOrUpdateUserStatus(userStatusData) { + async insertOrUpdateUserStatus(userStatusData) { if (!hasPermission(this.userId, 'manage-user-status')) { throw new Meteor.Error('not_authorized'); } @@ -26,9 +26,9 @@ Meteor.methods({ let matchingResults = []; if (userStatusData._id) { - matchingResults = CustomUserStatus.findByNameExceptId(userStatusData.name, userStatusData._id).fetch(); + matchingResults = await CustomUserStatus.findByNameExceptId(userStatusData.name, userStatusData._id).toArray(); } else { - matchingResults = CustomUserStatus.findByName(userStatusData.name).fetch(); + matchingResults = await CustomUserStatus.findByName(userStatusData.name).toArray(); } if (matchingResults.length > 0) { @@ -47,7 +47,7 @@ Meteor.methods({ statusType: userStatusData.statusType || null, }; - const _id = CustomUserStatus.create(createUserStatus); + const _id = await (await CustomUserStatus.create(createUserStatus)).insertedId; api.broadcast('user.updateCustomStatus', createUserStatus); @@ -56,11 +56,11 @@ Meteor.methods({ // update User status if (userStatusData.name !== userStatusData.previousName) { - CustomUserStatus.setName(userStatusData._id, userStatusData.name); + await CustomUserStatus.setName(userStatusData._id, userStatusData.name); } if (userStatusData.statusType !== userStatusData.previousStatusType) { - CustomUserStatus.setStatusType(userStatusData._id, userStatusData.statusType); + await CustomUserStatus.setStatusType(userStatusData._id, userStatusData.statusType); } api.broadcast('user.updateCustomStatus', userStatusData); diff --git a/app/user-status/server/methods/listCustomUserStatus.js b/app/user-status/server/methods/listCustomUserStatus.js index a47f1ff01bc2..b8ec637d99b6 100644 --- a/app/user-status/server/methods/listCustomUserStatus.js +++ b/app/user-status/server/methods/listCustomUserStatus.js @@ -1,14 +1,14 @@ import { Meteor } from 'meteor/meteor'; -import { CustomUserStatus } from '../../../models'; +import { CustomUserStatus } from '../../../models/server/raw'; Meteor.methods({ - listCustomUserStatus() { + async listCustomUserStatus() { const currentUserId = Meteor.userId(); if (!currentUserId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'listCustomUserStatus' }); } - return CustomUserStatus.find({}).fetch(); + return CustomUserStatus.find({}).toArray(); }, }); diff --git a/app/utils/client/lib/RestApiClient.d.ts b/app/utils/client/lib/RestApiClient.d.ts new file mode 100644 index 000000000000..c9d4c225488b --- /dev/null +++ b/app/utils/client/lib/RestApiClient.d.ts @@ -0,0 +1,44 @@ +import { Serialized } from '../../../../definition/Serialized'; + +export declare const APIClient: { + delete(endpoint: string, params?: Serialized

): Promise>; + get(endpoint: string, params?: void extends P ? void : Serialized

): Promise>; + post(endpoint: string, params?: Serialized

, body?: B): Promise>; + upload( + endpoint: string, + params?: Serialized

, + formData?: B, + xhrOptions?: { + progress: (amount: number) => void; + error: (ev: ProgressEvent) => void; + } + ): { promise: Promise> }; + getCredentials(): { + 'X-User-Id': string; + 'X-Auth-Token': string; + }; + _jqueryCall( + method?: string, + endpoint?: string, + params?: any, + body?: any, + headers?: Record, + dataType?: string + ): any; + v1: { + delete(endpoint: string, params?: Serialized

): Promise>; + get(endpoint: string, params?: Serialized

): Promise>; + post(endpoint: string, params?: Serialized

, body?: B): Promise>; + put(endpoint: string, params?: Serialized

, body?: B): Promise>; + upload( + endpoint: string, + params?: Serialized

, + formData?: B, + xhrOptions?: { + progress: (amount: number) => void; + error: (ev: ProgressEvent) => void; + } + ): { promise: Promise> }; + }; +}; diff --git a/app/utils/client/lib/RestApiClient.js b/app/utils/client/lib/RestApiClient.js index 9d857af0d580..38038e014247 100644 --- a/app/utils/client/lib/RestApiClient.js +++ b/app/utils/client/lib/RestApiClient.js @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; +import jQuery from 'jquery'; import { process2faReturn } from '../../../../client/lib/2fa/process2faReturn'; import { baseURI } from '../../../../client/lib/baseURI'; @@ -22,6 +23,15 @@ export const APIClient = { return APIClient._jqueryCall('POST', endpoint, params, body); }, + put(endpoint, params, body) { + if (!body) { + body = params; + params = {}; + } + + return APIClient._jqueryCall('PUT', endpoint, params, body); + }, + upload(endpoint, params, formData, xhrOptions) { if (!formData) { formData = params; @@ -168,5 +178,9 @@ export const APIClient = { upload(endpoint, params, formData) { return APIClient.upload(`v1/${ endpoint }`, params, formData); }, + + put(endpoint, params, body) { + return APIClient.put(`v1/${ endpoint }`, params, body); + }, }, }; diff --git a/app/utils/lib/getURL.tests.js b/app/utils/lib/getURL.tests.js index 73d6d0186550..1cccb926d941 100644 --- a/app/utils/lib/getURL.tests.js +++ b/app/utils/lib/getURL.tests.js @@ -1,8 +1,4 @@ -/* eslint-disable complexity */ -/* eslint-env mocha */ -import 'babel-polyfill'; -import assert from 'assert'; - +import { expect } from 'chai'; import s from 'underscore.string'; import { _getURL } from './getURL'; @@ -13,17 +9,17 @@ const testPaths = (o, _processPath) => { processPath = (path) => _processPath(o._root_url_path_prefix + path); } - assert.equal(_getURL('', o), processPath('')); - assert.equal(_getURL('/', o), processPath('')); - assert.equal(_getURL('//', o), processPath('')); - assert.equal(_getURL('///', o), processPath('')); - assert.equal(_getURL('/channel', o), processPath('/channel')); - assert.equal(_getURL('/channel/', o), processPath('/channel')); - assert.equal(_getURL('/channel//', o), processPath('/channel')); - assert.equal(_getURL('/channel/123', o), processPath('/channel/123')); - assert.equal(_getURL('/channel/123/', o), processPath('/channel/123')); - assert.equal(_getURL('/channel/123?id=456&name=test', o), processPath('/channel/123?id=456&name=test')); - assert.equal(_getURL('/channel/123/?id=456&name=test', o), processPath('/channel/123?id=456&name=test')); + expect(_getURL('', o)).to.be.equal(processPath('')); + expect(_getURL('/', o)).to.be.equal(processPath('')); + expect(_getURL('//', o)).to.be.equal(processPath('')); + expect(_getURL('///', o)).to.be.equal(processPath('')); + expect(_getURL('/channel', o)).to.be.equal(processPath('/channel')); + expect(_getURL('/channel/', o)).to.be.equal(processPath('/channel')); + expect(_getURL('/channel//', o)).to.be.equal(processPath('/channel')); + expect(_getURL('/channel/123', o)).to.be.equal(processPath('/channel/123')); + expect(_getURL('/channel/123/', o)).to.be.equal(processPath('/channel/123')); + expect(_getURL('/channel/123?id=456&name=test', o)).to.be.equal(processPath('/channel/123?id=456&name=test')); + expect(_getURL('/channel/123/?id=456&name=test', o)).to.be.equal(processPath('/channel/123?id=456&name=test')); }; const getCloudUrl = (_site_url, path) => { @@ -89,12 +85,6 @@ const testCases = (options) => { } } } else if (options._cdn_prefix === '') { - if (options.full && !options.cdn && !options.cloud) { - it('should return with host if full: true', () => { - testPaths(options, (path) => _site_url + path); - }); - } - if (!options.full && options.cdn) { it('should return with cloud host if cdn: true', () => { testPaths(options, (path) => getCloudUrl(_site_url, path)); @@ -106,88 +96,54 @@ const testCases = (options) => { testPaths(options, (path) => getCloudUrl(_site_url, path)); }); } - - if (options.full && options.cdn && !options.cloud) { - it('should return with host if full: true and cdn: true', () => { - testPaths(options, (path) => _site_url + path); - }); - } - } else { - if (options.full && !options.cdn && !options.cloud) { - it('should return with host if full: true', () => { - testPaths(options, (path) => _site_url + path); - }); - } - - if (!options.full && options.cdn && !options.cloud) { - it('should return with cdn prefix if cdn: true', () => { - testPaths(options, (path) => options._cdn_prefix + path); - }); - } - - if (!options.full && !options.cdn) { - it('should return with cloud host if full: fase and cdn: false', () => { - testPaths(options, (path) => getCloudUrl(_site_url, path)); - }); - } - - if (options.full && options.cdn && !options.cloud) { - it('should return with host if full: true and cdn: true', () => { - testPaths(options, (path) => options._cdn_prefix + path); - }); - } + } else if (!options.full && !options.cdn) { + it('should return with cloud host if full: fase and cdn: false', () => { + testPaths(options, (path) => getCloudUrl(_site_url, path)); + }); } }; -const testOptions = (options) => { - testCases({ ...options, cdn: false, full: false, cloud: false }); - testCases({ ...options, cdn: true, full: false, cloud: false }); - testCases({ ...options, cdn: false, full: true, cloud: false }); - testCases({ ...options, cdn: false, full: false, cloud: true }); - testCases({ ...options, cdn: true, full: true, cloud: false }); - testCases({ ...options, cdn: false, full: true, cloud: true }); - testCases({ ...options, cdn: true, full: false, cloud: true }); - testCases({ ...options, cdn: true, full: true, cloud: true }); +const testCasesForOptions = (description, options) => { + describe(description, () => { + testCases({ ...options, cdn: false, full: false, cloud: false }); + testCases({ ...options, cdn: true, full: false, cloud: false }); + testCases({ ...options, cdn: false, full: true, cloud: false }); + testCases({ ...options, cdn: false, full: false, cloud: true }); + testCases({ ...options, cdn: true, full: true, cloud: false }); + testCases({ ...options, cdn: false, full: true, cloud: true }); + testCases({ ...options, cdn: true, full: false, cloud: true }); + testCases({ ...options, cdn: true, full: true, cloud: true }); + }); }; describe('getURL', () => { - describe('getURL with no CDN, no PREFIX for http://localhost:3000/', () => { - testOptions({ - _cdn_prefix: '', - _root_url_path_prefix: '', - _site_url: 'http://localhost:3000/', - }); + testCasesForOptions('getURL with no CDN, no PREFIX for http://localhost:3000/', { + _cdn_prefix: '', + _root_url_path_prefix: '', + _site_url: 'http://localhost:3000/', }); - describe('getURL with no CDN, no PREFIX for http://localhost:3000', () => { - testOptions({ - _cdn_prefix: '', - _root_url_path_prefix: '', - _site_url: 'http://localhost:3000', - }); + testCasesForOptions('getURL with no CDN, no PREFIX for http://localhost:3000', { + _cdn_prefix: '', + _root_url_path_prefix: '', + _site_url: 'http://localhost:3000', }); - describe('getURL with CDN, no PREFIX for http://localhost:3000/', () => { - testOptions({ - _cdn_prefix: 'https://cdn.com', - _root_url_path_prefix: '', - _site_url: 'http://localhost:3000/', - }); + testCasesForOptions('getURL with CDN, no PREFIX for http://localhost:3000/', { + _cdn_prefix: 'https://cdn.com', + _root_url_path_prefix: '', + _site_url: 'http://localhost:3000/', }); - describe('getURL with CDN, PREFIX for http://localhost:3000/', () => { - testOptions({ - _cdn_prefix: 'https://cdn.com', - _root_url_path_prefix: 'sub', - _site_url: 'http://localhost:3000/', - }); + testCasesForOptions('getURL with CDN, PREFIX for http://localhost:3000/', { + _cdn_prefix: 'https://cdn.com', + _root_url_path_prefix: 'sub', + _site_url: 'http://localhost:3000/', }); - describe('getURL with CDN, PREFIX for https://localhost:3000/', () => { - testOptions({ - _cdn_prefix: 'https://cdn.com', - _root_url_path_prefix: 'sub', - _site_url: 'https://localhost:3000/', - }); + testCasesForOptions('getURL with CDN, PREFIX for https://localhost:3000/', { + _cdn_prefix: 'https://cdn.com', + _root_url_path_prefix: 'sub', + _site_url: 'https://localhost:3000/', }); }); diff --git a/app/utils/rocketchat.info b/app/utils/rocketchat.info index d767f4275484..a0aa10e4c04b 100644 --- a/app/utils/rocketchat.info +++ b/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "4.1.2" + "version": "4.2.0" } diff --git a/app/utils/server/functions/normalizeMessageFileUpload.js b/app/utils/server/functions/normalizeMessageFileUpload.js index 450396b0b765..948dc047c252 100644 --- a/app/utils/server/functions/normalizeMessageFileUpload.js +++ b/app/utils/server/functions/normalizeMessageFileUpload.js @@ -1,11 +1,11 @@ import { getURL } from '../../lib/getURL'; import { FileUpload } from '../../../file-upload/server'; -import { Uploads } from '../../../models/server'; +import { Uploads } from '../../../models/server/raw'; -export const normalizeMessageFileUpload = (message) => { +export const normalizeMessageFileUpload = async (message) => { if (message.file && !message.fileUpload) { const jwt = FileUpload.generateJWTToFileUrls({ rid: message.rid, userId: message.u._id, fileId: message.file._id }); - const file = Uploads.findOne({ _id: message.file._id }); + const file = await Uploads.findOne({ _id: message.file._id }); if (!file) { return message; } diff --git a/app/utils/server/lib/cron/Cronjobs.ts b/app/utils/server/lib/cron/Cronjobs.ts index 8ab96121e54f..7223330b9639 100644 --- a/app/utils/server/lib/cron/Cronjobs.ts +++ b/app/utils/server/lib/cron/Cronjobs.ts @@ -1,12 +1,6 @@ import { SyncedCron } from 'meteor/littledata:synced-cron'; -type ScheduleType = 'cron' | 'text'; - -export interface ICronJobs { - add(name: string, schedule: string, callback: Function, scheduleType?: ScheduleType): void; - remove(name: string): void; - nextScheduledAtDate(name: string): Date | number | undefined; -} +import { ICronJobs, ScheduleType } from '../../../../../definition/ICronJobs'; class SyncedCronJobs implements ICronJobs { add(name: string, schedule: string, callback: Function, scheduleType: ScheduleType = 'cron'): void { diff --git a/app/version-check/server/functions/checkVersionUpdate.js b/app/version-check/server/functions/checkVersionUpdate.js index 65cd246d663c..9933081a038c 100644 --- a/app/version-check/server/functions/checkVersionUpdate.js +++ b/app/version-check/server/functions/checkVersionUpdate.js @@ -42,7 +42,7 @@ export default () => { if (update.exists) { Settings.updateValueById('Update_LatestAvailableVersion', update.lastestVersion.version); - sendMessagesToAdmins({ + Promise.await(sendMessagesToAdmins({ msgs: ({ adminUser }) => [{ msg: `*${ TAPi18n.__('Update_your_RocketChat', adminUser.language) }*\n${ TAPi18n.__('New_version_available_(s)', update.lastestVersion.version, adminUser.language) }\n${ update.lastestVersion.infoUrl }` }], banners: [{ id: `versionUpdate-${ update.lastestVersion.version }`.replace(/\./g, '_'), @@ -52,11 +52,11 @@ export default () => { textArguments: [update.lastestVersion.version], link: update.lastestVersion.infoUrl, }], - }); + })); } if (alerts && alerts.length) { - sendMessagesToAdmins({ + Promise.await(sendMessagesToAdmins({ msgs: ({ adminUser }) => alerts .filter((alert) => !Users.bannerExistsById(adminUser._id, `alert-${ alert.id }`)) .map((alert) => ({ @@ -71,6 +71,6 @@ export default () => { modifiers: alert.modifiers, link: alert.infoUrl, })), - }); + })); } }; diff --git a/app/videobridge/client/tabBar.tsx b/app/videobridge/client/tabBar.tsx index d67fc0b88a20..b8ddf4ae6eb0 100644 --- a/app/videobridge/client/tabBar.tsx +++ b/app/videobridge/client/tabBar.tsx @@ -53,12 +53,13 @@ addAction('video', ({ room }) => { const enabledChannel = useSetting('Jitsi_Enable_Channels'); const enabledTeams = useSetting('Jitsi_Enable_Teams'); + const enabledLiveChat = useSetting('Omnichannel_call_provider') === 'Jitsi'; const groups = useStableArray([ 'direct', 'direct_multiple', 'group', - 'live', + enabledLiveChat && 'live', enabledTeams && 'team', enabledChannel && 'channel', ].filter(Boolean) as ToolboxActionConfig['groups']); diff --git a/app/videobridge/server/methods/jitsiSetTimeout.js b/app/videobridge/server/methods/jitsiSetTimeout.js index bfb109efd322..096304923a34 100644 --- a/app/videobridge/server/methods/jitsiSetTimeout.js +++ b/app/videobridge/server/methods/jitsiSetTimeout.js @@ -7,6 +7,13 @@ import { metrics } from '../../../metrics/server'; import * as CONSTANTS from '../../constants'; import { canSendMessage } from '../../../authorization/server'; import { SystemLogger } from '../../../../server/lib/logger/system'; +import { settings } from '../../../settings'; + +// TODO: Access Token missing. This is just a partial solution, it doesn't handle access token generation logic as present in this file - client/views/room/contextualBar/Call/Jitsi/CallJitsWithData.js +const resolveJitsiCallUrl = (room) => { + const rname = settings.get('Jitsi_URL_Room_Hash') ? settings.get('uniqueID') + room._id : encodeURIComponent(room.t === 'd' ? room.usernames.join(' x ') : room.name); + return `${ settings.get('Jitsi_SSL') ? 'https://' : 'http://' }${ settings.get('Jitsi_Domain') }/${ settings.get('Jitsi_URL_Room_Prefix') }${ rname }${ settings.get('Jitsi_URL_Room_Suffix') }`; +}; Meteor.methods({ 'jitsi:updateTimeout': (rid, joiningNow = true) => { @@ -43,6 +50,10 @@ Meteor.methods({ actionLinks: [ { icon: 'icon-videocam', label: TAPi18n.__('Click_to_join'), i18nLabel: 'Click_to_join', method_id: 'joinJitsiCall', params: '' }, ], + customFields: { + ...room.customFields && { ...room.customFields }, + ...room.t === 'l' && { jitsiCallUrl: resolveJitsiCallUrl(room) }, // Note: this is just a temporary solution for the jitsi calls to work in Livechat. In future we wish to create specific events for specific to livechat calls (eg: start, accept, decline, end, etc) and this url info will be passed via there + }, }); message.msg = TAPi18n.__('Started_a_video_call'); callbacks.run('afterSaveMessage', message, { ...room, jitsiTimeout: currentTime + CONSTANTS.TIMEOUT }); diff --git a/app/videobridge/server/settings.ts b/app/videobridge/server/settings.ts index 09b14ddc9b65..b5cbdace307b 100644 --- a/app/videobridge/server/settings.ts +++ b/app/videobridge/server/settings.ts @@ -139,7 +139,7 @@ settingsRegistry.addGroup('Video Conference', function() { public: true, }); - this.add('Jitsi_Open_New_Window', false, { + this.add('Jitsi_Open_New_Window', true, { type: 'boolean', enableQuery: { _id: 'Jitsi_Enabled', diff --git a/app/webdav/client/actionButton.js b/app/webdav/client/actionButton.js index 6b72d8785711..cbfd32da1c3f 100644 --- a/app/webdav/client/actionButton.js +++ b/app/webdav/client/actionButton.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { t, getURL } from '../../utils'; -import { WebdavAccounts } from '../../models'; +import { WebdavAccounts } from '../../models/client'; import { settings } from '../../settings'; import { MessageAction, modal } from '../../ui-utils'; import { messageArgs } from '../../ui-utils/client/lib/messageArgs'; diff --git a/app/webdav/client/selectWebdavAccount.js b/app/webdav/client/selectWebdavAccount.js index 5acb7f43f9c4..14f771822cc4 100644 --- a/app/webdav/client/selectWebdavAccount.js +++ b/app/webdav/client/selectWebdavAccount.js @@ -3,7 +3,7 @@ import { Template } from 'meteor/templating'; import { modal } from '../../ui-utils'; import { t } from '../../utils'; -import { WebdavAccounts } from '../../models'; +import { WebdavAccounts } from '../../models/client'; import { dispatchToastMessage } from '../../../client/lib/toast'; Template.selectWebdavAccount.helpers({ diff --git a/app/webdav/client/startup/messageBoxActions.js b/app/webdav/client/startup/messageBoxActions.js index a209259d4aaa..be63edecd599 100644 --- a/app/webdav/client/startup/messageBoxActions.js +++ b/app/webdav/client/startup/messageBoxActions.js @@ -4,7 +4,7 @@ import { Tracker } from 'meteor/tracker'; import { t } from '../../../utils'; import { settings } from '../../../settings'; import { messageBox, modal } from '../../../ui-utils'; -import { WebdavAccounts } from '../../../models'; +import { WebdavAccounts } from '../../../models/client'; messageBox.actions.add('WebDAV', 'Add Server', { id: 'add-webdav', diff --git a/app/webdav/server/lib/webdavClientAdapter.ts b/app/webdav/server/lib/webdavClientAdapter.ts index 038d0907c8fd..59e4b70f3f2f 100644 --- a/app/webdav/server/lib/webdavClientAdapter.ts +++ b/app/webdav/server/lib/webdavClientAdapter.ts @@ -1,6 +1,4 @@ -import { createClient } from 'webdav'; - -import type { WebDavClient, Stat } from '../../../../definition/webdav'; +import { createClient, WebDavClient, Stat } from 'webdav'; export type ServerCredentials = { token?: string; diff --git a/app/webdav/server/methods/addWebdavAccount.js b/app/webdav/server/methods/addWebdavAccount.js index 3bfc6aef3495..fdaf715903fa 100644 --- a/app/webdav/server/methods/addWebdavAccount.js +++ b/app/webdav/server/methods/addWebdavAccount.js @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { settings } from '../../../settings'; -import { WebdavAccounts } from '../../../models'; +import { settings } from '../../../settings/server'; +import { WebdavAccounts } from '../../../models/server/raw'; import { WebdavClientAdapter } from '../lib/webdavClientAdapter'; import { Notifications } from '../../../notifications/server'; @@ -24,7 +24,7 @@ Meteor.methods({ pass: String, })); - const duplicateAccount = WebdavAccounts.findOne({ user_id: userId, server_url: formData.serverURL, username: formData.username }); + const duplicateAccount = await WebdavAccounts.findOneByUserIdServerUrlAndUsername({ user_id: userId, server_url: formData.serverURL, username: formData.username }); if (duplicateAccount !== undefined) { throw new Meteor.Error('duplicated-account', { method: 'addWebdavAccount', @@ -49,7 +49,7 @@ Meteor.methods({ }; await client.stat('/'); - WebdavAccounts.insert(accountData); + await WebdavAccounts.insertOne(accountData); Notifications.notifyUser(userId, 'webdav', { type: 'changed', account: accountData, @@ -89,12 +89,14 @@ Meteor.methods({ }; await client.stat('/'); - WebdavAccounts.upsert({ + await WebdavAccounts.updateOne({ user_id: userId, server_url: data.serverURL, name: data.name, }, { $set: accountData, + }, { + upsert: true, }); Notifications.notifyUser(userId, 'webdav', { type: 'changed', diff --git a/app/webdav/server/methods/getFileFromWebdav.js b/app/webdav/server/methods/getFileFromWebdav.js index aeeda2a2636e..a63d72c04723 100644 --- a/app/webdav/server/methods/getFileFromWebdav.js +++ b/app/webdav/server/methods/getFileFromWebdav.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { settings } from '../../../settings'; import { getWebdavCredentials } from './getWebdavCredentials'; -import { WebdavAccounts } from '../../../models'; +import { WebdavAccounts } from '../../../models/server/raw'; import { WebdavClientAdapter } from '../lib/webdavClientAdapter'; Meteor.methods({ @@ -14,7 +14,7 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', { method: 'getFileFromWebdav' }); } - const account = WebdavAccounts.findOne({ _id: accountId, user_id: Meteor.userId() }); + const account = await WebdavAccounts.findOneByIdAndUserId(accountId, Meteor.userId()); if (!account) { throw new Meteor.Error('error-invalid-account', 'Invalid WebDAV Account', { method: 'getFileFromWebdav' }); } diff --git a/app/webdav/server/methods/getWebdavFileList.js b/app/webdav/server/methods/getWebdavFileList.js index 52e3c6e3904a..e9b5e3526d7c 100644 --- a/app/webdav/server/methods/getWebdavFileList.js +++ b/app/webdav/server/methods/getWebdavFileList.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { settings } from '../../../settings'; import { getWebdavCredentials } from './getWebdavCredentials'; -import { WebdavAccounts } from '../../../models'; +import { WebdavAccounts } from '../../../models/server/raw'; import { WebdavClientAdapter } from '../lib/webdavClientAdapter'; Meteor.methods({ @@ -15,7 +15,7 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', { method: 'getWebdavFileList' }); } - const account = WebdavAccounts.findOne({ _id: accountId, user_id: Meteor.userId() }); + const account = await WebdavAccounts.findOneByIdAndUserId(accountId, Meteor.userId()); if (!account) { throw new Meteor.Error('error-invalid-account', 'Invalid WebDAV Account', { method: 'getWebdavFileList' }); } diff --git a/app/webdav/server/methods/getWebdavFilePreview.js b/app/webdav/server/methods/getWebdavFilePreview.js index 2de21cae6fef..5940d44601b4 100644 --- a/app/webdav/server/methods/getWebdavFilePreview.js +++ b/app/webdav/server/methods/getWebdavFilePreview.js @@ -3,7 +3,7 @@ import { createClient } from 'webdav'; import { settings } from '../../../settings'; import { getWebdavCredentials } from './getWebdavCredentials'; -import { WebdavAccounts } from '../../../models'; +import { WebdavAccounts } from '../../../models/server/raw'; Meteor.methods({ async getWebdavFilePreview(accountId, path) { @@ -15,7 +15,7 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', { method: 'getWebdavFilePreview' }); } - const account = WebdavAccounts.findOne({ _id: accountId, user_id: Meteor.userId() }); + const account = await WebdavAccounts.findOneByIdAndUserId(accountId, Meteor.userId()); if (!account) { throw new Meteor.Error('error-invalid-account', 'Invalid WebDAV Account', { method: 'getWebdavFilePreview' }); } diff --git a/app/webdav/server/methods/removeWebdavAccount.js b/app/webdav/server/methods/removeWebdavAccount.js index 46f8c7b515f7..dc6ba032cfc7 100644 --- a/app/webdav/server/methods/removeWebdavAccount.js +++ b/app/webdav/server/methods/removeWebdavAccount.js @@ -1,18 +1,18 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { WebdavAccounts } from '../../../models'; +import { WebdavAccounts } from '../../../models/server/raw'; import { Notifications } from '../../../notifications/server'; Meteor.methods({ - removeWebdavAccount(accountId) { + async removeWebdavAccount(accountId) { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid User', { method: 'removeWebdavAccount' }); } check(accountId, String); - const removed = WebdavAccounts.removeByUserAndId(accountId, Meteor.userId()); + const removed = await WebdavAccounts.removeByUserAndId(accountId, Meteor.userId()); if (removed) { Notifications.notifyUser(Meteor.userId(), 'webdav', { type: 'removed', diff --git a/app/webdav/server/methods/uploadFileToWebdav.ts b/app/webdav/server/methods/uploadFileToWebdav.ts index 345550285e7f..1f794ea0ad48 100644 --- a/app/webdav/server/methods/uploadFileToWebdav.ts +++ b/app/webdav/server/methods/uploadFileToWebdav.ts @@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { settings } from '../../../settings/server'; import { Logger } from '../../../logger/server'; import { getWebdavCredentials } from './getWebdavCredentials'; -import { WebdavAccounts } from '../../../models/server'; +import { WebdavAccounts } from '../../../models/server/raw'; import { WebdavClientAdapter } from '../lib/webdavClientAdapter'; const logger = new Logger('WebDAV_Upload'); @@ -18,7 +18,7 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', { method: 'uploadFileToWebdav' }); } - const account = WebdavAccounts.findOne({ _id: accountId }); + const account = await WebdavAccounts.findOneById(accountId); if (!account) { throw new Meteor.Error('error-invalid-account', 'Invalid WebDAV Account', { method: 'uploadFileToWebdav' }); } diff --git a/app/webrtc/client/WebRTCClass.js b/app/webrtc/client/WebRTCClass.js index 1d8485b245d4..66e7a23950b2 100644 --- a/app/webrtc/client/WebRTCClass.js +++ b/app/webrtc/client/WebRTCClass.js @@ -115,7 +115,7 @@ class WebRTCClass { @param room {String} */ - constructor(selfId, room) { + constructor(selfId, room, autoAccept = false) { this.config = { iceServers: [], }; @@ -145,15 +145,15 @@ class WebRTCClass { this.remoteItems = new ReactiveVar([]); this.remoteItemsById = new ReactiveVar({}); this.callInProgress = new ReactiveVar(false); - this.audioEnabled = new ReactiveVar(true); - this.videoEnabled = new ReactiveVar(true); + this.audioEnabled = new ReactiveVar(false); + this.videoEnabled = new ReactiveVar(false); this.overlayEnabled = new ReactiveVar(false); this.screenShareEnabled = new ReactiveVar(false); this.localUrl = new ReactiveVar(); this.active = false; this.remoteMonitoring = false; this.monitor = false; - this.autoAccept = false; + this.autoAccept = autoAccept; this.navigator = undefined; const userAgent = navigator.userAgent.toLocaleLowerCase(); @@ -169,7 +169,7 @@ class WebRTCClass { this.screenShareAvailable = ['chrome', 'firefox', 'electron'].includes(this.navigator); this.media = { - video: false, + video: true, audio: true, }; this.transport = new this.TransportClass(this); @@ -498,11 +498,12 @@ class WebRTCClass { } const onSuccess = (stream) => { this.localStream = stream; + !this.audioEnabled.get() && this.disableAudio(); + !this.videoEnabled.get() && this.disableVideo(); this.localUrl.set(stream); - this.videoEnabled.set(this.media.video === true); - this.audioEnabled.set(this.media.audio === true); const { peerConnections } = this; Object.entries(peerConnections).forEach(([, peerConnection]) => peerConnection.addStream(stream)); + document.querySelector('video#localVideo').srcObject = stream; callback(null, this.localStream); }; const onError = (error) => { @@ -537,19 +538,10 @@ class WebRTCClass { setAudioEnabled(enabled = true) { if (this.localStream != null) { - if (enabled === true && this.media.audio !== true) { - delete this.localStream; - this.media.audio = true; - this.getLocalUserMedia(() => { - this.stopAllPeerConnections(); - this.joinCall(); - }); - } else { - this.localStream.getAudioTracks().forEach(function(audio) { - audio.enabled = enabled; - }); - this.audioEnabled.set(enabled); - } + this.localStream.getAudioTracks().forEach(function(audio) { + audio.enabled = enabled; + }); + this.audioEnabled.set(enabled); } } @@ -561,21 +553,19 @@ class WebRTCClass { this.setAudioEnabled(true); } + toggleAudio() { + if (this.audioEnabled.get()) { + return this.disableAudio(); + } + return this.enableAudio(); + } + setVideoEnabled(enabled = true) { if (this.localStream != null) { - if (enabled === true && this.media.video !== true) { - delete this.localStream; - this.media.video = true; - this.getLocalUserMedia(() => { - this.stopAllPeerConnections(); - this.joinCall(); - }); - } else { - this.localStream.getVideoTracks().forEach(function(video) { - video.enabled = enabled; - }); - this.videoEnabled.set(enabled); - } + this.localStream.getVideoTracks().forEach(function(video) { + video.enabled = enabled; + }); + this.videoEnabled.set(enabled); } } @@ -610,6 +600,13 @@ class WebRTCClass { this.setVideoEnabled(true); } + toggleVideo() { + if (this.videoEnabled.get()) { + return this.disableVideo(); + } + return this.enableVideo(); + } + stop() { this.active = false; this.monitor = false; @@ -663,7 +660,6 @@ class WebRTCClass { onRemoteCall(data) { if (this.autoAccept === true) { - goToRoomById(data.room); Meteor.defer(() => { this.joinCall({ to: data.from, @@ -735,12 +731,6 @@ class WebRTCClass { */ joinCall(data = {}, ...args) { - if (data.media && data.media.audio) { - this.media.audio = data.media.audio; - } - if (data.media && data.media.video) { - this.media.video = data.media.video; - } data.media = this.media; this.log('joinCall', [data, ...args]); this.getLocalUserMedia(() => { @@ -873,6 +863,7 @@ class WebRTCClass { if (peerConnection.iceConnectionState !== 'closed' && peerConnection.iceConnectionState !== 'failed' && peerConnection.iceConnectionState !== 'disconnected' && peerConnection.iceConnectionState !== 'completed') { peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate)); } + document.querySelector('video#remoteVideo').srcObject = this.remoteItems.get()[0]?.url; } @@ -916,27 +907,41 @@ const WebRTC = new class { this.instancesByRoomId = {}; } - getInstanceByRoomId(rid) { - const subscription = ChatSubscription.findOne({ rid }); - if (!subscription) { - return; - } + getInstanceByRoomId(rid, visitorId = null) { let enabled = false; - switch (subscription.t) { - case 'd': - enabled = settings.get('WebRTC_Enable_Direct'); - break; - case 'p': - enabled = settings.get('WebRTC_Enable_Private'); - break; - case 'c': - enabled = settings.get('WebRTC_Enable_Channel'); + if (!visitorId) { + const subscription = ChatSubscription.findOne({ rid }); + if (!subscription) { + return; + } + switch (subscription.t) { + case 'd': + enabled = settings.get('WebRTC_Enable_Direct'); + break; + case 'p': + enabled = settings.get('WebRTC_Enable_Private'); + break; + case 'c': + enabled = settings.get('WebRTC_Enable_Channel'); + break; + case 'l': + enabled = settings.get('Omnichannel_call_provider') === 'WebRTC'; + } + } else { + enabled = settings.get('Omnichannel_call_provider') === 'WebRTC'; } + enabled = enabled && settings.get('WebRTC_Enabled'); if (enabled === false) { return; } if (this.instancesByRoomId[rid] == null) { - this.instancesByRoomId[rid] = new WebRTCClass(Meteor.userId(), rid); + let uid = Meteor.userId(); + let autoAccept = false; + if (visitorId) { + uid = visitorId; + autoAccept = true; + } + this.instancesByRoomId[rid] = new WebRTCClass(uid, rid, autoAccept); } return this.instancesByRoomId[rid]; } diff --git a/app/webrtc/client/actionLink.tsx b/app/webrtc/client/actionLink.tsx new file mode 100644 index 000000000000..9d31b54b6cef --- /dev/null +++ b/app/webrtc/client/actionLink.tsx @@ -0,0 +1,27 @@ +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import toastr from 'toastr'; + +import { actionLinks } from '../../action-links/client'; +import { APIClient } from '../../utils/client'; +import { Rooms } from '../../models/client'; +import { IMessage } from '../../../definition/IMessage'; +import { Notifications } from '../../notifications/client'; + +actionLinks.register('joinLivechatWebRTCCall', (message: IMessage) => { + const { callStatus, _id } = Rooms.findOne({ _id: message.rid }); + if (callStatus === 'declined' || callStatus === 'ended') { + toastr.info(TAPi18n.__('Call_Already_Ended')); + return; + } + window.open(`/meet/${ _id }`, _id); +}); + +actionLinks.register('endLivechatWebRTCCall', async (message: IMessage) => { + const { callStatus, _id } = Rooms.findOne({ _id: message.rid }); + if (callStatus === 'declined' || callStatus === 'ended') { + toastr.info(TAPi18n.__('Call_Already_Ended')); + return; + } + await APIClient.v1.put(`livechat/webrtc.call/${ message._id }`, {}, { rid: _id, status: 'ended' }); + Notifications.notifyRoom(_id, 'webrtc', 'callStatus', { callStatus: 'ended' }); +}); diff --git a/app/webrtc/client/index.js b/app/webrtc/client/index.js index 305227a2f105..32ad3e530423 100644 --- a/app/webrtc/client/index.js +++ b/app/webrtc/client/index.js @@ -1,3 +1,5 @@ import './adapter'; +import './tabBar'; +import './actionLink'; export * from './WebRTCClass'; diff --git a/app/webrtc/client/tabBar.tsx b/app/webrtc/client/tabBar.tsx new file mode 100644 index 000000000000..1ae17abd4bf1 --- /dev/null +++ b/app/webrtc/client/tabBar.tsx @@ -0,0 +1,26 @@ +import { useMemo, useCallback } from 'react'; + +import { useSetting } from '../../../client/contexts/SettingsContext'; +import { addAction } from '../../../client/views/room/lib/Toolbox'; +import { APIClient } from '../../utils/client'; + +addAction('webRTCVideo', ({ room }) => { + const enabled = useSetting('WebRTC_Enabled') && (useSetting('Omnichannel_call_provider') === 'WebRTC') && room.servedBy; + + const handleClick = useCallback(async (): Promise => { + if (!room.callStatus || room.callStatus === 'declined' || room.callStatus === 'ended') { + await APIClient.v1.get('livechat/webrtc.call', { rid: room._id }); + } + window.open(`/meet/${ room._id }`, room._id); + }, [room._id, room.callStatus]); + + return useMemo(() => (enabled ? { + groups: ['live'], + id: 'webRTCVideo', + title: 'WebRTC_Call', + icon: 'phone', + action: handleClick, + full: true, + order: 4, + } : null), [enabled, handleClick]); +}); diff --git a/app/webrtc/server/settings.ts b/app/webrtc/server/settings.ts index b0cad64d4579..d94a0c8fde7c 100644 --- a/app/webrtc/server/settings.ts +++ b/app/webrtc/server/settings.ts @@ -1,24 +1,34 @@ import { settingsRegistry } from '../../settings/server'; settingsRegistry.addGroup('WebRTC', function() { + this.add('WebRTC_Enabled', false, { + type: 'boolean', + group: 'WebRTC', + public: true, + i18nLabel: 'Enabled', + }); this.add('WebRTC_Enable_Channel', false, { type: 'boolean', group: 'WebRTC', public: true, + enableQuery: { _id: 'WebRTC_Enabled', value: true }, }); this.add('WebRTC_Enable_Private', false, { type: 'boolean', group: 'WebRTC', public: true, + enableQuery: { _id: 'WebRTC_Enabled', value: true }, }); this.add('WebRTC_Enable_Direct', false, { type: 'boolean', group: 'WebRTC', public: true, + enableQuery: { _id: 'WebRTC_Enabled', value: true }, }); return this.add('WebRTC_Servers', 'stun:stun.l.google.com:19302, stun:23.21.150.121, team%40rocket.chat:demo@turn:numb.viagenie.ca:3478', { type: 'string', group: 'WebRTC', public: true, + enableQuery: { _id: 'WebRTC_Enabled', value: true }, }); }); diff --git a/client/.eslintrc.js b/client/.eslintrc.js index 4d6bf74449b7..da196844a21a 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -2,7 +2,7 @@ module.exports = { root: true, extends: ['@rocket.chat/eslint-config', 'prettier'], parser: 'babel-eslint', - plugins: ['react', 'react-hooks', 'prettier'], + plugins: ['react', 'react-hooks', 'prettier', 'testing-library'], rules: { 'import/named': 'error', 'import/order': [ @@ -63,6 +63,8 @@ module.exports = { plugins: ['@typescript-eslint', 'react', 'react-hooks', 'prettier'], rules: { '@typescript-eslint/ban-ts-ignore': 'off', + '@typescript-eslint/explicit-function-return-type': 'warn', + // '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/indent': 'off', '@typescript-eslint/interface-name-prefix': ['error', 'always'], '@typescript-eslint/no-extra-parens': 'off', @@ -133,5 +135,25 @@ module.exports = { 'react/no-multi-comp': 'off', }, }, + { + files: ['**/*.tests.js', '**/*.tests.ts', '**/*.spec.ts', '**/*.spec.tsx'], + extends: ['plugin:testing-library/react'], + rules: { + 'testing-library/no-await-sync-events': 'warn', + 'testing-library/no-manual-cleanup': 'warn', + 'testing-library/prefer-explicit-assert': 'warn', + 'testing-library/prefer-user-event': 'warn', + }, + env: { + mocha: true, + }, + }, + { + files: ['**/*.stories.ts', '**/*.stories.tsx'], + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + }, + }, ], }; diff --git a/client/components/FilterByText.tsx b/client/components/FilterByText.tsx index 0c06774c6e6d..170c0c1944ad 100644 --- a/client/components/FilterByText.tsx +++ b/client/components/FilterByText.tsx @@ -6,19 +6,22 @@ import { useTranslation } from '../contexts/TranslationContext'; type FilterByTextProps = { placeholder?: string; onChange: (filter: { text: string }) => void; - displayButton: boolean; + inputRef?: () => void; +}; + +type FilterByTextPropsWithButton = FilterByTextProps & { + displayButton: true; textButton: string; onButtonClick: () => void; - inputRef: () => void; }; +const isFilterByTextPropsWithButton = (props: any): props is FilterByTextPropsWithButton => + 'displayButton' in props && props.displayButton === true; const FilterByText: FC = ({ placeholder, onChange: setFilter, - displayButton: display = false, - textButton = '', - onButtonClick, inputRef, + children: _, ...props }) => { const t = useTranslation(); @@ -53,9 +56,11 @@ const FilterByText: FC = ({ onChange={handleInputChange} value={text} /> - + {isFilterByTextPropsWithButton(props) && ( + + )} ); }; diff --git a/client/components/GenericTable/GenericTable.tsx b/client/components/GenericTable/GenericTable.tsx index 99124d8186fd..c6e5317d24ed 100644 --- a/client/components/GenericTable/GenericTable.tsx +++ b/client/components/GenericTable/GenericTable.tsx @@ -1,20 +1,23 @@ -import { Box, Pagination, Table, Tile } from '@rocket.chat/fuselage'; +import { Pagination, Tile } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import React, { useState, useEffect, - useCallback, forwardRef, ReactNode, ReactElement, Key, - RefAttributes, + useMemo, + Ref, } from 'react'; import flattenChildren from 'react-keyed-flatten-children'; import { useTranslation } from '../../contexts/TranslationContext'; -import ScrollableContentWrapper from '../ScrollableContentWrapper'; -import LoadingRow from './LoadingRow'; +import { GenericTable as GenericTableV2 } from './V2/GenericTable'; +import { GenericTableBody } from './V2/GenericTableBody'; +import { GenericTableHeader } from './V2/GenericTableHeader'; +import { GenericTableLoadingTable } from './V2/GenericTableLoadingTable'; +import { usePagination } from './hooks/usePagination'; const defaultParamsValue = { text: '', current: 0, itemsPerPage: 25 } as const; const defaultSetParamsValue = (): void => undefined; @@ -37,14 +40,10 @@ type GenericTableProps< pagination?: boolean; } & FilterProps; -const GenericTable: { - < - FilterProps extends { onChange?: (params: GenericTableParams) => void }, - ResultProps extends { _id?: Key }, - >( - props: GenericTableProps & RefAttributes, - ): ReactElement | null; -} = forwardRef(function GenericTable( +const GenericTable = forwardRef(function GenericTable< + FilterProps extends { onChange?: (params: GenericTableParams) => void }, + ResultProps extends { _id?: Key }, +>( { children, fixed = true, @@ -57,41 +56,31 @@ const GenericTable: { total, pagination = true, ...props - }, - ref, + }: GenericTableProps, + ref: Ref, ) { const t = useTranslation(); const [filter, setFilter] = useState(paramsDefault); - const [itemsPerPage, setItemsPerPage] = useState<25 | 50 | 100>(25); - - const [current, setCurrent] = useState(0); + const { + itemsPerPage, + setItemsPerPage, + current, + setCurrent, + itemsPerPageLabel, + showingResultsLabel, + } = usePagination(); const params = useDebouncedValue(filter, 500); useEffect(() => { - setParams({ ...params, current, itemsPerPage }); + setParams({ text: params.text || '', current, itemsPerPage }); }, [params, current, itemsPerPage, setParams]); - const Loading = useCallback(() => { - const headerCells = flattenChildren(header); - return ( - <> - {Array.from({ length: 10 }, (_, i) => ( - - ))} - - ); - }, [header]); - - const showingResultsLabel = useCallback( - ({ count, current, itemsPerPage }) => - t('Showing_results_of', current + 1, Math.min(current + itemsPerPage, count), count), - [t], - ); + const headerCells = useMemo(() => flattenChildren(header).length, [header]); - const itemsPerPageLabel = useCallback(() => t('Items_per_page:'), [t]); + const isLoading = !results; return ( <> @@ -104,28 +93,18 @@ const GenericTable: { ) : ( <> - - - - {header && ( - - {header} - - )} - - {RenderRow && - (results ? ( - results.map((props, index) => ( - - )) - ) : ( - - ))} - {children && (results ? results.map(children) : )} - -
-
-
+ + {header && {header}} + + {isLoading && } + {!isLoading && + ((RenderRow && + results?.map((props, index: number) => ( + + ))) || + (children && results?.map(children)))} + + {pagination && ( (function GenericTable( + { fixed = true, children }, + ref, +) { + return ( + + + + {children} +
+
+
+ ); +}); diff --git a/client/components/GenericTable/V2/GenericTableBody.tsx b/client/components/GenericTable/V2/GenericTableBody.tsx new file mode 100644 index 000000000000..945688f9efaf --- /dev/null +++ b/client/components/GenericTable/V2/GenericTableBody.tsx @@ -0,0 +1,4 @@ +import { Table } from '@rocket.chat/fuselage'; +import React, { FC } from 'react'; + +export const GenericTableBody: FC = (props) => ; diff --git a/client/components/GenericTable/V2/GenericTableCell.tsx b/client/components/GenericTable/V2/GenericTableCell.tsx new file mode 100644 index 000000000000..883de10b3f3a --- /dev/null +++ b/client/components/GenericTable/V2/GenericTableCell.tsx @@ -0,0 +1,6 @@ +import { Table } from '@rocket.chat/fuselage'; +import React, { ComponentProps, FC } from 'react'; + +export const GenericTableCell: FC> = (props) => ( + +); diff --git a/client/components/GenericTable/V2/GenericTableHeader.tsx b/client/components/GenericTable/V2/GenericTableHeader.tsx new file mode 100644 index 000000000000..90f3a340f5a1 --- /dev/null +++ b/client/components/GenericTable/V2/GenericTableHeader.tsx @@ -0,0 +1,10 @@ +import { Table } from '@rocket.chat/fuselage'; +import React, { FC } from 'react'; + +import { GenericTableRow } from './GenericTableRow'; + +export const GenericTableHeader: FC = ({ children, ...props }) => ( + + {children} + +); diff --git a/client/components/GenericTable/V2/GenericTableHeaderCell.tsx b/client/components/GenericTable/V2/GenericTableHeaderCell.tsx new file mode 100644 index 000000000000..a4db4fbb3160 --- /dev/null +++ b/client/components/GenericTable/V2/GenericTableHeaderCell.tsx @@ -0,0 +1,30 @@ +import { Box, Table } from '@rocket.chat/fuselage'; +import React, { ComponentProps, ReactElement, useCallback } from 'react'; + +import SortIcon from '../SortIcon'; + +type GenericTableHeaderCellProps = Omit, 'onClick'> & { + active?: boolean; + direction?: 'asc' | 'desc'; + sort?: T; + onClick?: (sort: T) => void; +}; + +export const GenericTableHeaderCell = ({ + children, + active, + direction, + sort, + onClick, + ...props +}: GenericTableHeaderCellProps): ReactElement => { + const fn = useCallback(() => onClick && sort && onClick(sort), [sort, onClick]); + return ( + + + {children} + {sort && } + + + ); +}; diff --git a/client/components/GenericTable/V2/GenericTableLoadingRow.tsx b/client/components/GenericTable/V2/GenericTableLoadingRow.tsx new file mode 100644 index 000000000000..ecc34e2e5e7f --- /dev/null +++ b/client/components/GenericTable/V2/GenericTableLoadingRow.tsx @@ -0,0 +1,25 @@ +import { Box, Skeleton, Table } from '@rocket.chat/fuselage'; +import React, { ReactElement } from 'react'; + +type GenericTableLoadingRowRowProps = { + cols: number; +}; + +export const GenericTableLoadingRow = ({ cols }: GenericTableLoadingRowRowProps): ReactElement => ( + + + + + + + + + + + {Array.from({ length: cols - 1 }, (_, i) => ( + + + + ))} + +); diff --git a/client/components/GenericTable/V2/GenericTableLoadingTable.tsx b/client/components/GenericTable/V2/GenericTableLoadingTable.tsx new file mode 100644 index 000000000000..e7f2501bc7f4 --- /dev/null +++ b/client/components/GenericTable/V2/GenericTableLoadingTable.tsx @@ -0,0 +1,15 @@ +import React, { ReactElement } from 'react'; + +import { GenericTableLoadingRow } from './GenericTableLoadingRow'; + +export const GenericTableLoadingTable = ({ + headerCells, +}: { + headerCells: number; +}): ReactElement => ( + <> + {Array.from({ length: 10 }, (_, i) => ( + + ))} + +); diff --git a/client/components/GenericTable/V2/GenericTableRow.tsx b/client/components/GenericTable/V2/GenericTableRow.tsx new file mode 100644 index 000000000000..cd31eb47d65c --- /dev/null +++ b/client/components/GenericTable/V2/GenericTableRow.tsx @@ -0,0 +1,6 @@ +import { Table } from '@rocket.chat/fuselage'; +import React, { ComponentProps, FC } from 'react'; + +export const GenericTableRow: FC> = (props) => ( + +); diff --git a/client/components/GenericTable/hooks/useCurrent.ts b/client/components/GenericTable/hooks/useCurrent.ts new file mode 100644 index 000000000000..fc70a9f252d2 --- /dev/null +++ b/client/components/GenericTable/hooks/useCurrent.ts @@ -0,0 +1,9 @@ +import { useState } from 'react'; + +export const useCurrent = ( + currentInitialValue = 0, +): [number, React.Dispatch>] => { + const [current, setCurrent] = useState(currentInitialValue); + + return [current, setCurrent]; +}; diff --git a/client/components/GenericTable/hooks/useItemsPerPage.ts b/client/components/GenericTable/hooks/useItemsPerPage.ts new file mode 100644 index 000000000000..3d9c36807a61 --- /dev/null +++ b/client/components/GenericTable/hooks/useItemsPerPage.ts @@ -0,0 +1,11 @@ +import { useState } from 'react'; + +type UseItemsPerPageValue = 25 | 50 | 100; + +export const useItemsPerPage = ( + itemsPerPageInitialValue: UseItemsPerPageValue = 25, +): [UseItemsPerPageValue, React.Dispatch>] => { + const [itemsPerPage, setItemsPerPage] = useState(itemsPerPageInitialValue); + + return [itemsPerPage, setItemsPerPage]; +}; diff --git a/client/components/GenericTable/hooks/useItemsPerPageLabel.ts b/client/components/GenericTable/hooks/useItemsPerPageLabel.ts new file mode 100644 index 000000000000..79e65d88f370 --- /dev/null +++ b/client/components/GenericTable/hooks/useItemsPerPageLabel.ts @@ -0,0 +1,8 @@ +import { useCallback } from 'react'; + +import { useTranslation } from '../../../contexts/TranslationContext'; + +export const useItemsPerPageLabel = (): (() => string) => { + const t = useTranslation(); + return useCallback(() => t('Items_per_page:'), [t]); +}; diff --git a/client/components/GenericTable/hooks/usePagination.ts b/client/components/GenericTable/hooks/usePagination.ts new file mode 100644 index 000000000000..3f0558f4ac99 --- /dev/null +++ b/client/components/GenericTable/hooks/usePagination.ts @@ -0,0 +1,30 @@ +import { useCurrent } from './useCurrent'; +import { useItemsPerPage } from './useItemsPerPage'; +import { useItemsPerPageLabel } from './useItemsPerPageLabel'; +import { useShowingResultsLabel } from './useShowingResultsLabel'; + +export const usePagination = (): { + current: ReturnType[0]; + setCurrent: ReturnType[1]; + itemsPerPage: ReturnType[0]; + setItemsPerPage: ReturnType[1]; + itemsPerPageLabel: ReturnType; + showingResultsLabel: ReturnType; +} => { + const [itemsPerPage, setItemsPerPage] = useItemsPerPage(); + + const [current, setCurrent] = useCurrent(); + + const itemsPerPageLabel = useItemsPerPageLabel(); + + const showingResultsLabel = useShowingResultsLabel(); + + return { + itemsPerPage, + setItemsPerPage, + current, + setCurrent, + itemsPerPageLabel, + showingResultsLabel, + }; +}; diff --git a/client/components/GenericTable/hooks/useShowingResultsLabel.ts b/client/components/GenericTable/hooks/useShowingResultsLabel.ts new file mode 100644 index 000000000000..b7a54ac084f9 --- /dev/null +++ b/client/components/GenericTable/hooks/useShowingResultsLabel.ts @@ -0,0 +1,19 @@ +import { Pagination } from '@rocket.chat/fuselage'; +import { ComponentProps, useCallback } from 'react'; + +import { useTranslation } from '../../../contexts/TranslationContext'; + +type Props< + T extends ComponentProps['showingResultsLabel'] = ComponentProps< + typeof Pagination + >['showingResultsLabel'], +> = T extends (...args: any[]) => any ? Parameters : never; + +export const useShowingResultsLabel = (): ((...params: Props) => string) => { + const t = useTranslation(); + return useCallback( + ({ count, current, itemsPerPage }) => + t('Showing_results_of', current + 1, Math.min(current + itemsPerPage, count), count), + [t], + ); +}; diff --git a/client/components/GenericTable/hooks/useSort.ts b/client/components/GenericTable/hooks/useSort.ts new file mode 100644 index 000000000000..6858404208fc --- /dev/null +++ b/client/components/GenericTable/hooks/useSort.ts @@ -0,0 +1,34 @@ +import { useCallback, useState } from 'react'; + +type Direction = 'asc' | 'desc'; + +export const useSort = ( + by: T, + initialDirection: Direction = 'asc', +): { + sortBy: T; + sortDirection: Direction; + setSort: (sortBy: T, direction?: Direction | undefined) => void; +} => { + const [sort, _setSort] = useState<[T, Direction]>(() => [by, initialDirection]); + + const setSort = useCallback((id: T, direction?: Direction | undefined) => { + _setSort(([sortBy, sortDirection]) => { + if (direction) { + return [id, direction]; + } + + if (sortBy === id) { + return [id, sortDirection === 'asc' ? 'desc' : 'asc']; + } + + return [id, 'asc']; + }); + }, []); + + return { + sortBy: sort[0], + sortDirection: sort[1], + setSort, + }; +}; diff --git a/client/components/GenericTable/index.ts b/client/components/GenericTable/index.ts index 8da6df3fc14e..3a5150443024 100644 --- a/client/components/GenericTable/index.ts +++ b/client/components/GenericTable/index.ts @@ -4,3 +4,12 @@ import HeaderCell from './HeaderCell'; export default Object.assign(GenericTable, { HeaderCell, }); + +export * from './V2/GenericTable'; +export * from './V2/GenericTableBody'; +export * from './V2/GenericTableCell'; +export * from './V2/GenericTableHeader'; +export * from './V2/GenericTableHeaderCell'; +export * from './V2/GenericTableLoadingRow'; +export * from './V2/GenericTableLoadingTable'; +export * from './V2/GenericTableRow'; diff --git a/client/components/MarkdownText.tsx b/client/components/MarkdownText.tsx index 4baefbab19d1..82f4e724347a 100644 --- a/client/components/MarkdownText.tsx +++ b/client/components/MarkdownText.tsx @@ -31,6 +31,7 @@ const listItemMarked = (text: string): string => { const cleanText = text.replace(/|<\/p>/gi, ''); return `

  • ${cleanText}
  • `; }; +const horizontalRuleMarked = (): string => ''; documentRenderer.link = linkMarked; documentRenderer.listitem = listItemMarked; @@ -38,11 +39,13 @@ documentRenderer.listitem = listItemMarked; inlineRenderer.link = linkMarked; inlineRenderer.paragraph = paragraphMarked; inlineRenderer.listitem = listItemMarked; +inlineRenderer.hr = horizontalRuleMarked; inlineWithoutBreaks.link = linkMarked; inlineWithoutBreaks.paragraph = paragraphMarked; inlineWithoutBreaks.br = brMarked; inlineWithoutBreaks.listitem = listItemMarked; +inlineWithoutBreaks.hr = horizontalRuleMarked; const defaultOptions = { gfm: true, diff --git a/client/components/Message/Actions/Action.tsx b/client/components/Message/Actions/Action.tsx index d15aec9f17d0..fa635c4bd167 100644 --- a/client/components/Message/Actions/Action.tsx +++ b/client/components/Message/Actions/Action.tsx @@ -12,6 +12,7 @@ type ActionOptions = { i18nLabel?: TranslationKey; label?: string; runAction?: RunAction; + danger?: boolean; }; const resolveLegacyIcon = (legacyIcon: string | undefined): string | undefined => { @@ -22,13 +23,22 @@ const resolveLegacyIcon = (legacyIcon: string | undefined): string | undefined = return legacyIcon && legacyIcon.replace(/^icon-/, ''); }; -const Action: FC = ({ id, icon, i18nLabel, label, mid, runAction }) => { +const Action: FC = ({ id, icon, i18nLabel, label, mid, runAction, danger }) => { const t = useTranslation(); const resolvedIcon = resolveLegacyIcon(icon); return ( - diff --git a/client/components/Message/Actions/Actions.tsx b/client/components/Message/Actions/Actions.tsx index 64fb6dbf6a2e..a6e59d496055 100644 --- a/client/components/Message/Actions/Actions.tsx +++ b/client/components/Message/Actions/Actions.tsx @@ -14,19 +14,24 @@ type ActionOptions = { i18nLabel?: TranslationKey; label?: string; runAction?: RunAction; + actionLinksAlignment?: string; }; const Actions: FC<{ actions: Array; runAction: RunAction; mid: string }> = ({ actions, runAction, -}) => ( - - - {actions.map((action) => ( - - ))} - - -); +}) => { + const alignment = actions[0]?.actionLinksAlignment || 'center'; + + return ( + + + {actions.map((action) => ( + + ))} + + + ); +}; export default Actions; diff --git a/client/components/Omnichannel/hooks/useAgentsList.ts b/client/components/Omnichannel/hooks/useAgentsList.ts index 4b3b894ebc54..d280aa5a0df3 100644 --- a/client/components/Omnichannel/hooks/useAgentsList.ts +++ b/client/components/Omnichannel/hooks/useAgentsList.ts @@ -37,7 +37,7 @@ export const useAgentsList = ( ...(options.text && { text: options.text }), offset: start, count: end + start, - sort: JSON.stringify({ name: 1 }), + sort: `{ "name": 1 }`, }); const items = agents.map((agent: any) => { diff --git a/client/components/Omnichannel/hooks/useDepartmentsList.ts b/client/components/Omnichannel/hooks/useDepartmentsList.ts index 5447c37d1877..8545873bd9ff 100644 --- a/client/components/Omnichannel/hooks/useDepartmentsList.ts +++ b/client/components/Omnichannel/hooks/useDepartmentsList.ts @@ -40,7 +40,7 @@ export const useDepartmentsList = ( text: options.filter, offset: start, count: end + start, - sort: JSON.stringify({ name: 1 }), + sort: `{ "name": 1 }`, }); const items = departments diff --git a/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.js b/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.js index 11c383e86f8f..b34a35aed4de 100644 --- a/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.js +++ b/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultiple.js @@ -4,7 +4,6 @@ import React, { memo, useMemo, useState } from 'react'; import { useEndpointData } from '../../hooks/useEndpointData'; import UserAvatar from '../avatar/UserAvatar'; -import Avatar from './Avatar'; const query = (term = '') => ({ selector: JSON.stringify({ term }) }); @@ -40,8 +39,15 @@ const UserAutoCompleteMultiple = (props) => { )) } - renderItem={({ value, ...props }) => ( - )} options={options} /> diff --git a/client/components/avatar/UserAvatarEditor/UserAvatarEditor.js b/client/components/avatar/UserAvatarEditor/UserAvatarEditor.js index 46a94c62379e..0c4832943b06 100644 --- a/client/components/avatar/UserAvatarEditor/UserAvatarEditor.js +++ b/client/components/avatar/UserAvatarEditor/UserAvatarEditor.js @@ -19,10 +19,20 @@ function UserAvatarEditor({ const [newAvatarSource, setNewAvatarSource] = useState(); const [urlEmpty, setUrlEmpty] = useState(true); + const toDataURL = (file, callback) => { + const reader = new FileReader(); + reader.onload = function (e) { + callback(e.target.result); + }; + reader.readAsDataURL(file); + }; + const setUploadedPreview = useCallback( async (file, avatarObj) => { setAvatarObj(avatarObj); - setNewAvatarSource(URL.createObjectURL(file)); + toDataURL(file, (dataurl) => { + setNewAvatarSource(dataurl); + }); }, [setAvatarObj], ); diff --git a/client/contexts/ServerContext/ServerContext.ts b/client/contexts/ServerContext/ServerContext.ts index d791fea0d2d4..838500ef1864 100644 --- a/client/contexts/ServerContext/ServerContext.ts +++ b/client/contexts/ServerContext/ServerContext.ts @@ -1,8 +1,15 @@ import { createContext, useCallback, useContext, useMemo } from 'react'; -import { IServerInfo } from '../../../definition/IServerInfo'; +import type { IServerInfo } from '../../../definition/IServerInfo'; import type { Serialized } from '../../../definition/Serialized'; -import type { PathFor, Params, Return, Method } from './endpoints'; +import type { + Method, + PathFor, + OperationParams, + MatchPathPattern, + OperationResult, + PathPattern, +} from '../../../definition/rest'; import { ServerMethodFunction, ServerMethodName, @@ -18,11 +25,11 @@ type ServerContextValue = { methodName: MethodName, ...args: ServerMethodParameters ) => Promise>; - callEndpoint: >( - method: M, - path: P, - params: Params[0], - ) => Promise>>; + callEndpoint: >( + method: TMethod, + path: TPath, + params: Serialized>>, + ) => Promise>>>; uploadToEndpoint: (endpoint: string, params: any, formData: any) => Promise; getStream: ( streamName: string, @@ -70,10 +77,16 @@ export const useMethod = ( ); }; -export const useEndpoint = >( - method: M, - path: P, -): ((params: Params[0]) => Promise>>) => { +type EndpointFunction = ( + params: void extends OperationParams + ? void + : Serialized>, +) => Promise>>; + +export const useEndpoint = >( + method: TMethod, + path: TPath, +): EndpointFunction> => { const { callEndpoint } = useContext(ServerContext); return useCallback((params) => callEndpoint(method, path, params), [callEndpoint, path, method]); diff --git a/client/contexts/ServerContext/endpoints.ts b/client/contexts/ServerContext/endpoints.ts deleted file mode 100644 index 0a57ef4479d0..000000000000 --- a/client/contexts/ServerContext/endpoints.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { ExtractKeys, ValueOf } from '../../../definition/utils'; -import type { EngagementDashboardEndpoints } from '../../../ee/client/contexts/ServerContext/endpoints/v1/engagementDashboard'; -import type { AppsEndpoints } from './endpoints/apps'; -import type { ChannelsEndpoints } from './endpoints/v1/channels'; -import type { ChatEndpoints } from './endpoints/v1/chat'; -import type { CloudEndpoints } from './endpoints/v1/cloud'; -import type { CustomUserStatusEndpoints } from './endpoints/v1/customUserStatus'; -import type { DmEndpoints } from './endpoints/v1/dm'; -import type { DnsEndpoints } from './endpoints/v1/dns'; -import type { EmojiCustomEndpoints } from './endpoints/v1/emojiCustom'; -import type { GroupsEndpoints } from './endpoints/v1/groups'; -import type { ImEndpoints } from './endpoints/v1/im'; -import type { LDAPEndpoints } from './endpoints/v1/ldap'; -import type { LicensesEndpoints } from './endpoints/v1/licenses'; -import type { MiscEndpoints } from './endpoints/v1/misc'; -import type { OmnichannelEndpoints } from './endpoints/v1/omnichannel'; -import type { RoomsEndpoints } from './endpoints/v1/rooms'; -import type { StatisticsEndpoints } from './endpoints/v1/statistics'; -import type { TeamsEndpoints } from './endpoints/v1/teams'; -import type { UsersEndpoints } from './endpoints/v1/users'; - -type Endpoints = ChatEndpoints & - ChannelsEndpoints & - CloudEndpoints & - CustomUserStatusEndpoints & - DmEndpoints & - DnsEndpoints & - EmojiCustomEndpoints & - GroupsEndpoints & - ImEndpoints & - LDAPEndpoints & - RoomsEndpoints & - TeamsEndpoints & - UsersEndpoints & - EngagementDashboardEndpoints & - AppsEndpoints & - OmnichannelEndpoints & - StatisticsEndpoints & - LicensesEndpoints & - MiscEndpoints; - -type Endpoint = UnionizeEndpoints; - -type UnionizeEndpoints = ValueOf< - { - [P in keyof EE]: UnionizeMethods; - } ->; - -type ExtractOperations = ExtractKeys any>; - -type UnionizeMethods = ValueOf< - { - [M in keyof OO as ExtractOperations]: ( - method: M, - path: OO extends { path: string } ? OO['path'] : P, - ...params: Parameters any>> - ) => ReturnType any>>; - } ->; - -export type Method = Parameters[0]; -export type Path = Parameters[1]; - -export type MethodFor

    = P extends any - ? Parameters any>>[0] - : never; -export type PathFor = M extends any - ? Parameters any>>[1] - : never; - -type Operation> = M extends any - ? P extends any - ? Extract any> - : never - : never; - -type ExtractParams = Q extends [any, any] - ? [undefined?] - : Q extends [any, any, any, ...any[]] - ? [Q[2]] - : never; - -export type Params> = ExtractParams< - Parameters> ->; -export type Return> = ReturnType>; diff --git a/client/contexts/ServerContext/endpoints/v1/emojiCustom.ts b/client/contexts/ServerContext/endpoints/v1/emojiCustom.ts deleted file mode 100644 index 63648e6f3e26..000000000000 --- a/client/contexts/ServerContext/endpoints/v1/emojiCustom.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ICustomEmojiDescriptor } from '../../../../../definition/ICustomEmojiDescriptor'; - -export type EmojiCustomEndpoints = { - 'emoji-custom.list': { - GET: (params: { query: string }) => { - emojis?: { - update: ICustomEmojiDescriptor[]; - }; - }; - }; - 'emoji-custom.delete': { - POST: (params: { emojiId: ICustomEmojiDescriptor['_id'] }) => void; - }; -}; diff --git a/client/contexts/ServerContext/endpoints/v1/omnichannel.ts b/client/contexts/ServerContext/endpoints/v1/omnichannel.ts deleted file mode 100644 index 4254db71253a..000000000000 --- a/client/contexts/ServerContext/endpoints/v1/omnichannel.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { ILivechatDepartment } from '../../../../../definition/ILivechatDepartment'; -import { ILivechatMonitor } from '../../../../../definition/ILivechatMonitor'; -import { ILivechatTag } from '../../../../../definition/ILivechatTag'; -import { IOmnichannelCannedResponse } from '../../../../../definition/IOmnichannelCannedResponse'; -import { IOmnichannelRoom, IRoom } from '../../../../../definition/IRoom'; -import { ISetting } from '../../../../../definition/ISetting'; -import { IUser } from '../../../../../definition/IUser'; - -export type OmnichannelEndpoints = { - 'livechat/appearance': { - GET: () => { - appearance: ISetting[]; - }; - }; - 'livechat/visitors.info': { - GET: (params: { visitorId: string }) => { - visitor: { - visitorEmails: Array<{ - address: string; - }>; - }; - }; - }; - 'livechat/room.onHold': { - POST: (params: { roomId: IRoom['_id'] }) => void; - }; - 'livechat/monitors.list': { - GET: (params: { text: string; offset: number; count: number }) => { - monitors: ILivechatMonitor[]; - total: number; - }; - }; - 'livechat/tags.list': { - GET: (params: { text: string; offset: number; count: number }) => { - tags: ILivechatTag[]; - total: number; - }; - }; - 'livechat/department': { - GET: (params: { - text: string; - offset?: number; - count?: number; - sort?: string; - onlyMyDepartments?: boolean; - }) => { - departments: ILivechatDepartment[]; - total: number; - }; - }; - 'livechat/department/:_id': { - path: `livechat/department/${string}`; - GET: () => { - department: ILivechatDepartment; - }; - }; - 'livechat/departments.by-unit/': { - GET: (params: { text: string; offset: number; count: number }) => { - departments: ILivechatDepartment[]; - total: number; - }; - }; - 'livechat/custom-fields': { - GET: () => { - customFields: [ - { - _id: string; - label: string; - }, - ]; - }; - }; - 'livechat/rooms': { - GET: (params: { - guest: string; - fname: string; - servedBy: string[]; - status: string; - department: string; - from: string; - to: string; - customFields: any; - current: number; - itemsPerPage: number; - tags: string[]; - }) => { - rooms: IOmnichannelRoom[]; - count: number; - offset: number; - total: number; - }; - }; - 'livechat/users/agent': { - GET: (params: { text?: string; offset?: number; count?: number; sort?: string }) => { - users: { - _id: string; - emails: { - address: string; - verified: boolean; - }[]; - status: string; - name: string; - username: string; - statusLivechat: string; - livechat: { - maxNumberSimultaneousChat: number; - }; - }[]; - count: number; - offset: number; - total: number; - }; - }; - 'canned-responses': { - GET: (params: { - shortcut?: string; - text?: string; - scope?: string; - createdBy?: IUser['username']; - tags?: any; - departmentId?: ILivechatDepartment['_id']; - offset?: number; - count?: number; - }) => { - cannedResponses: IOmnichannelCannedResponse[]; - count?: number; - offset?: number; - total: number; - }; - POST: (params: { - _id?: IOmnichannelCannedResponse['_id']; - shortcut: string; - text: string; - scope: string; - tags?: any; - departmentId?: ILivechatDepartment['_id']; - }) => void; - DELETE: (params: { _id: IOmnichannelCannedResponse['_id'] }) => void; - }; - 'canned-responses/:_id': { - path: `canned-responses/${string}`; - GET: () => { - cannedResponse: IOmnichannelCannedResponse; - }; - }; -}; diff --git a/client/contexts/ServerContext/endpoints/v1/teams.ts b/client/contexts/ServerContext/endpoints/v1/teams.ts deleted file mode 100644 index 70f8a7c10b12..000000000000 --- a/client/contexts/ServerContext/endpoints/v1/teams.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { IRoom } from '../../../../../definition/IRoom'; -import type { IRecordsWithTotal, ITeam } from '../../../../../definition/ITeam'; -import type { IUser } from '../../../../../definition/IUser'; - -export type TeamsEndpoints = { - 'teams.addRooms': { - POST: (params: { rooms: IRoom['_id'][]; teamId: string }) => void; - }; - 'teams.info': { - GET: (params: { teamId: IRoom['teamId'] }) => { teamInfo: ITeam }; - }; - 'teams.listRooms': { - GET: (params: { - teamId: ITeam['_id']; - offset?: number; - count?: number; - filter: string; - type: string; - }) => Omit, 'records'> & { - count: number; - offset: number; - rooms: IRecordsWithTotal['records']; - }; - }; - 'teams.listRoomsOfUser': { - GET: (params: { - teamId: ITeam['_id']; - teamName?: string; - userId?: string; - canUserDelete?: boolean; - offset?: number; - count?: number; - }) => Omit, 'records'> & { - count: number; - offset: number; - rooms: IRecordsWithTotal['records']; - }; - }; - 'teams.create': { - POST: (params: { - name: ITeam['name']; - type?: ITeam['type']; - members?: IUser['_id'][]; - room: { - id?: string; - name?: IRoom['name']; - members?: IUser['_id'][]; - readOnly?: boolean; - extraData?: { - teamId?: string; - teamMain?: boolean; - } & { [key: string]: string | boolean }; - options?: { - nameValidationRegex?: string; - creator: string; - subscriptionExtra?: { - open: boolean; - ls: Date; - prid: IRoom['_id']; - }; - } & { - [key: string]: - | string - | { - open: boolean; - ls: Date; - prid: IRoom['_id']; - }; - }; - }; - owner?: IUser['_id']; - }) => { - team: ITeam; - }; - }; -}; diff --git a/client/contexts/ServerContext/index.ts b/client/contexts/ServerContext/index.ts index a2807467fddf..cd41c0b16c17 100644 --- a/client/contexts/ServerContext/index.ts +++ b/client/contexts/ServerContext/index.ts @@ -1,3 +1,2 @@ export * from './ServerContext'; -export * from './endpoints'; export * from './methods'; diff --git a/client/hooks/useCallbacks.js b/client/hooks/useCallbacks.js deleted file mode 100644 index b727689643ba..000000000000 --- a/client/hooks/useCallbacks.js +++ /dev/null @@ -1,3 +0,0 @@ -import { callbacks } from '../../app/callbacks/lib/callbacks'; - -export const useCallbacks = () => callbacks; diff --git a/client/hooks/useEndpointAction.ts b/client/hooks/useEndpointAction.ts index 65a7f2ccaed3..62990514185c 100644 --- a/client/hooks/useEndpointAction.ts +++ b/client/hooks/useEndpointAction.ts @@ -1,16 +1,24 @@ import { useCallback } from 'react'; import { Serialized } from '../../definition/Serialized'; +import { + MatchPathPattern, + Method, + OperationParams, + OperationResult, + PathFor, +} from '../../definition/rest'; import { useEndpoint } from '../contexts/ServerContext'; -import { Method, Params, PathFor, Return } from '../contexts/ServerContext/endpoints'; import { useToastMessageDispatch } from '../contexts/ToastMessagesContext'; -export const useEndpointAction = >( - method: M, - path: P, - params: Params[0] = {}, +export const useEndpointAction = >( + method: TMethod, + path: TPath, + params: Serialized>> = {} as Serialized< + OperationParams> + >, successMessage?: string, -): ((extraParams?: Params[1]) => Promise>>) => { +): (() => Promise>>>) => { const sendData = useEndpoint(method, path); const dispatchToastMessage = useToastMessageDispatch(); diff --git a/client/hooks/useEndpointActionExperimental.ts b/client/hooks/useEndpointActionExperimental.ts index b6cb286f46ae..1b5420acae27 100644 --- a/client/hooks/useEndpointActionExperimental.ts +++ b/client/hooks/useEndpointActionExperimental.ts @@ -1,15 +1,26 @@ import { useCallback } from 'react'; import { Serialized } from '../../definition/Serialized'; +import { + MatchPathPattern, + Method, + OperationParams, + OperationResult, + PathFor, +} from '../../definition/rest'; import { useEndpoint } from '../contexts/ServerContext'; -import { Method, Params, PathFor, Return } from '../contexts/ServerContext/endpoints'; import { useToastMessageDispatch } from '../contexts/ToastMessagesContext'; -export const useEndpointActionExperimental = >( - method: M, - path: P, +export const useEndpointActionExperimental = < + TMethod extends Method, + TPath extends PathFor, +>( + method: TMethod, + path: TPath, successMessage?: string, -): ((params: Params[0]) => Promise>>) => { +): (( + params: Serialized>>, +) => Promise>>>) => { const sendData = useEndpoint(method, path); const dispatchToastMessage = useToastMessageDispatch(); diff --git a/client/hooks/useEndpointData.ts b/client/hooks/useEndpointData.ts index 6412cd9d7f3b..38469217d172 100644 --- a/client/hooks/useEndpointData.ts +++ b/client/hooks/useEndpointData.ts @@ -1,18 +1,26 @@ import { useCallback, useEffect } from 'react'; import { Serialized } from '../../definition/Serialized'; +import { MatchPathPattern, OperationParams, OperationResult, PathFor } from '../../definition/rest'; import { useEndpoint } from '../contexts/ServerContext'; -import { Params, PathFor, Return } from '../contexts/ServerContext/endpoints'; import { useToastMessageDispatch } from '../contexts/ToastMessagesContext'; import { AsyncState, useAsyncState } from './useAsyncState'; -const defaultParams = {}; - -export const useEndpointData =

    >( - endpoint: P, - params: Params<'GET', P>[0] = defaultParams as Params<'GET', P>[0], - initialValue?: Serialized> | (() => Serialized>), -): AsyncState>> & { reload: () => void } => { +export const useEndpointData = >( + endpoint: TPath, + params: void extends OperationParams<'GET', MatchPathPattern> + ? void + : Serialized< + OperationParams<'GET', MatchPathPattern> + > = undefined as void extends OperationParams<'GET', MatchPathPattern> + ? void + : Serialized>>, + initialValue?: + | Serialized>> + | (() => Serialized>>), +): AsyncState>>> & { + reload: () => void; +} => { const { resolve, reject, reset, ...state } = useAsyncState(initialValue); const dispatchToastMessage = useToastMessageDispatch(); const getData = useEndpoint('GET', endpoint); diff --git a/client/lib/2fa/utils.ts b/client/lib/2fa/utils.ts index 8331bcd0d4ab..148b10683c2d 100644 --- a/client/lib/2fa/utils.ts +++ b/client/lib/2fa/utils.ts @@ -3,13 +3,16 @@ import { Meteor } from 'meteor/meteor'; export const isTotpRequiredError = ( error: unknown, -): error is Meteor.Error & { error: 'totp-required' } => - (error as { error?: unknown } | undefined)?.error === 'totp-required'; +): error is Meteor.Error & ({ error: 'totp-required' } | { errorType: 'totp-required' }) => + typeof error === 'object' && + ((error as { error?: unknown } | undefined)?.error === 'totp-required' || + (error as { errorType?: unknown } | undefined)?.errorType === 'totp-required'); export const isTotpInvalidError = ( error: unknown, -): error is Meteor.Error & { error: 'totp-invalid' } => - (error as { error?: unknown } | undefined)?.error === 'totp-invalid'; +): error is Meteor.Error & ({ error: 'totp-invalid' } | { errorType: 'totp-invalid' }) => + (error as { error?: unknown } | undefined)?.error === 'totp-invalid' || + (error as { errorType?: unknown } | undefined)?.errorType === 'totp-invalid'; export const isLoginCancelledError = (error: unknown): error is Meteor.Error => error instanceof Meteor.Error && error.error === Accounts.LoginCancelledError.numericError; diff --git a/client/lib/RoomManager.ts b/client/lib/RoomManager.ts index 78a4104bddeb..f10a06c4257e 100644 --- a/client/lib/RoomManager.ts +++ b/client/lib/RoomManager.ts @@ -132,7 +132,7 @@ export const RoomManager = new (class RoomManager extends Emitter<{ } })(); -const subscribeVistedRooms: Subscription = { +const subscribeVisitedRooms: Subscription = { getCurrentValue: () => RoomManager.visitedRooms(), subscribe(callback) { return RoomManager.on('changed', callback); @@ -166,7 +166,7 @@ export const useHandleRoom = (rid: IRoom['_id']): AsyncState return state; }; -export const useVisitedRooms = (): IRoom['_id'][] => useSubscription(subscribeVistedRooms); +export const useVisitedRooms = (): IRoom['_id'][] => useSubscription(subscribeVisitedRooms); export const useOpenedRoom = (): IRoom['_id'] | undefined => useSubscription(subscribeOpenedRoom); diff --git a/client/lib/createRouteGroup.ts b/client/lib/createRouteGroup.ts index 3b39890facb8..a1c0d160cf2a 100644 --- a/client/lib/createRouteGroup.ts +++ b/client/lib/createRouteGroup.ts @@ -1,4 +1,5 @@ -import { FlowRouter } from 'meteor/kadira:flow-router'; +import { FlowRouter, Group, RouteOptions } from 'meteor/kadira:flow-router'; +import { ReactiveVar } from 'meteor/reactive-var'; import { Tracker } from 'meteor/tracker'; import { ComponentType, createElement, lazy, ReactNode } from 'react'; @@ -8,15 +9,82 @@ import { createTemplateForComponent } from './portals/createTemplateForComponent type RouteRegister = { ( path: string, - params: Parameters[1] & - ( - | {} - | { - lazyRouteComponent: () => Promise<{ default: ComponentType }>; - props: Record; - } - ), - ): void; + options: RouteOptions & { + lazyRouteComponent: () => Promise<{ default: ComponentType }>; + props?: Record; + ready?: boolean; + }, + ): [register: () => void, unregister: () => void]; + (path: string, options: RouteOptions): void; +}; + +const registerLazyComponentRoute = ( + routeGroup: Group, + importRouter: () => Promise<{ + default: ComponentType<{ + renderRoute?: () => ReactNode; + }>; + }>, + path: string, + { + lazyRouteComponent, + props, + ready = true, + ...rest + }: RouteOptions & { + lazyRouteComponent: () => Promise<{ default: ComponentType }>; + props?: Record; + ready?: boolean; + }, +): [register: () => void, unregister: () => void] => { + const enabled = new ReactiveVar(ready ? true : undefined); + let computation: Tracker.Computation | undefined; + + const handleEnter = ( + _context: unknown, + _redirect: (pathDef: string) => void, + stop: () => void, + ): void => { + const _enabled = Tracker.nonreactive(() => enabled.get()); + if (_enabled === false) { + stop(); + return; + } + + computation = Tracker.autorun(() => { + const _enabled = enabled.get(); + + if (_enabled === false) { + FlowRouter.go('/'); + } + }); + }; + + const handleExit = (): void => { + computation?.stop(); + }; + + const RouteComponent = lazy(lazyRouteComponent); + const renderRoute = (): ReactNode => createElement(RouteComponent, props); + + routeGroup.route(path, { + ...rest, + triggersEnter: [handleEnter, ...(rest.triggersEnter ?? [])], + triggersExit: [handleExit, ...(rest.triggersExit ?? [])], + action() { + const center = createTemplateForComponent( + Tracker.nonreactive(() => FlowRouter.getRouteName()), + importRouter, + { + attachment: 'at-parent', + props: () => ({ renderRoute }), + }, + ); + appLayout.render('main', { center }); + }, + }); + + return [(): void => enabled.set(true), (): void => enabled.set(false)]; }; export const createRouteGroup = ( @@ -33,32 +101,31 @@ export const createRouteGroup = ( prefix, }); - const registerRoute: RouteRegister = (path, options) => { + function registerRoute( + path: string, + options: RouteOptions & { + lazyRouteComponent: () => Promise<{ default: ComponentType }>; + props?: Record; + ready?: boolean; + }, + ): [register: () => void, unregister: () => void]; + function registerRoute(path: string, options: RouteOptions): void; + function registerRoute( + path: string, + options: + | RouteOptions + | (RouteOptions & { + lazyRouteComponent: () => Promise<{ default: ComponentType }>; + props?: Record; + ready?: boolean; + }), + ): [register: () => void, unregister: () => void] | void { if ('lazyRouteComponent' in options) { - const { lazyRouteComponent, props, ...rest } = options; - - const RouteComponent = lazy(lazyRouteComponent); - const renderRoute = (): ReactNode => createElement(RouteComponent, props); - - routeGroup.route(path, { - ...rest, - action() { - const center = createTemplateForComponent( - Tracker.nonreactive(() => FlowRouter.getRouteName()), - importRouter, - { - attachment: 'at-parent', - props: () => ({ renderRoute }), - }, - ); - appLayout.render('main', { center }); - }, - }); - return; + return registerLazyComponentRoute(routeGroup, importRouter, path, options); } routeGroup.route(path, options); - }; + } registerRoute('/', { name: `${name}-index`, diff --git a/client/lib/createSidebarItems.ts b/client/lib/createSidebarItems.ts index 17f3cc9a3bf8..0a2808e70b4b 100644 --- a/client/lib/createSidebarItems.ts +++ b/client/lib/createSidebarItems.ts @@ -2,6 +2,9 @@ import type { Subscription } from 'use-subscription'; type SidebarItem = { i18nLabel: string; + href?: string; + icon?: string; + permissionGranted?: boolean | (() => boolean); }; export const createSidebarItems = ( diff --git a/client/lib/download.spec.ts b/client/lib/download.spec.ts index 12a9809afdca..042495a4ed7e 100644 --- a/client/lib/download.spec.ts +++ b/client/lib/download.spec.ts @@ -1,85 +1,47 @@ -import 'jsdom-global/register'; -import chai from 'chai'; -import chaiSpies from 'chai-spies'; -import { after, before, describe, it } from 'mocha'; +import { expect, spy } from 'chai'; +import { describe, it } from 'mocha'; import { download, downloadAs, downloadCsvAs, downloadJsonAs } from './download'; -chai.use(chaiSpies); - -const withURL = (): void => { - let createObjectURL: typeof URL.createObjectURL; - let revokeObjectURL: typeof URL.revokeObjectURL; - - before(() => { - const blobs = new Map(); - - createObjectURL = window.URL.createObjectURL; - revokeObjectURL = window.URL.revokeObjectURL; - - window.URL.createObjectURL = (blob: Blob): string => { - const uuid = Math.random().toString(36).slice(2); - const url = `blob://${uuid}`; - blobs.set(url, blob); - return url; - }; - - window.URL.revokeObjectURL = (url: string): void => { - blobs.delete(url); - }; - }); - - after(() => { - window.URL.createObjectURL = createObjectURL; - window.URL.revokeObjectURL = revokeObjectURL; - }); -}; - describe('download', () => { it('should work', () => { - const listener = chai.spy(); + const listener = spy(); document.addEventListener('click', listener, false); download('about:blank', 'blank'); document.removeEventListener('click', listener, false); - chai.expect(listener).to.have.been.called(); + expect(listener).to.have.been.called(); }); }); describe('downloadAs', () => { - withURL(); - it('should work', () => { - const listener = chai.spy(); + const listener = spy(); document.addEventListener('click', listener, false); downloadAs({ data: [] }, 'blank'); document.removeEventListener('click', listener, false); - chai.expect(listener).to.have.been.called(); + expect(listener).to.have.been.called(); }); }); describe('downloadJsonAs', () => { - withURL(); - it('should work', () => { - const listener = chai.spy(); + const listener = spy(); document.addEventListener('click', listener, false); downloadJsonAs({}, 'blank'); document.removeEventListener('click', listener, false); - chai.expect(listener).to.have.been.called(); + expect(listener).to.have.been.called(); }); }); describe('downloadCsvAs', () => { - withURL(); - it('should work', () => { - const listener = chai.spy(); + const listener = spy(); document.addEventListener('click', listener, false); downloadCsvAs( @@ -91,6 +53,6 @@ describe('downloadCsvAs', () => { ); document.removeEventListener('click', listener, false); - chai.expect(listener).to.have.been.called(); + expect(listener).to.have.been.called(); }); }); diff --git a/client/lib/download.ts b/client/lib/download.ts index 8edb28bf2f8b..b0e361f1ff62 100644 --- a/client/lib/download.ts +++ b/client/lib/download.ts @@ -37,7 +37,7 @@ export const downloadJsonAs = (jsonObject: unknown, basename: string): void => { ); }; -export const downloadCsvAs = (csvData: unknown[][], basename: string): void => { +export const downloadCsvAs = (csvData: readonly (readonly unknown[])[], basename: string): void => { const escapeCell = (cell: unknown): string => `"${String(cell).replace(/"/g, '""')}"`; const content = csvData.reduce( (content, row) => `${content + row.map(escapeCell).join(';')}\n`, diff --git a/client/lib/meteorCallWrapper.ts b/client/lib/meteorCallWrapper.ts index 4fbb7f4aaeab..622e2df9b5ed 100644 --- a/client/lib/meteorCallWrapper.ts +++ b/client/lib/meteorCallWrapper.ts @@ -35,7 +35,7 @@ function wrapMeteorDDPCalls(): void { ); const restParams = { - message: DDPCommon.stringifyDDP(message), + message: DDPCommon.stringifyDDP({ ...message }), }; const processResult = (_message: any): void => { @@ -51,7 +51,7 @@ function wrapMeteorDDPCalls(): void { .then(({ message: _message }) => { processResult(_message); if (message.method === 'login') { - const parsedMessage = DDPCommon.parseDDP(_message); + const parsedMessage = DDPCommon.parseDDP(_message) as { result?: { token?: string } }; if (parsedMessage.result?.token) { Meteor.loginWithToken(parsedMessage.result.token); } diff --git a/client/lib/minimongo/bson.spec.ts b/client/lib/minimongo/bson.spec.ts index 1f2c5048f424..f71a38f95141 100644 --- a/client/lib/minimongo/bson.spec.ts +++ b/client/lib/minimongo/bson.spec.ts @@ -1,4 +1,4 @@ -import chai from 'chai'; +import { expect } from 'chai'; import { describe, it } from 'mocha'; import { getBSONType, compareBSONValues } from './bson'; @@ -6,32 +6,32 @@ import { BSONType } from './types'; describe('getBSONType', () => { it('should work', () => { - chai.expect(getBSONType(1)).to.be.equals(BSONType.Double); - chai.expect(getBSONType('xyz')).to.be.equals(BSONType.String); - chai.expect(getBSONType({})).to.be.equals(BSONType.Object); - chai.expect(getBSONType([])).to.be.equals(BSONType.Array); - chai.expect(getBSONType(new Uint8Array())).to.be.equals(BSONType.BinData); - chai.expect(getBSONType(undefined)).to.be.equals(BSONType.Object); - chai.expect(getBSONType(null)).to.be.equals(BSONType.Null); - chai.expect(getBSONType(false)).to.be.equals(BSONType.Boolean); - chai.expect(getBSONType(/.*/)).to.be.equals(BSONType.Regex); - chai.expect(getBSONType(() => true)).to.be.equals(BSONType.JavaScript); - chai.expect(getBSONType(new Date(0))).to.be.equals(BSONType.Date); + expect(getBSONType(1)).to.be.equals(BSONType.Double); + expect(getBSONType('xyz')).to.be.equals(BSONType.String); + expect(getBSONType({})).to.be.equals(BSONType.Object); + expect(getBSONType([])).to.be.equals(BSONType.Array); + expect(getBSONType(new Uint8Array())).to.be.equals(BSONType.BinData); + expect(getBSONType(undefined)).to.be.equals(BSONType.Object); + expect(getBSONType(null)).to.be.equals(BSONType.Null); + expect(getBSONType(false)).to.be.equals(BSONType.Boolean); + expect(getBSONType(/.*/)).to.be.equals(BSONType.Regex); + expect(getBSONType(() => true)).to.be.equals(BSONType.JavaScript); + expect(getBSONType(new Date(0))).to.be.equals(BSONType.Date); }); }); describe('compareBSONValues', () => { it('should work for the same types', () => { - chai.expect(compareBSONValues(2, 3)).to.be.equals(-1); - chai.expect(compareBSONValues('xyz', 'abc')).to.be.equals(1); - chai.expect(compareBSONValues({}, {})).to.be.equals(0); - chai.expect(compareBSONValues(true, false)).to.be.equals(1); - chai.expect(compareBSONValues(new Date(0), new Date(1))).to.be.equals(-1); + expect(compareBSONValues(2, 3)).to.be.equals(-1); + expect(compareBSONValues('xyz', 'abc')).to.be.equals(1); + expect(compareBSONValues({}, {})).to.be.equals(0); + expect(compareBSONValues(true, false)).to.be.equals(1); + expect(compareBSONValues(new Date(0), new Date(1))).to.be.equals(-1); }); it('should work for different types', () => { - chai.expect(compareBSONValues(2, null)).to.be.equals(1); - chai.expect(compareBSONValues('xyz', {})).to.be.equals(-1); - chai.expect(compareBSONValues(false, 3)).to.be.equals(1); + expect(compareBSONValues(2, null)).to.be.equals(1); + expect(compareBSONValues('xyz', {})).to.be.equals(-1); + expect(compareBSONValues(false, 3)).to.be.equals(1); }); }); diff --git a/client/lib/minimongo/comparisons.spec.ts b/client/lib/minimongo/comparisons.spec.ts index eb32433d9a81..3048223f51ac 100644 --- a/client/lib/minimongo/comparisons.spec.ts +++ b/client/lib/minimongo/comparisons.spec.ts @@ -1,4 +1,4 @@ -import chai from 'chai'; +import { expect } from 'chai'; import { describe, it } from 'mocha'; import { equals, isObject, flatSome, some, isEmptyArray } from './comparisons'; @@ -6,57 +6,57 @@ import { equals, isObject, flatSome, some, isEmptyArray } from './comparisons'; describe('Comparisons service', () => { describe('equals', () => { it('should return true if two numbers are equal', () => { - chai.expect(equals(1, 1)).to.be.equal(true); + expect(equals(1, 1)).to.be.equal(true); }); it('should return false if arguments are null or undefined', () => { - chai.expect(equals(undefined, null)).to.be.equal(false); - chai.expect(equals(null, undefined)).to.be.equal(false); + expect(equals(undefined, null)).to.be.equal(false); + expect(equals(null, undefined)).to.be.equal(false); }); it('should return false if arguments arent objects and they are not the same', () => { - chai.expect(equals('not', 'thesame')).to.be.equal(false); + expect(equals('not', 'thesame')).to.be.equal(false); }); it('should return true if date objects provided have the same value', () => { const currentDate = new Date(); - chai.expect(equals(currentDate, currentDate)).to.be.equal(true); + expect(equals(currentDate, currentDate)).to.be.equal(true); }); it('should return true if 2 equal UInt8Array are provided', () => { const arr1 = new Uint8Array([1, 2]); const arr2 = new Uint8Array([1, 2]); - chai.expect(equals(arr1, arr2)).to.be.equal(true); + expect(equals(arr1, arr2)).to.be.equal(true); }); it('should return true if 2 equal arrays are provided', () => { const arr1 = [1, 2, 4]; const arr2 = [1, 2, 4]; - chai.expect(equals(arr1, arr2)).to.be.equal(true); + expect(equals(arr1, arr2)).to.be.equal(true); }); it('should return false if 2 arrays with different length are provided', () => { const arr1 = [1, 4, 5]; const arr2 = [1, 4, 5, 7]; - chai.expect(equals(arr1, arr2)).to.be.equal(false); + expect(equals(arr1, arr2)).to.be.equal(false); }); it('should return true if the objects provided are "equal"', () => { const obj = { a: 1 }; const obj2 = obj; - chai.expect(equals(obj, obj2)).to.be.equal(true); + expect(equals(obj, obj2)).to.be.equal(true); }); it('should return true if both objects have the same keys', () => { const obj = { a: 1 }; const obj2 = { a: 1 }; - chai.expect(equals(obj, obj2)).to.be.equal(true); + expect(equals(obj, obj2)).to.be.equal(true); }); }); @@ -65,14 +65,14 @@ describe('Comparisons service', () => { const obj = {}; const func = (a: any): any => a; - chai.expect(isObject(obj)).to.be.equal(true); - chai.expect(isObject(func)).to.be.equal(true); + expect(isObject(obj)).to.be.equal(true); + expect(isObject(func)).to.be.equal(true); }); it('should return false for other data types', () => { - chai.expect(isObject(1)).to.be.equal(false); - chai.expect(isObject(true)).to.be.equal(false); - chai.expect(isObject('212')).to.be.equal(false); + expect(isObject(1)).to.be.equal(false); + expect(isObject(true)).to.be.equal(false); + expect(isObject('212')).to.be.equal(false); }); }); @@ -81,14 +81,14 @@ describe('Comparisons service', () => { const arr = [1, 2, 4, 6, 9]; const isEven = (v: number): boolean => v % 2 === 0; - chai.expect(flatSome(arr, isEven)).to.be.equal(true); + expect(flatSome(arr, isEven)).to.be.equal(true); }); it('should run the function on the value when its not an array', () => { const val = 1; const isEven = (v: number): boolean => v % 2 === 0; - chai.expect(flatSome(val, isEven)).to.be.equal(false); + expect(flatSome(val, isEven)).to.be.equal(false); }); }); @@ -102,7 +102,7 @@ describe('Comparisons service', () => { return v % 2 === 0; }; - chai.expect(some(arr, isEven)).to.be.equal(true); + expect(some(arr, isEven)).to.be.equal(true); }); it('should run the function on the value when its not an array', () => { @@ -114,21 +114,21 @@ describe('Comparisons service', () => { return v % 2 === 0; }; - chai.expect(some(val, isEven)).to.be.equal(false); + expect(some(val, isEven)).to.be.equal(false); }); }); describe('isEmptyArray', () => { it('should return true if array is empty', () => { - chai.expect(isEmptyArray([])).to.be.equal(true); + expect(isEmptyArray([])).to.be.equal(true); }); it('should return false if value is not an array', () => { - chai.expect(isEmptyArray(1)).to.be.equal(false); + expect(isEmptyArray(1)).to.be.equal(false); }); it('should return false if array is not empty', () => { - chai.expect(isEmptyArray([1, 2])).to.be.equal(false); + expect(isEmptyArray([1, 2])).to.be.equal(false); }); }); }); diff --git a/client/lib/minimongo/lookups.spec.ts b/client/lib/minimongo/lookups.spec.ts index 1056a3d7f5c8..3bae10346b62 100644 --- a/client/lib/minimongo/lookups.spec.ts +++ b/client/lib/minimongo/lookups.spec.ts @@ -1,15 +1,17 @@ -import chai from 'chai'; +import { expect } from 'chai'; import { describe, it } from 'mocha'; import { createLookupFunction } from './lookups'; describe('createLookupFunction', () => { it('should work', () => { - chai.expect(createLookupFunction('a.x')({ a: { x: 1 } })).to.be.deep.equals([1]); - chai.expect(createLookupFunction('a.x')({ a: { x: [1] } })).to.be.deep.equals([[1]]); - chai.expect(createLookupFunction('a.x')({ a: 5 })).to.be.deep.equals([undefined]); - chai - .expect(createLookupFunction('a.x')({ a: [{ x: 1 }, { x: [2] }, { y: 3 }] })) - .to.be.deep.equals([1, [2], undefined]); + expect(createLookupFunction('a.x')({ a: { x: 1 } })).to.be.deep.equals([1]); + expect(createLookupFunction('a.x')({ a: { x: [1] } })).to.be.deep.equals([[1]]); + expect(createLookupFunction('a.x')({ a: 5 })).to.be.deep.equals([undefined]); + expect(createLookupFunction('a.x')({ a: [{ x: 1 }, { x: [2] }, { y: 3 }] })).to.be.deep.equals([ + 1, + [2], + undefined, + ]); }); }); diff --git a/client/lib/queryClient.ts b/client/lib/queryClient.ts new file mode 100644 index 000000000000..0fd037d304c0 --- /dev/null +++ b/client/lib/queryClient.ts @@ -0,0 +1,3 @@ +import { QueryClient } from 'react-query'; + +export const queryClient = new QueryClient(); diff --git a/client/lib/userData.ts b/client/lib/userData.ts index 2b37d1957f5a..76488ead8013 100644 --- a/client/lib/userData.ts +++ b/client/lib/userData.ts @@ -5,14 +5,39 @@ import { Users } from '../../app/models/client'; import { Notifications } from '../../app/notifications/client'; import { APIClient } from '../../app/utils/client'; import type { IUser, IUserDataEvent } from '../../definition/IUser'; +import { Serialized } from '../../definition/Serialized'; export const isSyncReady = new ReactiveVar(false); -type RawUserData = Omit & { - _updatedAt: string; -}; +type RawUserData = Serialized< + Pick< + IUser, + | '_id' + | 'type' + | 'name' + | 'username' + | 'emails' + | 'status' + | 'statusDefault' + | 'statusText' + | 'statusConnection' + | 'avatarOrigin' + | 'utcOffset' + | 'language' + | 'settings' + | 'roles' + | 'active' + | 'defaultRoom' + | 'customFields' + | 'statusLivechat' + | 'oauth' + | 'createdAt' + | '_updatedAt' + | 'avatarETag' + > +>; -const updateUser = (userData: IUser & { _updatedAt: Date }): void => { +const updateUser = (userData: IUser): void => { const user: IUser = Users.findOne({ _id: userData._id }); if (!user || !user._updatedAt || user._updatedAt.getTime() < userData._updatedAt.getTime()) { @@ -57,6 +82,7 @@ export const synchronizeUserData = async (uid: Meteor.User['_id']): Promise( }); }); -const callEndpoint = >( - method: M, - path: P, - params: Params[0], -): Promise>> => { +const callEndpoint = >( + method: TMethod, + path: TPath, + params: Serialized>>, +): Promise>>> => { const api = path[0] === '/' ? APIClient : APIClient.v1; const endpointPath = path[0] === '/' ? path.slice(1) : path; @@ -62,18 +65,6 @@ const uploadToEndpoint = (endpoint: string, params: any, formData: any): Promise return APIClient.v1.upload(endpoint, params, formData).promise; }; -declare module 'meteor/meteor' { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Meteor { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace StreamerCentral { - const instances: { - [name: string]: typeof Meteor.Streamer; - }; - } - } -} - const getStream = ( streamName: string, options: {} = {}, diff --git a/client/providers/UserProvider.tsx b/client/providers/UserProvider.tsx index 45a91b6adf97..ebf37f035fb7 100644 --- a/client/providers/UserProvider.tsx +++ b/client/providers/UserProvider.tsx @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import React, { useMemo, FC } from 'react'; -import { callbacks } from '../../app/callbacks/client'; +import { callbacks } from '../../app/callbacks/lib/callbacks'; import { Subscriptions, Rooms } from '../../app/models/client'; import { getUserPreference } from '../../app/utils/client'; import { IRoom } from '../../definition/IRoom'; diff --git a/client/sidebar/header/UserDropdown.js b/client/sidebar/header/UserDropdown.js index c9a058bb50eb..f98aef084449 100644 --- a/client/sidebar/header/UserDropdown.js +++ b/client/sidebar/header/UserDropdown.js @@ -3,7 +3,7 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { FlowRouter } from 'meteor/kadira:flow-router'; import React from 'react'; -import { callbacks } from '../../../app/callbacks/client'; +import { callbacks } from '../../../app/callbacks/lib/callbacks'; import { popover, AccountBox, SideNav } from '../../../app/ui-utils/client'; import { userStatus } from '../../../app/user-status/client'; import MarkdownText from '../../components/MarkdownText'; @@ -34,6 +34,7 @@ const ADMIN_PERMISSIONS = [ 'manage-incoming-integrations', 'manage-own-outgoing-integrations', 'manage-own-incoming-integrations', + 'view-engagement-dashboard', ]; const style = { diff --git a/client/sidebar/sections/Omnichannel.js b/client/sidebar/sections/Omnichannel.js index 3a18ac5b6347..6cecae5f7766 100644 --- a/client/sidebar/sections/Omnichannel.js +++ b/client/sidebar/sections/Omnichannel.js @@ -2,12 +2,14 @@ import { Sidebar } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import React, { memo } from 'react'; +import { hasPermission } from '../../../app/authorization/client'; +import { useLayout } from '../../contexts/LayoutContext'; import { useOmnichannelShowQueueLink, useOmnichannelAgentAvailable, useOmnichannelQueueLink, - useOmnichannelDirectoryLink, } from '../../contexts/OmnichannelContext'; +import { useRoute } from '../../contexts/RouterContext'; import { useMethod } from '../../contexts/ServerContext'; import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; import { useTranslation } from '../../contexts/TranslationContext'; @@ -18,7 +20,8 @@ const OmnichannelSection = (props) => { const agentAvailable = useOmnichannelAgentAvailable(); const showOmnichannelQueueLink = useOmnichannelShowQueueLink(); const queueLink = useOmnichannelQueueLink(); - const directoryLink = useOmnichannelDirectoryLink(); + const { sidebar } = useLayout(); + const directoryRoute = useRoute('omnichannel-directory'); const dispatchToastMessage = useToastMessageDispatch(); const icon = { @@ -41,6 +44,11 @@ const OmnichannelSection = (props) => { } }); + const handleDirectory = useMutableCallback(() => { + sidebar.toggle(); + directoryRoute.push({}); + }); + return ( {t('Omnichannel')} @@ -49,7 +57,9 @@ const OmnichannelSection = (props) => { )} - + {hasPermission(['view-omnichannel-contact-center']) && ( + + )} ); diff --git a/client/startup/banners.ts b/client/startup/banners.ts index 57e0d9c7daa9..64ba23041acc 100644 --- a/client/startup/banners.ts +++ b/client/startup/banners.ts @@ -4,14 +4,15 @@ import { Tracker } from 'meteor/tracker'; import { Notifications } from '../../app/notifications/client'; import { APIClient } from '../../app/utils/client'; import { IBanner, BannerPlatform } from '../../definition/IBanner'; +import { Serialized } from '../../definition/Serialized'; import * as banners from '../lib/banners'; const fetchInitialBanners = async (): Promise => { - const response = (await APIClient.get('v1/banners', { - platform: BannerPlatform.Web, - })) as { + const response: Serialized<{ banners: IBanner[]; - }; + }> = await APIClient.get('v1/banners', { + platform: BannerPlatform.Web, + }); for (const banner of response.banners) { banners.open({ @@ -22,11 +23,11 @@ const fetchInitialBanners = async (): Promise => { }; const handleBanner = async (event: { bannerId: string }): Promise => { - const response = (await APIClient.get(`v1/banners/${event.bannerId}`, { - platform: BannerPlatform.Web, - })) as { + const response: Serialized<{ banners: IBanner[]; - }; + }> = await APIClient.get(`v1/banners/${event.bannerId}`, { + platform: BannerPlatform.Web, + }); if (!response.banners.length) { return banners.closeById(event.bannerId); diff --git a/client/startup/renderMessage/autolinker.ts b/client/startup/renderMessage/autolinker.ts index bf240aab3ea7..174da6e915a0 100644 --- a/client/startup/renderMessage/autolinker.ts +++ b/client/startup/renderMessage/autolinker.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { callbacks } from '../../../app/callbacks/client'; +import { callbacks } from '../../../app/callbacks/lib/callbacks'; import { settings } from '../../../app/settings/client'; Meteor.startup(() => { diff --git a/client/startup/renderMessage/autotranslate.ts b/client/startup/renderMessage/autotranslate.ts index 9142d4670a36..be727cd743db 100644 --- a/client/startup/renderMessage/autotranslate.ts +++ b/client/startup/renderMessage/autotranslate.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { hasPermission } from '../../../app/authorization/client'; -import { callbacks } from '../../../app/callbacks/client'; +import { callbacks } from '../../../app/callbacks/lib/callbacks'; import { settings } from '../../../app/settings/client'; Meteor.startup(() => { diff --git a/client/startup/renderMessage/emoji.ts b/client/startup/renderMessage/emoji.ts index a5abc9d82773..ddb3bf318b45 100644 --- a/client/startup/renderMessage/emoji.ts +++ b/client/startup/renderMessage/emoji.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { callbacks } from '../../../app/callbacks/client'; +import { callbacks } from '../../../app/callbacks/lib/callbacks'; import { getUserPreference } from '../../../app/utils/client'; Meteor.startup(() => { diff --git a/client/startup/renderMessage/hexcolor.ts b/client/startup/renderMessage/hexcolor.ts index c24b9dc559e9..aba80d6f0a0a 100644 --- a/client/startup/renderMessage/hexcolor.ts +++ b/client/startup/renderMessage/hexcolor.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { callbacks } from '../../../app/callbacks/client'; +import { callbacks } from '../../../app/callbacks/lib/callbacks'; import { settings } from '../../../app/settings/client'; Meteor.startup(() => { diff --git a/client/startup/renderMessage/highlightWords.ts b/client/startup/renderMessage/highlightWords.ts index 928565d238c5..d95887d18b3a 100644 --- a/client/startup/renderMessage/highlightWords.ts +++ b/client/startup/renderMessage/highlightWords.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { callbacks } from '../../../app/callbacks/client'; +import { callbacks } from '../../../app/callbacks/lib/callbacks'; import { getUserPreference } from '../../../app/utils/client'; Meteor.startup(() => { diff --git a/client/startup/renderMessage/issuelink.ts b/client/startup/renderMessage/issuelink.ts index 7a465e15b19a..643615a6b684 100644 --- a/client/startup/renderMessage/issuelink.ts +++ b/client/startup/renderMessage/issuelink.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { callbacks } from '../../../app/callbacks/client'; +import { callbacks } from '../../../app/callbacks/lib/callbacks'; import { settings } from '../../../app/settings/client'; Meteor.startup(() => { diff --git a/client/startup/renderMessage/katex.ts b/client/startup/renderMessage/katex.ts index 4f5d042d1b3a..d48bb471a9b5 100644 --- a/client/startup/renderMessage/katex.ts +++ b/client/startup/renderMessage/katex.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { callbacks } from '../../../app/callbacks/client'; +import { callbacks } from '../../../app/callbacks/lib/callbacks'; import { settings } from '../../../app/settings/client'; Meteor.startup(() => { diff --git a/client/startup/renderMessage/markdown.ts b/client/startup/renderMessage/markdown.ts index e38f62bb89bc..e00aee6fb937 100644 --- a/client/startup/renderMessage/markdown.ts +++ b/client/startup/renderMessage/markdown.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { callbacks } from '../../../app/callbacks/client'; +import { callbacks } from '../../../app/callbacks/lib/callbacks'; import { settings } from '../../../app/settings/client'; Meteor.startup(() => { diff --git a/client/startup/renderMessage/mentionsMessage.ts b/client/startup/renderMessage/mentionsMessage.ts index e7dc900bc038..6db644599895 100644 --- a/client/startup/renderMessage/mentionsMessage.ts +++ b/client/startup/renderMessage/mentionsMessage.ts @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { callbacks } from '../../../app/callbacks/client'; +import { callbacks } from '../../../app/callbacks/lib/callbacks'; import { Users } from '../../../app/models/client'; import { settings } from '../../../app/settings/client'; diff --git a/client/startup/renderNotification/markdown.ts b/client/startup/renderNotification/markdown.ts index 80b28bed11cc..c2f8179852b4 100644 --- a/client/startup/renderNotification/markdown.ts +++ b/client/startup/renderNotification/markdown.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../../app/callbacks/client'; +import { callbacks } from '../../../app/callbacks/lib/callbacks'; import { settings } from '../../../app/settings/client'; Meteor.startup(() => { diff --git a/client/startup/routes.ts b/client/startup/routes.ts index 2c17a3bb003d..aa6e5dc8ea1b 100644 --- a/client/startup/routes.ts +++ b/client/startup/routes.ts @@ -1,10 +1,13 @@ import { FlowRouter } from 'meteor/kadira:flow-router'; import { Meteor } from 'meteor/meteor'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { Session } from 'meteor/session'; import { Tracker } from 'meteor/tracker'; import { lazy } from 'react'; +import toastr from 'toastr'; import { KonchatNotification } from '../../app/ui/client'; +import { APIClient } from '../../app/utils/client'; import { IUser } from '../../definition/IUser'; import { appLayout } from '../lib/appLayout'; import { createTemplateForComponent } from '../lib/portals/createTemplateForComponent'; @@ -14,6 +17,7 @@ import { handleError } from '../lib/utils/handleError'; const SetupWizardRoute = lazy(() => import('../views/setupWizard/SetupWizardRoute')); const MailerUnsubscriptionPage = lazy(() => import('../views/mailer/MailerUnsubscriptionPage')); const NotFoundPage = lazy(() => import('../views/notFound/NotFoundPage')); +const MeetPage = lazy(() => import('../views/meet/MeetPage')); FlowRouter.wait(); @@ -50,6 +54,25 @@ FlowRouter.route('/login', { }, }); +FlowRouter.route('/meet/:rid', { + name: 'meet', + + async action(_params, queryParams) { + if (queryParams?.token !== undefined) { + // visitor login + const visitor = await APIClient.v1.get(`livechat/visitor/${queryParams?.token}`); + if (visitor?.visitor) { + return appLayout.render({ component: MeetPage }); + } + return toastr.error(TAPi18n.__('Visitor_does_not_exist')); + } + if (!Meteor.userId()) { + FlowRouter.go('home'); + } + appLayout.render({ component: MeetPage }); + }, +}); + FlowRouter.route('/home', { name: 'home', diff --git a/client/startup/streamMessage/autotranslate.ts b/client/startup/streamMessage/autotranslate.ts index c543998999b9..f788b1565109 100644 --- a/client/startup/streamMessage/autotranslate.ts +++ b/client/startup/streamMessage/autotranslate.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { hasPermission } from '../../../app/authorization/client'; -import { callbacks } from '../../../app/callbacks/client'; +import { callbacks } from '../../../app/callbacks/lib/callbacks'; import { settings } from '../../../app/settings/client'; Meteor.startup(() => { diff --git a/client/startup/userStatusManuallySet.ts b/client/startup/userStatusManuallySet.ts index 46c052c88395..b969e621ba1d 100644 --- a/client/startup/userStatusManuallySet.ts +++ b/client/startup/userStatusManuallySet.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../app/callbacks/client'; +import { callbacks } from '../../app/callbacks/lib/callbacks'; import { UserStatus } from '../../definition/UserStatus'; import { fireGlobalEvent } from '../lib/utils/fireGlobalEvent'; diff --git a/client/types/less-browser.d.ts b/client/types/less-browser.d.ts deleted file mode 100644 index d9bc07eff65b..000000000000 --- a/client/types/less-browser.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare function createLess(window: Window, options: Less.Options): LessStatic; - -declare module 'less/browser' { - export = createLess; -} diff --git a/client/types/main.d.ts b/client/types/main.d.ts deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/client/types/meteor-mongo.d.ts b/client/types/meteor-mongo.d.ts deleted file mode 100644 index fa23ee6be63f..000000000000 --- a/client/types/meteor-mongo.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -declare module 'meteor/mongo' { - namespace Mongo { - // eslint-disable-next-line @typescript-eslint/interface-name-prefix - interface CollectionStatic { - new ( - name: string | null, - options?: { - connection?: object | null; - idGeneration?: string; - transform?: Function | null; - }, - ): Collection; - } - } -} diff --git a/client/views/account/security/EndToEnd.js b/client/views/account/security/EndToEnd.js index 740ad54c5828..e7d1121dd7cd 100644 --- a/client/views/account/security/EndToEnd.js +++ b/client/views/account/security/EndToEnd.js @@ -1,16 +1,22 @@ import { Box, Margins, PasswordInput, Field, FieldGroup, Button } from '@rocket.chat/fuselage'; -import { useLocalStorage } from '@rocket.chat/fuselage-hooks'; +import { useLocalStorage, useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { Meteor } from 'meteor/meteor'; import React, { useCallback, useEffect } from 'react'; +import { callbacks } from '../../../../app/callbacks/lib/callbacks'; import { e2e } from '../../../../app/e2e/client/rocketchat.e2e'; +import { useRoute } from '../../../contexts/RouterContext'; import { useMethod } from '../../../contexts/ServerContext'; import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext'; import { useTranslation } from '../../../contexts/TranslationContext'; +import { useUser } from '../../../contexts/UserContext'; import { useForm } from '../../../hooks/useForm'; const EndToEnd = (props) => { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); + const homeRoute = useRoute('home'); + const user = useUser(); const publicKey = useLocalStorage('public_key'); const privateKey = useLocalStorage('private_key'); @@ -30,6 +36,14 @@ const EndToEnd = (props) => { : undefined; const canSave = keysExist && !passwordError && passwordConfirm.length > 0; + const handleLogout = useMutableCallback(() => { + Meteor.logout(() => { + callbacks.run('afterLogoutCleanUp', user); + Meteor.call('logoutCleanUp', user); + homeRoute.push({}); + }); + }); + const saveNewPassword = useCallback(async () => { try { await e2e.changePassword(password); @@ -45,11 +59,12 @@ const EndToEnd = (props) => { const result = await resetE2eKey(); if (result) { dispatchToastMessage({ type: 'success', message: t('User_e2e_key_was_reset') }); + handleLogout(); } } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } - }, [dispatchToastMessage, resetE2eKey, t]); + }, [dispatchToastMessage, resetE2eKey, handleLogout, t]); useEffect(() => { if (password.trim() === '') { diff --git a/client/views/admin/apps/AppRow.tsx b/client/views/admin/apps/AppRow.tsx index 01104d18689d..762257cb5c86 100644 --- a/client/views/admin/apps/AppRow.tsx +++ b/client/views/admin/apps/AppRow.tsx @@ -93,7 +93,6 @@ const AppRow: FC = ({ medium, ...props }) => { {current} ))} - ; )} diff --git a/client/views/admin/apps/AppsPage.js b/client/views/admin/apps/AppsPage.js deleted file mode 100644 index 2c4c47b7cdc9..000000000000 --- a/client/views/admin/apps/AppsPage.js +++ /dev/null @@ -1,46 +0,0 @@ -import { Button, ButtonGroup, Icon } from '@rocket.chat/fuselage'; -import React from 'react'; - -import Page from '../../../components/Page'; -import { useRoute } from '../../../contexts/RouterContext'; -import { useSetting } from '../../../contexts/SettingsContext'; -import { useTranslation } from '../../../contexts/TranslationContext'; -import AppsTable from './AppsTable'; - -function AppsPage() { - const t = useTranslation(); - - const isDevelopmentMode = useSetting('Apps_Framework_Development_Mode'); - const marketplaceRoute = useRoute('admin-marketplace'); - const appsRoute = useRoute('admin-apps'); - - const handleUploadButtonClick = () => { - appsRoute.push({ context: 'install' }); - }; - - const handleViewMarketplaceButtonClick = () => { - marketplaceRoute.push(); - }; - - return ( - - - - {isDevelopmentMode && ( - - )} - - - - - - - - ); -} - -export default AppsPage; diff --git a/client/views/admin/apps/AppsPage.tsx b/client/views/admin/apps/AppsPage.tsx new file mode 100644 index 000000000000..ab42a902de2c --- /dev/null +++ b/client/views/admin/apps/AppsPage.tsx @@ -0,0 +1,84 @@ +import { Button, ButtonGroup, Icon, Skeleton, Tabs } from '@rocket.chat/fuselage'; +import React, { useEffect, useState, ReactElement } from 'react'; + +import Page from '../../../components/Page'; +import { useRoute } from '../../../contexts/RouterContext'; +import { useMethod } from '../../../contexts/ServerContext'; +import { useSetting } from '../../../contexts/SettingsContext'; +import { useTranslation } from '../../../contexts/TranslationContext'; +import AppsTable from './AppsTable'; +import MarketplaceTable from './MarketplaceTable'; + +type AppsPageProps = { + isMarketPlace: boolean; + context: string; +}; + +const AppsPage = ({ isMarketPlace, context }: AppsPageProps): ReactElement => { + const t = useTranslation(); + + const isDevelopmentMode = useSetting('Apps_Framework_Development_Mode'); + const marketplaceRoute = useRoute('admin-marketplace'); + const appsRoute = useRoute('admin-apps'); + const cloudRoute = useRoute('cloud'); + const checkUserLoggedIn = useMethod('cloud:checkUserLoggedIn'); + + const [isLoggedInCloud, setIsLoggedInCloud] = useState(); + + useEffect(() => { + const initialize = async (): Promise => { + setIsLoggedInCloud(await checkUserLoggedIn()); + }; + initialize(); + }, [checkUserLoggedIn]); + + const handleLoginButtonClick = (): void => { + cloudRoute.push(); + }; + + const handleUploadButtonClick = (): void => { + appsRoute.push({ context: 'install' }); + }; + + return ( + + + + {isMarketPlace && !isLoggedInCloud && ( + + )} + {Boolean(isDevelopmentMode) && ( + + )} + + + + marketplaceRoute.push({ context: '' })} + selected={isMarketPlace} + > + {t('Marketplace')} + + marketplaceRoute.push({ context: 'installed' })} + selected={context === 'installed'} + > + {t('Installed')} + + + {context === 'installed' ? : } + + ); +}; + +export default AppsPage; diff --git a/client/views/admin/apps/AppsRoute.js b/client/views/admin/apps/AppsRoute.js index 9c0299c653dd..6cdeb5f710ce 100644 --- a/client/views/admin/apps/AppsRoute.js +++ b/client/views/admin/apps/AppsRoute.js @@ -3,16 +3,15 @@ import React, { useState, useEffect } from 'react'; import NotAuthorizedPage from '../../../components/NotAuthorizedPage'; import PageSkeleton from '../../../components/PageSkeleton'; import { usePermission } from '../../../contexts/AuthorizationContext'; -import { useRouteParameter, useRoute, useCurrentRoute } from '../../../contexts/RouterContext'; +import { useRouteParameter, useRoute } from '../../../contexts/RouterContext'; import { useMethod } from '../../../contexts/ServerContext'; import AppDetailsPage from './AppDetailsPage'; import AppInstallPage from './AppInstallPage'; import AppLogsPage from './AppLogsPage'; import AppsPage from './AppsPage'; import AppsProvider from './AppsProvider'; -import MarketplacePage from './MarketplacePage'; -function AppsRoute() { +const AppsRoute = () => { const [isLoading, setLoading] = useState(true); const canViewAppsAndMarketplace = usePermission('manage-apps'); const isAppsEngineEnabled = useMethod('apps/is-enabled'); @@ -45,11 +44,10 @@ function AppsRoute() { }; }, [canViewAppsAndMarketplace, isAppsEngineEnabled, appsWhatIsItRoute]); - const [currentRouteName] = useCurrentRoute(); + const context = useRouteParameter('context'); - const isMarketPlace = currentRouteName === 'admin-marketplace'; + const isMarketPlace = !context; - const context = useRouteParameter('context'); const id = useRouteParameter('id'); const version = useRouteParameter('version'); @@ -63,13 +61,14 @@ function AppsRoute() { return ( - {(!context && isMarketPlace && ) || - (!context && !isMarketPlace && ) || + {((!context || context === 'installed') && ( + + )) || (context === 'details' && ) || (context === 'logs' && ) || (context === 'install' && )} ); -} +}; export default AppsRoute; diff --git a/client/views/admin/customEmoji/CustomEmoji.js b/client/views/admin/customEmoji/CustomEmoji.js deleted file mode 100644 index b5474ab6d3ef..000000000000 --- a/client/views/admin/customEmoji/CustomEmoji.js +++ /dev/null @@ -1,65 +0,0 @@ -import { Box, Table } from '@rocket.chat/fuselage'; -import React, { useMemo } from 'react'; - -import FilterByText from '../../../components/FilterByText'; -import GenericTable from '../../../components/GenericTable'; -import { useTranslation } from '../../../contexts/TranslationContext'; - -function CustomEmoji({ data, sort, onClick, onHeaderClick, setParams, params }) { - const t = useTranslation(); - - const header = useMemo( - () => [ - - {t('Name')} - , - - {t('Aliases')} - , - ], - [onHeaderClick, sort, t], - ); - - const renderRow = (emojis) => { - const { _id, name, aliases } = emojis; - return ( - - - {name} - - - {aliases} - - - ); - }; - - return ( - } - /> - ); -} - -export default CustomEmoji; diff --git a/client/views/admin/customEmoji/CustomEmoji.tsx b/client/views/admin/customEmoji/CustomEmoji.tsx new file mode 100644 index 000000000000..c674080821c7 --- /dev/null +++ b/client/views/admin/customEmoji/CustomEmoji.tsx @@ -0,0 +1,121 @@ +import { Box, Pagination } from '@rocket.chat/fuselage'; +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import React, { FC, MutableRefObject, useEffect, useMemo, useState } from 'react'; + +import FilterByText from '../../../components/FilterByText'; +import { + GenericTable, + GenericTableBody, + GenericTableCell, + GenericTableHeader, + GenericTableHeaderCell, + GenericTableLoadingTable, + GenericTableRow, +} from '../../../components/GenericTable'; +import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; +import { useSort } from '../../../components/GenericTable/hooks/useSort'; +import { useTranslation } from '../../../contexts/TranslationContext'; +import { useEndpointData } from '../../../hooks/useEndpointData'; +import { AsyncStatePhase } from '../../../lib/asyncState'; + +type CustomEmojiProps = { + reload: MutableRefObject<() => void>; + onClick: (emoji: string) => () => void; +}; + +const CustomEmoji: FC = function CustomEmoji({ onClick, reload }) { + const t = useTranslation(); + + const { + current, + itemsPerPage, + setItemsPerPage: onSetItemsPerPage, + setCurrent: onSetCurrent, + ...paginationProps + } = usePagination(); + + const [text, setText] = useState(''); + + const { sortBy, sortDirection, setSort } = useSort<'name'>('name'); + + const query = useDebouncedValue( + useMemo( + () => ({ + query: JSON.stringify({ name: { $regex: text || '', $options: 'i' } }), + sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`, + count: itemsPerPage, + offset: current, + }), + [text, itemsPerPage, current, sortBy, sortDirection], + ), + 500, + ); + + const { value: data, phase, reload: reloadEndPoint } = useEndpointData('emoji-custom.all', query); + + useEffect(() => { + reload.current = reloadEndPoint; + }, [reload, reloadEndPoint]); + return ( + <> + setText(text)} /> + + + + {t('Name')} + + + {t('Aliases')} + + + + {phase === AsyncStatePhase.LOADING && } + {phase === AsyncStatePhase.RESOLVED && + data && + data.emojis && + data.emojis.length > 0 && + data?.emojis.map((emojis) => ( + + + {emojis.name} + + + {emojis.aliases} + + + ))} + {/* {phase === AsyncStatePhase.RESOLVED && + !data.emojis.length + ))} */} + + + {phase === AsyncStatePhase.RESOLVED && ( + + )} + + ); +}; + +export default CustomEmoji; diff --git a/client/views/admin/customEmoji/CustomEmojiRoute.js b/client/views/admin/customEmoji/CustomEmojiRoute.js index d1430d21062a..36cb32d84a62 100644 --- a/client/views/admin/customEmoji/CustomEmojiRoute.js +++ b/client/views/admin/customEmoji/CustomEmojiRoute.js @@ -1,6 +1,5 @@ import { Button, Icon } from '@rocket.chat/fuselage'; -import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import React, { useMemo, useState, useCallback } from 'react'; +import React, { useCallback, useRef } from 'react'; import NotAuthorizedPage from '../../../components/NotAuthorizedPage'; import Page from '../../../components/Page'; @@ -8,7 +7,6 @@ import VerticalBar from '../../../components/VerticalBar'; import { usePermission } from '../../../contexts/AuthorizationContext'; import { useRoute, useRouteParameter } from '../../../contexts/RouterContext'; import { useTranslation } from '../../../contexts/TranslationContext'; -import { useEndpointData } from '../../../hooks/useEndpointData'; import AddCustomEmoji from './AddCustomEmoji'; import CustomEmoji from './CustomEmoji'; import EditCustomEmojiWithData from './EditCustomEmojiWithData'; @@ -20,24 +18,6 @@ function CustomEmojiRoute() { const canManageEmoji = usePermission('manage-emoji'); const t = useTranslation(); - - const [params, setParams] = useState(() => ({ text: '', current: 0, itemsPerPage: 25 })); - const [sort, setSort] = useState(() => ['name', 'asc']); - - const { text, itemsPerPage, current } = useDebouncedValue(params, 500); - const [column, direction] = useDebouncedValue(sort, 500); - const query = useMemo( - () => ({ - query: JSON.stringify({ name: { $regex: text || '', $options: 'i' } }), - sort: JSON.stringify({ [column]: direction === 'asc' ? 1 : -1 }), - ...(itemsPerPage && { count: itemsPerPage }), - ...(current && { offset: current }), - }), - [text, itemsPerPage, current, column, direction], - ); - - const { value: data, reload } = useEndpointData('emoji-custom.all', query); - const handleItemClick = (_id) => () => { route.push({ context: 'edit', @@ -45,16 +25,6 @@ function CustomEmojiRoute() { }); }; - const handleHeaderClick = (id) => { - setSort(([sortBy, sortDirection]) => { - if (sortBy === id) { - return [id, sortDirection === 'asc' ? 'desc' : 'asc']; - } - - return [id, 'asc']; - }); - }; - const handleNewButtonClick = useCallback(() => { route.push({ context: 'new' }); }, [route]); @@ -63,8 +33,10 @@ function CustomEmojiRoute() { route.push({}); }; + const reload = useRef(() => null); + const handleChange = useCallback(() => { - reload(); + reload.current(); }, [reload]); if (!canManageEmoji) { @@ -80,14 +52,7 @@ function CustomEmojiRoute() { - + {context && ( diff --git a/client/views/admin/index.ts b/client/views/admin/index.ts index a962afc2ff1c..39ecf34bc30c 100644 --- a/client/views/admin/index.ts +++ b/client/views/admin/index.ts @@ -1,2 +1,5 @@ export { registerAdminRoute } from './routes'; -export { registerAdminSidebarItem } from './sidebarItems'; +export { + registerAdminSidebarItem, + unregisterSidebarItem as unregisterAdminSidebarItem, +} from './sidebarItems'; diff --git a/client/views/admin/routes.js b/client/views/admin/routes.js index 815fed54be02..4d86946c7603 100644 --- a/client/views/admin/routes.js +++ b/client/views/admin/routes.js @@ -26,6 +26,11 @@ registerAdminRoute('/apps/:context?/:id?/:version?', { lazyRouteComponent: () => import('./apps/AppsRoute'), }); +registerAdminRoute('/apps/:context?/:id?/:version?', { + name: 'admin-apps', + lazyRouteComponent: () => import('./apps/AppsRoute'), +}); + registerAdminRoute('/info', { name: 'admin-info', lazyRouteComponent: () => import('./info/InformationRoute'), diff --git a/client/views/admin/settings/groups/LDAPGroupPage.tsx b/client/views/admin/settings/groups/LDAPGroupPage.tsx index 94a4b59273a1..94cc03e0e8ff 100644 --- a/client/views/admin/settings/groups/LDAPGroupPage.tsx +++ b/client/views/admin/settings/groups/LDAPGroupPage.tsx @@ -19,7 +19,6 @@ function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element { const syncNow = useEndpoint('POST', 'ldap.syncNow'); const testSearch = useEndpoint('POST', 'ldap.testSearch'); const ldapEnabled = useSetting('LDAP_Enable'); - const ldapSyncEnabled = useSetting('LDAP_Background_Sync') && ldapEnabled; const setModal = useSetModal(); const closeModal = useMutableCallback(() => setModal()); @@ -39,7 +38,7 @@ function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element { const handleTestConnectionButtonClick = async (): Promise => { try { - const { message } = await testConnection(undefined); + const { message } = await testConnection(); dispatchToastMessage({ type: 'success', message: t(message) }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); @@ -48,12 +47,12 @@ function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element { const handleSyncNowButtonClick = async (): Promise => { try { - await testConnection(undefined); + await testConnection(); const confirmSync = async (): Promise => { closeModal(); try { - const { message } = await syncNow(undefined); + const { message } = await syncNow(); dispatchToastMessage({ type: 'success', message: t(message) }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); @@ -79,7 +78,7 @@ function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element { const handleSearchTestButtonClick = async (): Promise => { try { - await testConnection(undefined); + await testConnection(); let username = ''; const handleChangeUsername = (event: FormEvent): void => { username = event.currentTarget.value; @@ -135,13 +134,11 @@ function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element { disabled={!ldapEnabled || changed} onClick={handleSearchTestButtonClick} /> - {ldapSyncEnabled && ( - diff --git a/client/views/admin/sidebarItems.js b/client/views/admin/sidebarItems.js index f8f7a9d7579d..a9531ca52e44 100644 --- a/client/views/admin/sidebarItems.js +++ b/client/views/admin/sidebarItems.js @@ -62,16 +62,10 @@ export const { i18nLabel: 'Federation Dashboard', permissionGranted: () => hasRole(Meteor.userId(), 'admin'), }, - { - icon: 'cube', - href: 'admin-apps', - i18nLabel: 'Apps', - permissionGranted: () => hasPermission(['manage-apps']), - }, { icon: 'cube', href: 'admin-marketplace', - i18nLabel: 'Marketplace', + i18nLabel: 'Apps', permissionGranted: () => hasPermission(['manage-apps']), }, { diff --git a/client/views/admin/users/EditUser.js b/client/views/admin/users/EditUser.js index bb9195a1236f..978c9dcabd95 100644 --- a/client/views/admin/users/EditUser.js +++ b/client/views/admin/users/EditUser.js @@ -11,7 +11,7 @@ import { useForm } from '../../../hooks/useForm'; import UserForm from './UserForm'; const getInitialValue = (data) => ({ - roles: data.rolesId, + roles: data.roles, name: data.name ?? '', password: '', username: data.username, diff --git a/client/views/admin/users/UserInfo.js b/client/views/admin/users/UserInfo.js index 81cead31e9ca..01ee1762c4b5 100644 --- a/client/views/admin/users/UserInfo.js +++ b/client/views/admin/users/UserInfo.js @@ -6,6 +6,7 @@ import { getUserEmailAddress } from '../../../../lib/getUserEmailAddress'; import { FormSkeleton } from '../../../components/Skeleton'; import UserCard from '../../../components/UserCard'; import { UserStatus } from '../../../components/UserStatus'; +import { useRolesDescription } from '../../../contexts/AuthorizationContext'; import { useSetting } from '../../../contexts/SettingsContext'; import { useTranslation } from '../../../contexts/TranslationContext'; import { AsyncStatePhase } from '../../../hooks/useAsyncState'; @@ -17,6 +18,7 @@ import { UserInfoActions } from './UserInfoActions'; export function UserInfoWithData({ uid, username, onReload, ...props }) { const t = useTranslation(); const showRealNames = useSetting('UI_Use_Real_Name'); + const getRoles = useRolesDescription(); const approveManuallyUsers = useSetting('Accounts_ManuallyApproveNewUsers'); const { @@ -57,7 +59,9 @@ export function UserInfoWithData({ uid, username, onReload, ...props }) { username, lastLogin, showRealNames, - roles: roles.map((role, index) => {role}), + roles: + roles && + getRoles(roles).map((role, index) => {role}), bio, phone: user.phone, utcOffset, @@ -74,7 +78,7 @@ export function UserInfoWithData({ uid, username, onReload, ...props }) { customStatus: statusText, nickname, }; - }, [approveManuallyUsers, data, showRealNames]); + }, [approveManuallyUsers, data, showRealNames, getRoles]); if (state === AsyncStatePhase.LOADING) { return ( diff --git a/client/views/hooks/useDepartmentsByUnitsList.ts b/client/views/hooks/useDepartmentsByUnitsList.ts index e2a1de9352a9..7211afe283fc 100644 --- a/client/views/hooks/useDepartmentsByUnitsList.ts +++ b/client/views/hooks/useDepartmentsByUnitsList.ts @@ -21,9 +21,7 @@ export const useDepartmentsByUnitsList = ( } => { const [itemsList, setItemsList] = useState(() => new RecordList()); const reload = useCallback(() => setItemsList(new RecordList()), []); - const endpoint = `livechat/departments.available-by-unit/${ - options.unitId || 'none' - }` as 'livechat/departments.by-unit/'; + const endpoint = `livechat/departments.available-by-unit/${options.unitId || 'none'}` as const; const getDepartments = useEndpoint('GET', endpoint); diff --git a/client/views/meet/CallPage.tsx b/client/views/meet/CallPage.tsx new file mode 100644 index 000000000000..4f5283c6def5 --- /dev/null +++ b/client/views/meet/CallPage.tsx @@ -0,0 +1,396 @@ +import { Box, Flex, ButtonGroup, Button, Icon } from '@rocket.chat/fuselage'; +import moment from 'moment'; +import React, { FC, useEffect, useState } from 'react'; + +import { Notifications } from '../../../app/notifications/client'; +import { WebRTC } from '../../../app/webrtc/client'; +import { WEB_RTC_EVENTS } from '../../../app/webrtc/index'; +import UserAvatar from '../../components/avatar/UserAvatar'; +import { useTranslation } from '../../contexts/TranslationContext'; +import OngoingCallDuration from './OngoingCallDuration'; +import './styles.css'; + +type CallPageProps = { + roomId: any; + visitorToken: any; + visitorId: any; + status: any; + setStatus: any; + layout: any; + visitorName: any; + agentName: any; + callStartTime: any; +}; + +const CallPage: FC = ({ + roomId, + visitorToken, + visitorId, + status, + setStatus, + layout, + visitorName, + agentName, + callStartTime, +}) => { + const [isAgentActive, setIsAgentActive] = useState(false); + const [isMicOn, setIsMicOn] = useState(false); + const [isCameraOn, setIsCameraOn] = useState(false); + const [isRemoteMobileDevice, setIsRemoteMobileDevice] = useState(false); + const [callInIframe, setCallInIframe] = useState(false); + const [isRemoteCameraOn, setIsRemoteCameraOn] = useState(false); + const [isLocalMobileDevice, setIsLocalMobileDevice] = useState(false); + + let iconSize = 'x21'; + let buttonSize = 'x40'; + const avatarSize = 'x48'; + if (layout === 'embedded') { + iconSize = 'x19'; + buttonSize = 'x35'; + } + + const t = useTranslation(); + useEffect(() => { + if (visitorToken) { + const webrtcInstance = WebRTC.getInstanceByRoomId(roomId, visitorId); + const isMobileDevice = (): boolean => { + if (layout === 'embedded') { + setCallInIframe(true); + } + if (window.innerWidth <= 450 && window.innerHeight >= 629 && window.innerHeight <= 900) { + setIsLocalMobileDevice(true); + webrtcInstance.media = { + audio: true, + video: { + width: { ideal: 440 }, + height: { ideal: 580 }, + }, + }; + return true; + } + return false; + }; + Notifications.onUser( + WEB_RTC_EVENTS.WEB_RTC, + (type: any, data: any) => { + if (data.room == null) { + return; + } + webrtcInstance.onUserStream(type, data); + }, + visitorId, + ); + Notifications.onRoom(roomId, 'webrtc', (type: any, data: any) => { + if (type === 'callStatus' && data.callStatus === 'ended') { + webrtcInstance.stop(); + setStatus(data.callStatus); + } else if (type === 'getDeviceType') { + Notifications.notifyRoom(roomId, 'webrtc', 'deviceType', { + isMobileDevice: isMobileDevice(), + }); + } else if (type === 'cameraStatus') { + setIsRemoteCameraOn(data.isCameraOn); + } + }); + Notifications.notifyRoom(roomId, 'webrtc', 'deviceType', { + isMobileDevice: isMobileDevice(), + }); + Notifications.notifyRoom(roomId, 'webrtc', 'callStatus', { callStatus: 'inProgress' }); + } else if (!isAgentActive) { + const webrtcInstance = WebRTC.getInstanceByRoomId(roomId); + if (status === 'inProgress') { + Notifications.notifyRoom(roomId, 'webrtc', 'getDeviceType'); + webrtcInstance.startCall({ + audio: true, + video: { + width: { ideal: 1920 }, + height: { ideal: 1080 }, + }, + }); + } + Notifications.onRoom(roomId, 'webrtc', (type: any, data: any) => { + if (type === 'callStatus') { + switch (data.callStatus) { + case 'ended': + webrtcInstance.stop(); + break; + case 'inProgress': + webrtcInstance.startCall({ + audio: true, + video: { + width: { ideal: 1920 }, + height: { ideal: 1080 }, + }, + }); + } + setStatus(data.callStatus); + } else if (type === 'deviceType' && data.isMobileDevice) { + setIsRemoteMobileDevice(true); + } else if (type === 'cameraStatus') { + setIsRemoteCameraOn(data.isCameraOn); + } + }); + setIsAgentActive(true); + } + }, [isAgentActive, status, setStatus, visitorId, roomId, visitorToken, layout]); + + const toggleButton = (control: any): any => { + if (control === 'mic') { + WebRTC.getInstanceByRoomId(roomId, visitorToken).toggleAudio(); + return setIsMicOn(!isMicOn); + } + WebRTC.getInstanceByRoomId(roomId, visitorToken).toggleVideo(); + setIsCameraOn(!isCameraOn); + Notifications.notifyRoom(roomId, 'webrtc', 'cameraStatus', { isCameraOn: !isCameraOn }); + }; + + const closeWindow = (): void => { + if (layout === 'embedded') { + return (parent as any)?.handleIframeClose(); + } + return window.close(); + }; + + const getCallDuration = (callStartTime: any): any => + moment.duration(moment(new Date()).diff(moment(callStartTime))).asSeconds(); + + const showCallPage = (localAvatar: any, remoteAvatar: any): any => ( + + + + + + + + + + {layout === 'embedded' && ( + + )} + + + + + + + + + + {remoteAvatar} + + + + + ); + + return ( + <> + {status === 'ringing' && ( + + + + + + + + + {'Calling...'} + + + {visitorName} + + + + + )} + {status === 'declined' && ( + + {t('Call_declined')} + + )} + {status === 'inProgress' && ( + + {visitorToken + ? showCallPage(visitorName, agentName) + : showCallPage(agentName, visitorName)} + + )} + + ); +}; + +export default CallPage; diff --git a/client/views/meet/MeetPage.tsx b/client/views/meet/MeetPage.tsx new file mode 100644 index 000000000000..58d243cbb2d4 --- /dev/null +++ b/client/views/meet/MeetPage.tsx @@ -0,0 +1,159 @@ +import { Button, Box, Icon, Flex } from '@rocket.chat/fuselage'; +import { Meteor } from 'meteor/meteor'; +import React, { useEffect, useState, useCallback, FC } from 'react'; + +import { APIClient } from '../../../app/utils/client'; +import UserAvatar from '../../components/avatar/UserAvatar'; +import { useRouteParameter, useQueryStringParameter } from '../../contexts/RouterContext'; +import NotFoundPage from '../notFound/NotFoundPage'; +import PageLoading from '../root/PageLoading'; +import CallPage from './CallPage'; +import './styles.css'; + +const MeetPage: FC = () => { + const [isRoomMember, setIsRoomMember] = useState(false); + const [status, setStatus] = useState(null); + const [visitorId, setVisitorId] = useState(null); + const roomId = useRouteParameter('rid'); + const visitorToken = useQueryStringParameter('token'); + const layout = useQueryStringParameter('layout'); + const [visitorName, setVisitorName] = useState(''); + const [agentName, setAgentName] = useState(''); + const [callStartTime, setCallStartTime] = useState(undefined); + + const isMobileDevice = (): boolean => window.innerWidth <= 450; + const closeCallTab = (): void => window.close(); + + const setupCallForVisitor = useCallback(async () => { + const room = await APIClient.v1.get(`livechat/room?token=${visitorToken}&rid=${roomId}`); + if (room?.room?.v?.token === visitorToken) { + setVisitorId(room.room.v._id); + setVisitorName(room.room.fname); + room?.room?.responseBy?.username + ? setAgentName(room.room.responseBy.username) + : setAgentName(room.room.servedBy.username); + setStatus(room?.room?.callStatus || 'ended'); + setCallStartTime(room.room.webRtcCallStartTime); + return setIsRoomMember(true); + } + }, [visitorToken, roomId]); + + const setupCallForAgent = useCallback(async () => { + const room = await APIClient.v1.get(`rooms.info?roomId=${roomId}`); + if (room?.room?.servedBy?._id === Meteor.userId()) { + setVisitorName(room.room.fname); + room?.room?.responseBy?.username + ? setAgentName(room.room.responseBy.username) + : setAgentName(room.room.servedBy.username); + setStatus(room?.room?.callStatus || 'ended'); + setCallStartTime(room.room.webRtcCallStartTime); + return setIsRoomMember(true); + } + }, [roomId]); + + useEffect(() => { + if (visitorToken) { + setupCallForVisitor(); + return; + } + setupCallForAgent(); + }, [setupCallForAgent, setupCallForVisitor, visitorToken]); + if (status === null) { + return ; + } + if (!isRoomMember) { + return ; + } + if (status === 'ended') { + return ( + + + + + + + +

    {'Call Ended!'}

    +

    + {visitorToken ? agentName : visitorName} +

    + + + + + + + ); + } + + return ( + + ); +}; + +export default MeetPage; diff --git a/client/views/meet/OngoingCallDuration.tsx b/client/views/meet/OngoingCallDuration.tsx new file mode 100644 index 000000000000..6bcf61036bae --- /dev/null +++ b/client/views/meet/OngoingCallDuration.tsx @@ -0,0 +1,21 @@ +import { Box } from '@rocket.chat/fuselage'; +import React, { FC, useEffect, useState } from 'react'; + +type OngoingCallDurationProps = { + counter: number; +}; + +const OngoingCallDuration: FC = ({ counter: defaultCounter = 0 }) => { + const [counter, setCounter] = useState(defaultCounter); + useEffect(() => { + setTimeout(() => setCounter(counter + 1), 1000); + }, [counter]); + + return ( + + {new Date(counter * 1000).toISOString().substr(11, 8)} + + ); +}; + +export default OngoingCallDuration; diff --git a/client/views/meet/styles.css b/client/views/meet/styles.css new file mode 100644 index 000000000000..799fd59faec8 --- /dev/null +++ b/client/views/meet/styles.css @@ -0,0 +1,41 @@ +.Off { + color: #ffffff !important; + border-color: #2f343d !important; + background-color: #2f343d !important; +} + +.On { + color: #000000 !important; + border-color: #ffffff !important; + background-color: #ffffff !important; +} + +.Self_Video { + display: flex; + + width: 15%; + height: 17.5%; + + justify-content: center; +} + +@media (max-width: 900px) and (min-height: 500px) { + .Self_Video { + width: 30%; + height: 20%; + } +} + +@media (max-width: 900px) and (max-height: 500px) { + .Self_Video { + width: 30%; + height: 35%; + } +} + +@media (min-width: 901px) and (max-width: 1300px) and (max-height: 500px) { + .Self_Video { + width: 20%; + height: 40%; + } +} diff --git a/client/views/notFound/NotFoundPage.js b/client/views/notFound/NotFoundPage.js index 9a564a753053..0aaf3bf64d1a 100644 --- a/client/views/notFound/NotFoundPage.js +++ b/client/views/notFound/NotFoundPage.js @@ -38,11 +38,16 @@ function NotFoundPage() { 404 - + {t('Oops_page_not_found')} - + {t('Sorry_page_you_requested_does_not_exist_or_was_deleted')} diff --git a/client/views/notFound/NotFoundPage.spec.tsx b/client/views/notFound/NotFoundPage.spec.tsx new file mode 100644 index 000000000000..3f1389631580 --- /dev/null +++ b/client/views/notFound/NotFoundPage.spec.tsx @@ -0,0 +1,79 @@ +import { render, waitFor, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { expect, spy } from 'chai'; +import React from 'react'; + +import RouterContextMock from '../../../tests/mocks/client/RouterContextMock'; +import NotFoundPage from './NotFoundPage'; + +describe('views/notFound/NotFoundPage', () => { + it('should look good', async () => { + render(); + + expect(screen.getByRole('heading', { level: 1, name: 'Oops_page_not_found' })).to.exist; + expect( + screen.getByRole('status', { + name: 'Sorry_page_you_requested_does_not_exist_or_was_deleted', + }), + ).to.exist; + expect( + screen.getByRole('button', { name: 'Return_to_previous_page' }), + ).to.exist.and.to.not.match(':disabled'); + expect(screen.getByRole('button', { name: 'Return_to_home' })).to.exist.and.to.not.match( + ':disabled', + ); + }); + + it('should have correct tab order', () => { + render(); + + expect(document.body).to.have.focus; + userEvent.tab(); + expect(screen.getByRole('button', { name: 'Return_to_previous_page' })).to.have.focus; + userEvent.tab(); + expect(screen.getByRole('button', { name: 'Return_to_home' })).to.have.focus; + userEvent.tab(); + expect(document.body).to.have.focus; + }); + + context('"Return to previous page" button', () => { + context('when clicked', () => { + const listener = spy(); + + before(() => { + window.history.pushState('404-page', '', 'http://localhost:3000/404'); + window.addEventListener('popstate', listener); + }); + + after(() => { + window.removeEventListener('popstate', listener); + }); + + it('should go back on history', async () => { + render(); + const button = screen.getByRole('button', { name: 'Return_to_previous_page' }); + + userEvent.click(button); + await waitFor(() => expect(listener).to.have.been.called(), { timeout: 2000 }); + expect(window.history.state).to.not.be.eq('404-page'); + }); + }); + }); + + context('"Return to home" button', () => { + context('when clicked', () => { + it('should go back on history', async () => { + const pushRoute = spy(); + render( + + + , + ); + const button = screen.getByRole('button', { name: 'Return_to_home' }); + + userEvent.click(button); + await waitFor(() => expect(pushRoute).to.have.been.called.with('home')); + }); + }); + }); +}); diff --git a/client/views/omnichannel/businessHours/BusinessHoursRouter.js b/client/views/omnichannel/businessHours/BusinessHoursRouter.js index f895667994eb..c1b788ba4b0d 100644 --- a/client/views/omnichannel/businessHours/BusinessHoursRouter.js +++ b/client/views/omnichannel/businessHours/BusinessHoursRouter.js @@ -21,15 +21,15 @@ const BusinessHoursRouter = () => { const router = useRoute('omnichannel-businessHours'); useEffect(() => { - if (isSingleBH && (context !== 'edit' || type !== 'default')) { + if (isSingleBH) { router.push({ context: 'edit', type: 'default', }); } - }, [context, isSingleBH, router, type]); + }, [isSingleBH, router]); - if ((context === 'edit' && type) || (isSingleBH && (context !== 'edit' || type !== 'default'))) { + if (context === 'edit' || isSingleBH) { return type ? : null; } diff --git a/client/views/omnichannel/currentChats/CurrentChatsPage.tsx b/client/views/omnichannel/currentChats/CurrentChatsPage.tsx index e5df137c3c20..fde9fec1bcbe 100644 --- a/client/views/omnichannel/currentChats/CurrentChatsPage.tsx +++ b/client/views/omnichannel/currentChats/CurrentChatsPage.tsx @@ -44,11 +44,10 @@ const CurrentChatsPage: FC<{ renderRow={renderRow} results={data && data.rooms} total={data && data.total} - setParams={setParams} params={params} reload={reload} renderFilter={({ onChange, ...props }: any): any => ( - + )} /> diff --git a/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx b/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx index 7e766e4c00f4..4bf9e123be84 100644 --- a/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx +++ b/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx @@ -47,6 +47,7 @@ const useQuery: useQueryType = ( departmentId?: string; tags?: string[]; customFields?: string; + onhold?: boolean; } = { ...(guest && { roomName: guest }), sort: JSON.stringify({ @@ -71,8 +72,10 @@ const useQuery: useQueryType = ( }), }); } + if (status !== 'all') { - query.open = status === 'opened'; + query.open = status === 'opened' || status === 'onhold'; + query.onhold = status === 'onhold'; } if (servedBy && servedBy !== 'all') { query.agents = [servedBy]; @@ -215,25 +218,32 @@ const CurrentChatsRoute: FC = () => { ); const renderRow = useCallback( - ({ _id, fname, servedBy, ts, lm, department, open }) => ( - onRowClick(_id)} - action - qa-user-id={_id} - > - {fname} - {department ? department.name : ''} - {servedBy && servedBy.username} - {moment(ts).format('L LTS')} - {moment(lm).format('L LTS')} - {open ? t('Open') : t('Closed')} - {canRemoveClosedChats && !open && } - - ), - [onRowClick, reload, t, canRemoveClosedChats], + ({ _id, fname, servedBy, ts, lm, department, open, onHold }) => { + const getStatusText = (open: boolean, onHold: boolean): string => { + if (!open) return t('Closed'); + return onHold ? t('On_Hold_Chats') : t('Open'); + }; + + return ( + onRowClick(_id)} + action + qa-user-id={_id} + > + {fname} + {department ? department.name : ''} + {servedBy && servedBy.username} + {moment(ts).format('L LTS')} + {moment(lm).format('L LTS')} + {getStatusText(open, onHold)} + {canRemoveClosedChats && !open && } + + ); + }, + [onRowClick, reload, canRemoveClosedChats, t], ); if (!canViewCurrentChats) { diff --git a/client/views/omnichannel/currentChats/FilterByText.tsx b/client/views/omnichannel/currentChats/FilterByText.tsx index 6a2445da147d..1f4eebc35dfa 100644 --- a/client/views/omnichannel/currentChats/FilterByText.tsx +++ b/client/views/omnichannel/currentChats/FilterByText.tsx @@ -18,10 +18,11 @@ import RemoveAllClosed from './RemoveAllClosed'; type FilterByTextType = FC<{ setFilter: Dispatch>; + setParams: Dispatch>; reload?: () => void; }>; -const FilterByText: FilterByTextType = ({ setFilter, reload, ...props }) => { +const FilterByText: FilterByTextType = ({ setFilter, setParams, reload, ...props }) => { const setModal = useSetModal(); const dispatchToastMessage = useToastMessageDispatch(); const t = useTranslation(); @@ -31,6 +32,7 @@ const FilterByText: FilterByTextType = ({ setFilter, reload, ...props }) => { ['all', t('All')], ['closed', t('Closed')], ['opened', t('Open')], + ['onhold', t('On_Hold_Chats')], ]; const customFieldsOptions: [string, string][] = useMemo( () => @@ -95,7 +97,17 @@ const FilterByText: FilterByTextType = ({ setFilter, reload, ...props }) => { tags: tags.map((tag) => tag.label), customFields: customFields.reduce(reducer, {}), }); - }, [setFilter, guest, servedBy, status, department, from, to, tags, customFields]); + setParams({ + guest, + servedBy, + status, + ...(department?.value && department.value !== 'all' && { department: department.value }), + from: from && moment(new Date(from)).utc().format('YYYY-MM-DDTHH:mm:ss'), + to: to && moment(new Date(to)).utc().format('YYYY-MM-DDTHH:mm:ss'), + tags: tags.map((tag) => tag.label), + customFields: customFields.reduce(reducer, {}), + }); + }, [setFilter, guest, servedBy, status, department, from, to, tags, customFields, setParams]); const handleClearFilters = useMutableCallback(() => { reset(); @@ -110,7 +122,7 @@ const FilterByText: FilterByTextType = ({ setFilter, reload, ...props }) => { reload && reload(); dispatchToastMessage({ type: 'success', message: t('Chat_removed') }); } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); + dispatchToastMessage({ type: 'error', message: (error as Error).message }); } setModal(null); }; diff --git a/client/views/omnichannel/departments/EditDepartment.js b/client/views/omnichannel/departments/EditDepartment.js index 24885d0f53a6..3fbb653e2569 100644 --- a/client/views/omnichannel/departments/EditDepartment.js +++ b/client/views/omnichannel/departments/EditDepartment.js @@ -1,4 +1,3 @@ -/* eslint-disable complexity */ import { FieldGroup, Field, @@ -33,11 +32,7 @@ import DepartmentsAgentsTable from './DepartmentsAgentsTable'; function EditDepartment({ data, id, title, reload, allowedToForwardData }) { const t = useTranslation(); - const agentsRoute = useRoute('omnichannel-departments'); - const eeForms = useSubscription(formsSubscription); - const initialAgents = useRef((data && data.agents) || []); - - const router = useRoute('omnichannel-departments'); + const departmentsRoute = useRoute('omnichannel-departments'); const { useEeNumberInput = () => {}, @@ -45,7 +40,9 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { useEeTextAreaInput = () => {}, useDepartmentForwarding = () => {}, useDepartmentBusinessHours = () => {}, - } = eeForms; + } = useSubscription(formsSubscription); + + const initialAgents = useRef((data && data.agents) || []); const MaxChats = useEeNumberInput(); const VisitorInactivity = useEeNumberInput(); @@ -57,29 +54,23 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { const { department } = data || { department: {} }; - const [tags, setTags] = useState((department && department.chatClosingTags) || []); - const [tagsText, setTagsText] = useState(); + const [[tags, tagsText], setTagsState] = useState(() => [department?.chatClosingTags ?? [], '']); const { values, handlers, hasUnsavedChanges } = useForm({ - name: (department && department.name) || '', - email: (department && department.email) || '', - description: (department && department.description) || '', - enabled: !!(department && department.enabled), - maxNumberSimultaneousChat: (department && department.maxNumberSimultaneousChat) || undefined, - showOnRegistration: !!(department && department.showOnRegistration), - showOnOfflineForm: !!(department && department.showOnOfflineForm), - abandonedRoomsCloseCustomMessage: - (department && department.abandonedRoomsCloseCustomMessage) || '', - requestTagBeforeClosingChat: (department && department.requestTagBeforeClosingChat) || false, - offlineMessageChannelName: (department && department.offlineMessageChannelName) || '', - visitorInactivityTimeoutInSeconds: - (department && department.visitorInactivityTimeoutInSeconds) || undefined, - waitingQueueMessage: (department && department.waitingQueueMessage) || '', + name: department?.name || '', + email: department?.email || '', + description: department?.description || '', + enabled: !!department?.enabled, + maxNumberSimultaneousChat: department?.maxNumberSimultaneousChat || undefined, + showOnRegistration: !!department?.showOnRegistration, + showOnOfflineForm: !!department?.showOnOfflineForm, + abandonedRoomsCloseCustomMessage: department?.abandonedRoomsCloseCustomMessage || '', + requestTagBeforeClosingChat: department?.requestTagBeforeClosingChat || false, + offlineMessageChannelName: department?.offlineMessageChannelName || '', + visitorInactivityTimeoutInSeconds: department?.visitorInactivityTimeoutInSeconds || undefined, + waitingQueueMessage: department?.waitingQueueMessage || '', departmentsAllowedToForward: - (allowedToForwardData && - allowedToForwardData.departments && - allowedToForwardData.departments.map((dep) => ({ label: dep.name, value: dep._id }))) || - [], + allowedToForwardData?.departments?.map((dep) => ({ label: dep.name, value: dep._id })) || [], }); const { handleName, @@ -119,20 +110,25 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { const { phase: roomsPhase, items: roomsItems, itemCount: roomsTotal } = useRecordList(RoomsList); const handleTagChipClick = (tag) => () => { - setTags((tags) => tags.filter((_tag) => _tag !== tag)); + setTagsState(([tags, tagsText]) => [tags.filter((_tag) => _tag !== tag), tagsText]); }; const handleTagTextSubmit = useMutableCallback(() => { - if (!tags.includes(tagsText)) { - setTags([...tags, tagsText]); - setTagsText(''); - } - }); + setTagsState((state) => { + const [tags, tagsText] = state; + + if (tags.includes(tagsText)) { + return state; + } - const handleTagTextChange = useMutableCallback((e) => { - setTagsText(e.target.value); + return [[...tags, tagsText], '']; + }); }); + const handleTagTextChange = (e) => { + setTagsState(([tags]) => [tags, e.target.value]); + }; + const saveDepartmentInfo = useMethod('livechat:saveDepartment'); const saveDepartmentAgentsInfoOnEdit = useEndpoint('POST', `livechat/department/${id}/agents`); @@ -197,8 +193,7 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { visitorInactivityTimeoutInSeconds, abandonedRoomsCloseCustomMessage, waitingQueueMessage, - departmentsAllowedToForward: - departmentsAllowedToForward && departmentsAllowedToForward.map((dep) => dep.value).join(), + departmentsAllowedToForward: departmentsAllowedToForward?.map((dep) => dep.value).join(), }; const agentListPayload = { @@ -227,14 +222,14 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { } dispatchToastMessage({ type: 'success', message: t('Saved') }); reload(); - agentsRoute.push({}); + departmentsRoute.push({}); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } }); const handleReturn = useMutableCallback(() => { - router.push({}); + departmentsRoute.push({}); }); const invalidForm = @@ -438,7 +433,7 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { {t('Conversation_closing_tags_description')} - {tags && tags.length > 0 && ( + {tags?.length > 0 && ( {tags.map((tag, i) => ( @@ -451,7 +446,7 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { )} {DepartmentBusinessHours && ( - + )} diff --git a/client/views/omnichannel/departments/EditDepartmentWithAllowedForwardData.js b/client/views/omnichannel/departments/EditDepartmentWithAllowedForwardData.js index ff9c26f4ef57..a217c4ec8aaa 100644 --- a/client/views/omnichannel/departments/EditDepartmentWithAllowedForwardData.js +++ b/client/views/omnichannel/departments/EditDepartmentWithAllowedForwardData.js @@ -1,4 +1,3 @@ -/* eslint-disable complexity */ import { Box } from '@rocket.chat/fuselage'; import React, { useMemo } from 'react'; diff --git a/client/views/omnichannel/departments/EditDepartmentWithData.js b/client/views/omnichannel/departments/EditDepartmentWithData.js index ccdc6ab6e589..d4bbc31c35ca 100644 --- a/client/views/omnichannel/departments/EditDepartmentWithData.js +++ b/client/views/omnichannel/departments/EditDepartmentWithData.js @@ -1,4 +1,3 @@ -/* eslint-disable complexity */ import { Box } from '@rocket.chat/fuselage'; import React from 'react'; diff --git a/client/views/omnichannel/directory/OmnichannelDirectoryPage.js b/client/views/omnichannel/directory/OmnichannelDirectoryPage.js index 4f19bbf53901..3a0cec01e44a 100644 --- a/client/views/omnichannel/directory/OmnichannelDirectoryPage.js +++ b/client/views/omnichannel/directory/OmnichannelDirectoryPage.js @@ -1,7 +1,9 @@ import { Tabs } from '@rocket.chat/fuselage'; import React, { useEffect, useCallback, useState } from 'react'; +import NotAuthorizedPage from '../../../components/NotAuthorizedPage'; import Page from '../../../components/Page'; +import { usePermission } from '../../../contexts/AuthorizationContext'; import { useCurrentRoute, useRoute, useRouteParameter } from '../../../contexts/RouterContext'; import { useTranslation } from '../../../contexts/TranslationContext'; import ContextualBar from './ContextualBar'; @@ -14,6 +16,7 @@ const OmnichannelDirectoryPage = () => { const [routeName] = useCurrentRoute(); const tab = useRouteParameter('page'); const directoryRoute = useRoute('omnichannel-directory'); + const canViewDirectory = usePermission('view-omnichannel-contact-center'); useEffect(() => { if (routeName !== 'omnichannel-directory') { @@ -32,6 +35,10 @@ const OmnichannelDirectoryPage = () => { const t = useTranslation(); + if (!canViewDirectory) { + return ; + } + return ( diff --git a/client/views/omnichannel/realTimeMonitoring/charts/ChatsChart.js b/client/views/omnichannel/realTimeMonitoring/charts/ChatsChart.js index 7fee07da730f..38eba111c21a 100644 --- a/client/views/omnichannel/realTimeMonitoring/charts/ChatsChart.js +++ b/client/views/omnichannel/realTimeMonitoring/charts/ChatsChart.js @@ -7,11 +7,12 @@ import { useEndpointData } from '../../../../hooks/useEndpointData'; import Chart from './Chart'; import { useUpdateChartData } from './useUpdateChartData'; -const labels = ['Open', 'Queued', 'Closed']; +const labels = ['Open', 'Queued', 'On_Hold_Chats', 'Closed']; const initialData = { open: 0, queued: 0, + onhold: 0, closed: 0, }; @@ -45,7 +46,7 @@ const ChatsChart = ({ params, reloadRef, ...props }) => { reloadRef.current.chatsChart = reload; - const { open, queued, closed } = data ?? initialData; + const { open, queued, closed, onhold } = data ?? initialData; useEffect(() => { const initChart = async () => { @@ -58,9 +59,10 @@ const ChatsChart = ({ params, reloadRef, ...props }) => { if (state === AsyncStatePhase.RESOLVED) { updateChartData(t('Open'), [open]); updateChartData(t('Closed'), [closed]); + updateChartData(t('On_Hold_Chats'), [onhold]); updateChartData(t('Queued'), [queued]); } - }, [closed, open, queued, state, t, updateChartData]); + }, [closed, open, queued, onhold, state, t, updateChartData]); return ; }; diff --git a/client/views/omnichannel/realTimeMonitoring/charts/ChatsPerAgentChart.js b/client/views/omnichannel/realTimeMonitoring/charts/ChatsPerAgentChart.js index 1092c7646b24..07fd9ba6c94b 100644 --- a/client/views/omnichannel/realTimeMonitoring/charts/ChatsPerAgentChart.js +++ b/client/views/omnichannel/realTimeMonitoring/charts/ChatsPerAgentChart.js @@ -12,7 +12,7 @@ const initialData = { }; const init = (canvas, context, t) => - drawLineChart(canvas, context, [t('Open'), t('Closed')], [], [[], []], { + drawLineChart(canvas, context, [t('Open'), t('Closed'), t('On_Hold_Chats')], [], [[], []], { legends: true, anim: true, smallTicks: true, @@ -53,7 +53,7 @@ const ChatsPerAgentChart = ({ params, reloadRef, ...props }) => { if (chartData && chartData.success) { delete chartData.success; Object.entries(chartData).forEach(([name, value]) => { - updateChartData(name, [value.open, value.closed]); + updateChartData(name, [value.open, value.closed, value.onhold]); }); } } diff --git a/client/views/room/contexts/RoomContext.ts b/client/views/room/contexts/RoomContext.ts index 624cc4b98166..bea6ae8af3df 100644 --- a/client/views/room/contexts/RoomContext.ts +++ b/client/views/room/contexts/RoomContext.ts @@ -5,6 +5,7 @@ import { IRoom, IOmnichannelRoom, isOmnichannelRoom } from '../../../../definiti export type RoomContextValue = { rid: IRoom['_id']; room: IRoom; + subscribed: boolean; }; export const RoomContext = createContext(null); diff --git a/client/views/room/contextualBar/ExportMessages/MailExportForm.js b/client/views/room/contextualBar/ExportMessages/MailExportForm.js index 35fd205d982c..218a841b47f0 100644 --- a/client/views/room/contextualBar/ExportMessages/MailExportForm.js +++ b/client/views/room/contextualBar/ExportMessages/MailExportForm.js @@ -111,7 +111,7 @@ const MailExportForm = ({ onCancel, rid }) => { await roomsExport({ rid, type: 'email', - toUsers: [toUsers], + toUsers, toEmails: additionalEmails.split(','), subject, messages: selectedMessages, diff --git a/client/views/room/providers/RoomProvider.tsx b/client/views/room/providers/RoomProvider.tsx index ada05e29c237..8630ecc4741e 100644 --- a/client/views/room/providers/RoomProvider.tsx +++ b/client/views/room/providers/RoomProvider.tsx @@ -1,7 +1,9 @@ import React, { ReactNode, useMemo, memo, useEffect } from 'react'; +import { UserAction } from '../../../../app/ui'; import { roomTypes } from '../../../../app/utils/client'; import { IRoom } from '../../../../definition/IRoom'; +import { useUserSubscription } from '../../../contexts/UserContext'; import { RoomManager, useHandleRoom } from '../../../lib/RoomManager'; import { AsyncStatePhase } from '../../../lib/asyncState'; import Skeleton from '../Room/Skeleton'; @@ -13,18 +15,23 @@ export type Props = { rid: IRoom['_id']; }; +const fields = {}; + const RoomProvider = ({ rid, children }: Props): JSX.Element => { const { phase, value: room } = useHandleRoom(rid); + + const subscribed = Boolean(useUserSubscription(rid, fields)); const context = useMemo(() => { if (!room) { return null; } room._id = rid; return { + subscribed, rid, room: { ...room, name: roomTypes.getRoomName(room.t, room) }, }; - }, [room, rid]); + }, [room, rid, subscribed]); useEffect(() => { RoomManager.open(rid); @@ -33,6 +40,17 @@ const RoomProvider = ({ rid, children }: Props): JSX.Element => { }; }, [rid]); + useEffect(() => { + if (!subscribed) { + return (): void => undefined; + } + + UserAction.addStream(rid); + return (): void => { + UserAction.cancel(rid); + }; + }, [rid, subscribed]); + if (phase === AsyncStatePhase.LOADING || !room) { return ; } diff --git a/client/views/root/AppRoot.tsx b/client/views/root/AppRoot.tsx index 155453cddbee..3aa3453203a5 100644 --- a/client/views/root/AppRoot.tsx +++ b/client/views/root/AppRoot.tsx @@ -1,5 +1,7 @@ import React, { FC, lazy, Suspense } from 'react'; +import { QueryClientProvider } from 'react-query'; +import { queryClient } from '../../lib/queryClient'; import PageLoading from './PageLoading'; const ConnectionStatusBar = lazy( @@ -13,10 +15,12 @@ const PortalsWrapper = lazy(() => import('./PortalsWrapper')); const AppRoot: FC = () => ( }> - - - - + + + + + + ); diff --git a/client/views/setupWizard/steps/AdminUserInformationStep.js b/client/views/setupWizard/steps/AdminUserInformationStep.js index ce14cfe7f609..dffa56414b64 100644 --- a/client/views/setupWizard/steps/AdminUserInformationStep.js +++ b/client/views/setupWizard/steps/AdminUserInformationStep.js @@ -10,13 +10,13 @@ import { import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; import React, { useMemo, useState, useEffect } from 'react'; +import { callbacks } from '../../../../app/callbacks/lib/callbacks'; import { useMethod } from '../../../contexts/ServerContext'; import { useSessionDispatch } from '../../../contexts/SessionContext'; import { useSetting } from '../../../contexts/SettingsContext'; import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext'; import { useTranslation } from '../../../contexts/TranslationContext'; import { useLoginWithPassword } from '../../../contexts/UserContext'; -import { useCallbacks } from '../../../hooks/useCallbacks'; import { Pager } from '../Pager'; import { Step } from '../Step'; import { StepHeader } from '../StepHeader'; @@ -27,7 +27,6 @@ function AdminUserInformationStep({ step, title, active }) { const defineUsername = useMethod('setUsername'); const setForceLogin = useSessionDispatch('forceLogin'); - const callbacks = useCallbacks(); const dispatchToastMessage = useToastMessageDispatch(); const registerAdminUser = async ({ diff --git a/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts b/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts index 6f144e0400a8..b6b01f96953c 100644 --- a/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts +++ b/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts @@ -41,13 +41,17 @@ export const useTeamsChannelList = ( }); return { - items: rooms.map(({ _updatedAt, lastMessage, lm, jitsiTimeout, ...room }) => ({ - jitsiTimeout: new Date(jitsiTimeout), - ...(lm && { lm: new Date(lm) }), - _updatedAt: new Date(_updatedAt), - ...(lastMessage && { lastMessage: mapMessageFromApi(lastMessage) }), - ...room, - })), + items: rooms.map( + ({ _updatedAt, lastMessage, lm, ts, jitsiTimeout, webRtcCallStartTime, ...room }) => ({ + jitsiTimeout: new Date(jitsiTimeout), + ...(lm && { lm: new Date(lm) }), + ...(ts && { ts: new Date(ts) }), + _updatedAt: new Date(_updatedAt), + ...(lastMessage && { lastMessage: mapMessageFromApi(lastMessage) }), + ...(webRtcCallStartTime && { webRtcCallStartTime: new Date(webRtcCallStartTime) }), + ...room, + }), + ), itemCount: total, }; }, diff --git a/definition/Federation.ts b/definition/Federation.ts new file mode 100644 index 000000000000..d961d8226429 --- /dev/null +++ b/definition/Federation.ts @@ -0,0 +1,5 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IFederationServer extends IRocketChatRecord { + domain: string; +} diff --git a/definition/IAnalytic.ts b/definition/IAnalytic.ts new file mode 100644 index 000000000000..2a9b1e83c2e9 --- /dev/null +++ b/definition/IAnalytic.ts @@ -0,0 +1,33 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +interface IAnalyticsBase extends IRocketChatRecord { + type: 'messages' | 'users' | 'seat-request'; + date: number; +} + +export interface IAnalyticsMessages extends IAnalyticsBase { + type: 'messages'; + room: { + _id: string; + name?: string; + t: string; + usernames: string[]; + }; +} + +export interface IAnalyticsUsers extends IAnalyticsBase { + type: 'users'; + room: { + _id: string; + name?: string; + t: string; + usernames: string[]; + }; +} + +export interface IAnalyticsSeatRequest extends IAnalyticsBase { + type: 'seat-request'; + count: number; +} + +export type IAnalytic = IAnalyticsBase | IAnalyticsMessages | IAnalyticsUsers | IAnalyticsSeatRequest; diff --git a/definition/IAvatar.ts b/definition/IAvatar.ts new file mode 100644 index 000000000000..b8bd8b82bfbd --- /dev/null +++ b/definition/IAvatar.ts @@ -0,0 +1,13 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IAvatar extends IRocketChatRecord { + name: string; + rid: string; + userId: string; + store: string; + complete: boolean; + uploading: boolean; + progress: number; + extension: string; + uploadedAt: Date; +} diff --git a/definition/ICredentialToken.ts b/definition/ICredentialToken.ts new file mode 100644 index 000000000000..ceaf44203cba --- /dev/null +++ b/definition/ICredentialToken.ts @@ -0,0 +1,10 @@ +export interface ICredentialToken { + _id: string; + + userInfo: { + username?: string; + attributes?: any; + profile?: Record; + }; + expireAt: Date; +} diff --git a/definition/ICronJobs.ts b/definition/ICronJobs.ts new file mode 100644 index 000000000000..cbd83ecbe074 --- /dev/null +++ b/definition/ICronJobs.ts @@ -0,0 +1,7 @@ +export type ScheduleType = 'cron' | 'text'; + +export interface ICronJobs { + add(name: string, schedule: string, callback: Function, scheduleType?: ScheduleType): void; + remove(name: string): void; + nextScheduledAtDate(name: string): Date | number | undefined; +} diff --git a/definition/ICustomSound.ts b/definition/ICustomSound.ts new file mode 100644 index 000000000000..0fd8c09e0851 --- /dev/null +++ b/definition/ICustomSound.ts @@ -0,0 +1,6 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface ICustomSound extends IRocketChatRecord { + name: string; + statusType: string; +} diff --git a/definition/ICustomUserStatus.ts b/definition/ICustomUserStatus.ts new file mode 100644 index 000000000000..56bb8874be3f --- /dev/null +++ b/definition/ICustomUserStatus.ts @@ -0,0 +1,6 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface ICustomUserStatus extends IRocketChatRecord { + name: string; + statusType: string; +} diff --git a/definition/IEmojiCustom.ts b/definition/IEmojiCustom.ts new file mode 100644 index 000000000000..a7d874f834ea --- /dev/null +++ b/definition/IEmojiCustom.ts @@ -0,0 +1,7 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IEmojiCustom extends IRocketChatRecord { + name: string; + aliases: string; + extension: string; +} diff --git a/definition/IExportOperation.ts b/definition/IExportOperation.ts new file mode 100644 index 000000000000..9ccd591c4823 --- /dev/null +++ b/definition/IExportOperation.ts @@ -0,0 +1,16 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IExportOperation extends IRocketChatRecord { + roomList?: string[]; + status: string; + fileList: string[]; + generatedFile?: string; + fileId: string; + userNameTable: string; + userData: string; + generatedUserFile: string; + generatedAvatar: string; + exportPath: string; + assetsPath: string; + createdAt: Date; +} diff --git a/definition/IInquiry.ts b/definition/IInquiry.ts index 8cc531ef1f49..d4d259467ae7 100644 --- a/definition/IInquiry.ts +++ b/definition/IInquiry.ts @@ -1,5 +1,35 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + export interface IInquiry { _id: string; _updatedAt?: Date; department?: string; } + +export enum LivechatInquiryStatus { + QUEUED = 'queued', + TAKEN = 'taken', + READY = 'ready' +} + +export interface IVisitor { + _id: string; + username: string; + token: string; + status: string; +} + +export interface ILivechatInquiryRecord extends IRocketChatRecord { + rid: string; + name: string; + ts: Date; + message: string; + status: LivechatInquiryStatus; + v: IVisitor; + t: 'l'; + queueOrder: number; + estimatedWaitingTimeQueue: number; + estimatedServiceTimeAt: string; + department: string; + estimatedInactivityCloseTimeAt: Date; +} diff --git a/definition/IIntegration.ts b/definition/IIntegration.ts index 5ad55faab937..fa851e5a1208 100644 --- a/definition/IIntegration.ts +++ b/definition/IIntegration.ts @@ -1,5 +1,9 @@ -export interface IIntegration { - _id: string; +import { IRocketChatRecord } from './IRocketChatRecord'; +import { IUser } from './IUser'; + +export interface IIntegration extends IRocketChatRecord { type: string; enabled: boolean; + channel: string; + _createdBy: IUser; } diff --git a/definition/IIntegrationHistory.ts b/definition/IIntegrationHistory.ts index 2149aff521be..04e1df6cbb03 100644 --- a/definition/IIntegrationHistory.ts +++ b/definition/IIntegrationHistory.ts @@ -1,5 +1,6 @@ -export interface IIntegrationHistory { - _id: string; +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IIntegrationHistory extends IRocketChatRecord { type: string; step: string; integration: { @@ -8,7 +9,20 @@ export interface IIntegrationHistory { event: string; _createdAt: Date; _updatedAt: Date; - // "data" : + data?: { + user?: any; + room?: any; + }; ranPrepareScript: boolean; finished: boolean; + + triggerWord?: string; + prepareSentMessage?: string; + processSentMessage?: string; + url?: string; + httpCallData?: string; + httpError?: any; + httpResult?: string; + error?: any; + errorStack?: any; } diff --git a/definition/IInvite.ts b/definition/IInvite.ts new file mode 100644 index 000000000000..b02a02576257 --- /dev/null +++ b/definition/IInvite.ts @@ -0,0 +1,11 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IInvite extends IRocketChatRecord { + days: number; + maxUses: number; + rid: string; + userId: string; + createdAt: Date; + expires: Date; + uses: number; +} diff --git a/definition/ILivechatAgentActivity.ts b/definition/ILivechatAgentActivity.ts new file mode 100644 index 000000000000..29abc17c529e --- /dev/null +++ b/definition/ILivechatAgentActivity.ts @@ -0,0 +1,15 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface ILivechatAgentActivity extends IRocketChatRecord { + agentId: string; + date: number; + lastStartedAt: Date; + availableTime: number; + serviceHistory: IServiceHistory[]; + lastStoppedAt?: Date; +} + +export interface IServiceHistory { + startedAt: Date; + stoppedAt: Date; +} diff --git a/definition/ILivechatBusinessHour.ts b/definition/ILivechatBusinessHour.ts index cdb18c9fd4be..ab2ec4f70315 100644 --- a/definition/ILivechatBusinessHour.ts +++ b/definition/ILivechatBusinessHour.ts @@ -21,6 +21,7 @@ export interface IBusinessHourWorkHour { start: IBusinessHourTime; finish: IBusinessHourTime; open: boolean; + code: unknown; } export interface IBusinessHourTimezone { diff --git a/definition/ILivechatCustomField.ts b/definition/ILivechatCustomField.ts new file mode 100644 index 000000000000..b3ba5c619720 --- /dev/null +++ b/definition/ILivechatCustomField.ts @@ -0,0 +1,13 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface ILivechatCustomField extends IRocketChatRecord { + label: string; + scope: 'visitor' | 'room'; + visibility: string; + type?: string; + regexp?: string; + required?: boolean; + defaultValue?: string; + options?: string; + public?: boolean; +} diff --git a/definition/ILivechatDepartment.ts b/definition/ILivechatDepartment.ts index 22c352007e8b..eb74209c6088 100644 --- a/definition/ILivechatDepartment.ts +++ b/definition/ILivechatDepartment.ts @@ -2,14 +2,16 @@ export interface ILivechatDepartment { _id: string; name: string; enabled: boolean; - description: string; + description?: string; showOnRegistration: boolean; showOnOfflineForm: boolean; - requestTagBeforeClosingChat: boolean; + requestTagBeforeClosingChat?: boolean; email: string; - chatClosingTags: string[]; + chatClosingTags?: string[]; offlineMessageChannelName: string; numAgents: number; _updatedAt?: Date; businessHourId?: string; + // extra optional fields + [k: string]: any; } diff --git a/definition/ILivechatDepartmentAgents.ts b/definition/ILivechatDepartmentAgents.ts index 3c221e1325f8..e33c80ff9245 100644 --- a/definition/ILivechatDepartmentAgents.ts +++ b/definition/ILivechatDepartmentAgents.ts @@ -4,4 +4,6 @@ export interface ILivechatDepartmentAgents { departmentEnabled: boolean; agentId: string; username: string; + count: number; + order: number; } diff --git a/definition/ILivechatDepartmentRecord.ts b/definition/ILivechatDepartmentRecord.ts index 9b2ff1d5100b..028bec46e543 100644 --- a/definition/ILivechatDepartmentRecord.ts +++ b/definition/ILivechatDepartmentRecord.ts @@ -5,13 +5,15 @@ export interface ILivechatDepartmentRecord extends IRocketChatRecord { _id: string; name: string; enabled: boolean; - description: string; + description?: string; showOnRegistration: boolean; showOnOfflineForm: boolean; - requestTagBeforeClosingChat: boolean; + requestTagBeforeClosingChat?: boolean; email: string; - chatClosingTags: string[]; + chatClosingTags?: string[]; offlineMessageChannelName: string; numAgents: number; businessHourId?: string; + // extra optional fields + [k: string]: any; } diff --git a/definition/ILivechatTrigger.ts b/definition/ILivechatTrigger.ts new file mode 100644 index 000000000000..d577bcfee4f4 --- /dev/null +++ b/definition/ILivechatTrigger.ts @@ -0,0 +1,30 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export enum ILivechatTriggerType { + TIME_ON_SITE = 'time-on-site', + PAGE_URL = 'page-url', + CHAT_OPENED_BY_VISITOR = 'chat-opened-by-visitor' +} + +export interface ILivechatTriggerCondition { + name: ILivechatTriggerType; + value?: string | number; +} + +export interface ILivechatTriggerAction { + name: 'send-message'; + params?: { + sender: 'queue' | 'custom'; + msg: string; + name: string; + }; +} + +export interface ILivechatTrigger extends IRocketChatRecord { + name: string; + description: string; + enabled: boolean; + runOnce: boolean; + conditions: ILivechatTriggerCondition[]; + actions: ILivechatTriggerAction[]; +} diff --git a/definition/ILivechatVisitor.ts b/definition/ILivechatVisitor.ts new file mode 100644 index 000000000000..d3552a627c1e --- /dev/null +++ b/definition/ILivechatVisitor.ts @@ -0,0 +1,35 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IVisitorPhone { + phoneNumber: string; +} + +export interface IVisitorLastChat { + _id: string; + ts: string; +} + +export interface ILivechatVisitorConnectionData { + httpHeaders: { + [k: string]: string; + }; + clientAddress: string; +} + +export interface IVisitorEmail { + address: string; +} + +export interface ILivechatVisitor extends IRocketChatRecord { + username: string; + ts: Date; + token: string; + department?: string; + name?: string; + phone?: (IVisitorPhone)[] | null; + lastChat?: IVisitorLastChat; + userAgent?: string; + ip?: string; + host?: string; + visitorEmails?: IVisitorEmail[]; +} diff --git a/definition/IMessage/IMessage.ts b/definition/IMessage/IMessage.ts index 55f46c283868..2bfbace17dc7 100644 --- a/definition/IMessage/IMessage.ts +++ b/definition/IMessage/IMessage.ts @@ -1,11 +1,11 @@ import { MessageSurfaceLayout } from '@rocket.chat/ui-kit'; import { parser } from '@rocket.chat/message-parser'; -import { IRocketChatRecord } from '../IRocketChatRecord'; -import { IUser } from '../IUser'; -import { ChannelName, RoomID } from '../IRoom'; -import { MessageAttachment } from './MessageAttachment/MessageAttachment'; -import { FileProp } from './MessageAttachment/Files/FileProp'; +import type { IRocketChatRecord } from '../IRocketChatRecord'; +import type { IUser } from '../IUser'; +import type { ChannelName, RoomID } from '../IRoom'; +import type { MessageAttachment } from './MessageAttachment/MessageAttachment'; +import type { FileProp } from './MessageAttachment/Files/FileProp'; type MentionType = 'user' | 'team'; @@ -48,6 +48,7 @@ export interface IMessage extends IRocketChatRecord { channels?: Array; u: Pick; blocks?: MessageSurfaceLayout; + alias?: string; md?: ReturnType; _hidden?: boolean; @@ -73,3 +74,13 @@ export interface IMessage extends IRocketChatRecord { files?: FileProp[]; attachments?: MessageAttachment[]; } + +export type IMessageInbox = IMessage & { + // email inbox fields + email?: { + references?: string[]; + messageId?: string; + }; +} + +export const isIMessageInbox = (message: IMessage): message is IMessageInbox => 'email' in message; diff --git a/definition/IOAuthApps.ts b/definition/IOAuthApps.ts new file mode 100644 index 000000000000..71e004a067bd --- /dev/null +++ b/definition/IOAuthApps.ts @@ -0,0 +1,14 @@ +export interface IOAuthApps { + _id: string; + + name: string; + active: boolean; + clientId: string; + clientSecret: string; + redirectUri: string; + _createdAt: Date; + _createdBy: { + _id: string; + username: string; + }; +} diff --git a/definition/IOEmbedCache.ts b/definition/IOEmbedCache.ts new file mode 100644 index 000000000000..b99341bddf63 --- /dev/null +++ b/definition/IOEmbedCache.ts @@ -0,0 +1,6 @@ +export interface IOEmbedCache { + _id: string; + + data: any; + updatedAt: Date; +} diff --git a/definition/IOmnichannelBusinessUnit.ts b/definition/IOmnichannelBusinessUnit.ts new file mode 100644 index 000000000000..9aacd2219eff --- /dev/null +++ b/definition/IOmnichannelBusinessUnit.ts @@ -0,0 +1,9 @@ +export interface IOmnichannelBusinessUnit { + _id: string; + name: string; + visibility: 'public' | 'private'; + type: string; + numMonitors: number; + numDepartments: number; + _updatedAt: Date; +} diff --git a/definition/IReport.ts b/definition/IReport.ts new file mode 100644 index 000000000000..61ee97bb9914 --- /dev/null +++ b/definition/IReport.ts @@ -0,0 +1,9 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; +import type { IMessage } from './IMessage/IMessage'; + +export interface IReport extends IRocketChatRecord { + message: IMessage; + description: string; + ts: Date; + userId: string; +} diff --git a/definition/IRocketChatRecord.ts b/definition/IRocketChatRecord.ts index 0dc3f5f51166..12a3ae9eb5d3 100644 --- a/definition/IRocketChatRecord.ts +++ b/definition/IRocketChatRecord.ts @@ -2,3 +2,8 @@ export interface IRocketChatRecord { _id: string; _updatedAt: Date; } + +export type RocketChatRecordDeleted = T & IRocketChatRecord & { + _deletedAt: Date; + __collection__: string; +}; diff --git a/definition/IRoom.ts b/definition/IRoom.ts index 62d68e97de2c..f070c35f14d7 100644 --- a/definition/IRoom.ts +++ b/definition/IRoom.ts @@ -3,6 +3,7 @@ import { IMessage } from './IMessage'; import { IUser, Username } from './IUser'; type RoomType = 'c' | 'd' | 'p' | 'l'; +type CallStatus = 'ringing' | 'ended' | 'declined' | 'ongoing'; export type RoomID = string; export type ChannelName = string; @@ -32,6 +33,11 @@ export interface IRoom extends IRocketChatRecord { lm?: Date; usersCount: number; jitsiTimeout: Date; + callStatus?: CallStatus; + webRtcCallStartTime?: Date; + servedBy?: { + _id: string; + }; streamingOptions?: { id?: string; @@ -61,6 +67,9 @@ export interface IRoom extends IRocketChatRecord { sysMes?: string[]; muted?: string[]; + + usernames?: string[]; + ts?: Date; } export interface ICreatedRoom extends IRoom { @@ -73,6 +82,8 @@ export interface IDirectMessageRoom extends Omit; } +export const isDirectMessageRoom = (room: Partial): room is IDirectMessageRoom => room.t === 'd'; + export enum OmnichannelSourceType { WIDGET = 'widget', EMAIL = 'email', @@ -107,6 +118,8 @@ export interface IOmnichannelRoom extends Omit room.t === 'l'; diff --git a/definition/IServerEvent.ts b/definition/IServerEvent.ts index edc831fb7c56..a0aab45e7a81 100644 --- a/definition/IServerEvent.ts +++ b/definition/IServerEvent.ts @@ -10,5 +10,5 @@ export interface IServerEvent { t: IServerEventType; ts: Date; ip: string; - u?: Partial; + u?: Partial>; } diff --git a/definition/ISession.ts b/definition/ISession.ts new file mode 100644 index 000000000000..93981f655cf7 --- /dev/null +++ b/definition/ISession.ts @@ -0,0 +1,29 @@ +export interface ISession { + _id: string; + + type: string; + mostImportantRole: string; + userId: string; + lastActivityAt: Date; + device: { + type: string; + name: string; + longVersion: string; + os: { + name: string; + version: string; + }; + version: string; + }; + year: number; + month: number; + day: number; + instanceId: string; + sessionId: string; + _updatedAt: Date; + createdAt: Date; + host: string; + ip: string; + loginAt: Date; + closedAt: Date; +} diff --git a/definition/ISetting.ts b/definition/ISetting.ts index 096d25bcae85..635506db1c2e 100644 --- a/definition/ISetting.ts +++ b/definition/ISetting.ts @@ -57,7 +57,7 @@ export interface ISettingBase { wizard?: { step: number; order: number; - }; + } | null; persistent?: boolean; // todo: remove readonly?: boolean; // todo: remove alert?: string; // todo: check if this is still used @@ -97,6 +97,7 @@ export interface ISettingCode extends ISettingBase { export interface ISettingAction extends ISettingBase { type: 'action'; + value: string; actionText?: string; } export interface ISettingAsset extends ISettingBase { @@ -104,6 +105,14 @@ export interface ISettingAsset extends ISettingBase { value: AssetValue; } +export interface ISettingDate extends ISettingBase { + type: 'date'; + value: Date; +} + +export const isDateSetting = (setting: ISetting): setting is ISettingDate => setting.type === 'date'; + + export const isSettingEnterprise = (setting: ISettingBase): setting is ISettingEnterprise => setting.enterprise === true; export const isSettingColor = (setting: ISettingBase): setting is ISettingColor => setting.type === 'color'; diff --git a/definition/ISmarshHistory.ts b/definition/ISmarshHistory.ts new file mode 100644 index 000000000000..43ba0d8f91a8 --- /dev/null +++ b/definition/ISmarshHistory.ts @@ -0,0 +1,6 @@ +export interface ISmarshHistory { + _id: string; + + lastRan: Date; + lastResult: string; +} diff --git a/definition/IStatistic.ts b/definition/IStatistic.ts new file mode 100644 index 000000000000..c94e92e5da5f --- /dev/null +++ b/definition/IStatistic.ts @@ -0,0 +1,24 @@ +export interface IStatistic { + _id: string; + + version: string; + instanceCount: number; + oplogEnabled: boolean; + totalUsers: number; + activeUsers: number; + nonActiveUsers: number; + onlineUsers: number; + awayUsers: number; + offlineUsers: number; + totalRooms: number; + totalChannels: number; + totalPrivateGroups: number; + totalDirect: number; + totalLivechat: number; + totalMessages: number; + totalChannelMessages: number; + totalPrivateGroupMessages: number; + totalDirectMessages: number; + totalLivechatMessages: number; + pushQueue: number; +} diff --git a/definition/ISubscription.ts b/definition/ISubscription.ts index 1761791b2fed..cceb56c56383 100644 --- a/definition/ISubscription.ts +++ b/definition/ISubscription.ts @@ -7,6 +7,7 @@ type RoomID = string; export interface ISubscription extends IRocketChatRecord { u: Pick; + v?: Pick; rid: RoomID; open: boolean; ts: Date; @@ -57,7 +58,6 @@ export interface ISubscription extends IRocketChatRecord { ignored?: unknown; department?: unknown; - v?: unknown; } export interface IOmnichannelSubscription extends ISubscription { diff --git a/definition/IUpload.ts b/definition/IUpload.ts new file mode 100644 index 000000000000..1eba78e441da --- /dev/null +++ b/definition/IUpload.ts @@ -0,0 +1,12 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IUpload extends IRocketChatRecord { + typeGroup?: string; + type?: string; + name: string; + aliases?: string; + extension?: string; + complete?: boolean; + uploading?: boolean; + progress?: number; +} diff --git a/definition/IUser.ts b/definition/IUser.ts index 732c684961ba..18642e511e7b 100644 --- a/definition/IUser.ts +++ b/definition/IUser.ts @@ -94,18 +94,11 @@ export interface IRole { mandatory2fa?: boolean; name: string; protected: boolean; - scope?: string; + // scope?: string; + scope: 'Users' | 'Subscriptions'; _id: string; } -export interface IDailyActiveUsers { - usersList: string[]; - users: number; - day: number; - month: number; - year: number; -} - export interface IUser extends IRocketChatRecord { _id: string; createdAt: Date; @@ -143,6 +136,12 @@ export interface IUser extends IRocketChatRecord { ldap?: boolean; } +export interface IRegisterUser extends IUser { + username: string; + name: string; +} +export const isRegisterUser = (user: IUser): user is IRegisterUser => user.username !== undefined && user.name !== undefined; + export type IUserDataEvent = { id: unknown; } diff --git a/definition/IUserDataFile.ts b/definition/IUserDataFile.ts new file mode 100644 index 000000000000..8be01d1c88d2 --- /dev/null +++ b/definition/IUserDataFile.ts @@ -0,0 +1,13 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IUserDataFile extends IRocketChatRecord { + name: string; + rid: string; + userId: string; + store: string; + complete: boolean; + uploading: boolean; + progress: number; + extension: string; + uploadedAt: Date; +} diff --git a/definition/IWebdavAccount.ts b/definition/IWebdavAccount.ts new file mode 100644 index 000000000000..264df6f57546 --- /dev/null +++ b/definition/IWebdavAccount.ts @@ -0,0 +1,9 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IWebdavAccount extends IRocketChatRecord { + user_id: string; + server_url: string; + username: string; + password: string; + name: string; +} diff --git a/definition/Serialized.ts b/definition/Serialized.ts index 2871f401689e..a6626dcdb9b8 100644 --- a/definition/Serialized.ts +++ b/definition/Serialized.ts @@ -1,9 +1,10 @@ -export type Serialized = T extends Date - ? (Exclude | string) - : T extends boolean | number | string | null | undefined - ? T - : T extends {} - ? { - [K in keyof T]: Serialized; - } - : null; +export type Serialized = + T extends Date + ? (Exclude | string) + : T extends boolean | number | string | null | undefined + ? T + : T extends {} + ? { + [K in keyof T]: Serialized; + } + : null; diff --git a/client/types/global.d.ts b/definition/externals/global.d.ts similarity index 96% rename from client/types/global.d.ts rename to definition/externals/global.d.ts index 9db2a7370da7..7d37c828de77 100644 --- a/client/types/global.d.ts +++ b/definition/externals/global.d.ts @@ -22,4 +22,5 @@ interface Window { interface PromiseConstructor { await(promise: Promise): T; + await(value: T): T; } diff --git a/definition/ldap-escape.d.ts b/definition/externals/ldap-escape.d.ts similarity index 100% rename from definition/ldap-escape.d.ts rename to definition/externals/ldap-escape.d.ts diff --git a/definition/externals/less/browser.d.ts b/definition/externals/less/browser.d.ts new file mode 100644 index 000000000000..c4fd60586556 --- /dev/null +++ b/definition/externals/less/browser.d.ts @@ -0,0 +1,5 @@ +declare module 'less/browser' { + function createLess(window: Window, options: Less.Options): LessStatic; + + export = createLess; +} diff --git a/definition/externals/meteor/accounts-base.d.ts b/definition/externals/meteor/accounts-base.d.ts new file mode 100644 index 000000000000..da689829afa5 --- /dev/null +++ b/definition/externals/meteor/accounts-base.d.ts @@ -0,0 +1,19 @@ +declare module 'meteor/accounts-base' { + namespace Accounts { + function _bcryptRounds(): number; + + function _getLoginToken(connectionId: string): string | undefined; + + function insertUserDoc(options: Record, user: Record): string; + + function _generateStampedLoginToken(): {token: string; when: Date}; + + function _runLoginHandlers(methodInvocation: Function, loginRequest: Record): Record | undefined; + + export class ConfigError extends Error {} + + export class LoginCancelledError extends Error { + public static readonly numericError: number; + } + } +} diff --git a/definition/externals/meteor/check.d.ts b/definition/externals/meteor/check.d.ts new file mode 100644 index 000000000000..401b56c831fd --- /dev/null +++ b/definition/externals/meteor/check.d.ts @@ -0,0 +1,7 @@ +import 'meteor/check'; + +declare module 'meteor/check' { + namespace Match { + function Where(condition: (val: T) => val is U): Matcher; + } +} diff --git a/definition/externals/meteor/ddp-common.d.ts b/definition/externals/meteor/ddp-common.d.ts new file mode 100644 index 000000000000..2dc41a5f819d --- /dev/null +++ b/definition/externals/meteor/ddp-common.d.ts @@ -0,0 +1,6 @@ +declare module 'meteor/ddp-common' { + namespace DDPCommon { + function stringifyDDP(msg: EJSONable): string; + function parseDDP(msg: string): EJSONable; + } +} diff --git a/client/types/meteor-htmljs.d.ts b/definition/externals/meteor/htmljs.d.ts similarity index 100% rename from client/types/meteor-htmljs.d.ts rename to definition/externals/meteor/htmljs.d.ts diff --git a/client/types/kadira-flow-router.d.ts b/definition/externals/meteor/kadira-flow-router.d.ts similarity index 94% rename from client/types/kadira-flow-router.d.ts rename to definition/externals/meteor/kadira-flow-router.d.ts index 7bf423879c7d..76d8d597600e 100644 --- a/client/types/kadira-flow-router.d.ts +++ b/definition/externals/meteor/kadira-flow-router.d.ts @@ -1,6 +1,11 @@ declare module 'meteor/kadira:flow-router' { import { Subscription } from 'meteor/meteor'; + type Context = { + params: Record; + queryParams: Record; + }; + type RouteOptions = { name: string; action?: ( @@ -13,13 +18,12 @@ declare module 'meteor/kadira:flow-router' { params?: Record, queryParams?: Record, ) => void; - triggersEnter?: unknown[]; - triggersExit?: unknown[]; - }; - - type Context = { - params: Record; - queryParams: Record; + triggersEnter?: (( + context: Context, + redirect: (pathDef: string) => void, + stop: () => void, + ) => void)[]; + triggersExit?: ((context: Context) => void)[]; }; class Route { diff --git a/client/types/konecty-user-presence.d.ts b/definition/externals/meteor/konecty-user-presence.d.ts similarity index 100% rename from client/types/konecty-user-presence.d.ts rename to definition/externals/meteor/konecty-user-presence.d.ts diff --git a/definition/externals/meteor/littledata-synced-cron.d.ts b/definition/externals/meteor/littledata-synced-cron.d.ts new file mode 100644 index 000000000000..412fa6f60d7c --- /dev/null +++ b/definition/externals/meteor/littledata-synced-cron.d.ts @@ -0,0 +1,7 @@ +declare module 'meteor/littledata:synced-cron' { + namespace SyncedCron { + function add(params: { name: string; schedule: Function; job: Function }): string; + function remove(name: string): string; + function nextScheduledAtDate(name: string): Date | number | undefined; + } +} diff --git a/definition/externals/meteor/logging.d.ts b/definition/externals/meteor/logging.d.ts new file mode 100644 index 000000000000..f5dedcfa522e --- /dev/null +++ b/definition/externals/meteor/logging.d.ts @@ -0,0 +1,19 @@ +declare module 'meteor/logging' { + namespace Log { + function format(obj: { + time?: Date; + timeInexact?: boolean; + level?: 'debug' | 'info' | 'warn' | 'error'; + file?: string; + line?: number; + app?: string; + originApp?: string; + message?: string; + program?: string; + satellite?: string; + stderr?: string; + }, options: { + color?: boolean; + }): string; + } +} diff --git a/client/types/meteor.d.ts b/definition/externals/meteor/meteor.d.ts similarity index 63% rename from client/types/meteor.d.ts rename to definition/externals/meteor/meteor.d.ts index bc1b77f9bcc4..d2f9bc1c8300 100644 --- a/client/types/meteor.d.ts +++ b/definition/externals/meteor/meteor.d.ts @@ -1,5 +1,25 @@ +/* eslint-disable @typescript-eslint/interface-name-prefix */ +import 'meteor/meteor'; + declare module 'meteor/meteor' { namespace Meteor { + interface ErrorStatic { + new (error: string | number, reason?: string, details?: any): Error; + } + + interface Error extends globalThis.Error { + error: string | number; + reason?: string; + } + + const server: any; + + const runAsUser: (userId: string, scope: Function) => any; + + interface MethodThisType { + twoFactorChecked: boolean | undefined; + } + interface IDDPMessage { msg: 'method'; method: string; @@ -36,7 +56,7 @@ declare module 'meteor/meteor' { const connection: IMeteorConnection; - function _relativeToSiteRootUrl(path: string): void; + function _relativeToSiteRootUrl(path: string): string; const _localStorage: Window['localStorage']; } } diff --git a/definition/externals/meteor/meteorhacks-inject-initial.d.ts b/definition/externals/meteor/meteorhacks-inject-initial.d.ts new file mode 100644 index 000000000000..b0498aaf4a0d --- /dev/null +++ b/definition/externals/meteor/meteorhacks-inject-initial.d.ts @@ -0,0 +1,5 @@ +declare module 'meteor/meteorhacks:inject-initial' { + namespace Inject { + function rawBody(key: string, value: string): void; + } +} diff --git a/client/types/mizzao-timesync.d.ts b/definition/externals/meteor/mizzao-timesync.d.ts similarity index 100% rename from client/types/mizzao-timesync.d.ts rename to definition/externals/meteor/mizzao-timesync.d.ts diff --git a/definition/externals/meteor/mongo.d.ts b/definition/externals/meteor/mongo.d.ts new file mode 100644 index 000000000000..7efb5b5b0860 --- /dev/null +++ b/definition/externals/meteor/mongo.d.ts @@ -0,0 +1,44 @@ +/* eslint-disable @typescript-eslint/interface-name-prefix */ +import * as mongodb from 'mongodb'; + +declare module 'meteor/mongo' { + interface RemoteCollectionDriver { + mongo: MongoConnection; + } + + interface OplogHandle { + stop(): void; + onOplogEntry(trigger: Record, callback: Function): void; + onSkippedEntries(callback: Function): void; + waitUntilCaughtUp(): void; + _defineTooFarBehind(value: number): void; + } + + interface MongoConnection { + db: mongodb.Db; + _oplogHandle: OplogHandle; + rawCollection(name: string): mongodb.Collection; + } + + namespace MongoInternals { + function defaultRemoteCollectionDriver(): RemoteCollectionDriver; + + class ConnectionClass {} + + function Connection(): ConnectionClass; + } + + namespace Mongo { + // eslint-disable-next-line @typescript-eslint/interface-name-prefix + interface CollectionStatic { + new ( + name: string | null, + options?: { + connection?: object | null; + idGeneration?: string; + transform?: Function | null; + }, + ): Collection; + } + } +} diff --git a/definition/externals/meteor/random.d.ts b/definition/externals/meteor/random.d.ts new file mode 100644 index 000000000000..4da29740e9cf --- /dev/null +++ b/definition/externals/meteor/random.d.ts @@ -0,0 +1,5 @@ +declare module 'meteor/random' { + namespace Random { + function _randomString(numberOfChars: number, map: string): string; + } +} diff --git a/definition/externals/meteor/rocketchat-streamer.d.ts b/definition/externals/meteor/rocketchat-streamer.d.ts new file mode 100644 index 000000000000..857a8b0daeea --- /dev/null +++ b/definition/externals/meteor/rocketchat-streamer.d.ts @@ -0,0 +1,90 @@ +declare module 'meteor/rocketchat:streamer' { + type Connection = any; + + type Client = { + meteorClient: boolean; + ws: any; + userId?: string; + send: Function; + } + + interface IPublication { + onStop: Function; + stop: Function; + connection: Connection; + _session: { + sendAdded(publicationName: string, id: string, fields: Record): void; + userId?: string; + socket?: { + send: Function; + }; + }; + ready: Function; + userId: string | undefined; + client: Client; + } + + type Rule = (this: IPublication, eventName: string, ...args: any) => Promise; + + interface IRules { + [k: string]: Rule; + } + + type DDPSubscription = { + eventName: string; + subscription: IPublication; + } + + type TransformMessage = (streamer: IStreamer, subscription: DDPSubscription, eventName: string, args: any[], allowed: boolean | object) => string | false; + + interface IStreamer { + serverOnly: boolean; + + subscriptions: Set; + + subscriptionName: string; + + allowEmit(eventName: string | boolean | Rule, fn?: Rule | 'all' | 'none' | 'logged'): void; + + allowWrite(eventName: string | boolean | Rule, fn?: Rule | 'all' | 'none' | 'logged'): void; + + allowRead(eventName: string | boolean | Rule, fn?: Rule | 'all' | 'none' | 'logged'): void; + + emit(event: string, ...data: any[]): void; + + on(event: string, fn: (...data: any[]) => void): void; + + removeSubscription(subscription: DDPSubscription, eventName: string): void; + + removeListener(event: string, fn: (...data: any[]) => void): void; + + __emit(...data: any[]): void; + + _emit(eventName: string, args: any[], origin: Connection | undefined, broadcast: boolean, transform?: TransformMessage): boolean; + + emitWithoutBroadcast(event: string, ...data: any[]): void; + + changedPayload(collection: string, id: string, fields: Record): string | false; + + _publish(publication: IPublication, eventName: string, options: boolean | {useCollection?: boolean; args?: any}): Promise; + } + + interface IStreamerConstructor { + // eslint-disable-next-line @typescript-eslint/no-misused-new + new(name: string, options?: {retransmit?: boolean; retransmitToSelf?: boolean}): IStreamer; + } +} + +declare module 'meteor/meteor' { + import { IStreamerConstructor, IStreamer } from 'meteor/rocketchat:streamer'; + + namespace Meteor { + const Streamer: IStreamerConstructor & IStreamer; + + namespace StreamerCentral { + const instances: { + [name: string]: IStreamer; + }; + } + } +} diff --git a/definition/externals/meteor/rocketchat-tap-i18n.d.ts b/definition/externals/meteor/rocketchat-tap-i18n.d.ts new file mode 100644 index 000000000000..d2c9baf18143 --- /dev/null +++ b/definition/externals/meteor/rocketchat-tap-i18n.d.ts @@ -0,0 +1,31 @@ +declare module 'meteor/rocketchat:tap-i18n' { + import { Tracker } from 'meteor/tracker'; + import i18next from 'i18next'; + + namespace TAPi18n { + function __(s: string | undefined, options?: { + lng?: string; + } & { + [replacements: string]: boolean | number | string; + }): string; + function getLanguages(): { + [language: string]: { + name: string; + en: string; + }; + }; + function setLanguage(language: string): void; + + // eslint-disable-next-line @typescript-eslint/camelcase + const _language_changed_tracker: Tracker.Dependency; + + // eslint-disable-next-line @typescript-eslint/camelcase + const _loaded_lang_session_key: string; + + const conf: { + i18n_files_route: string; + }; + } + + export const TAPi18next: typeof i18next; +} diff --git a/definition/externals/meteor/routepolicy.d.ts b/definition/externals/meteor/routepolicy.d.ts new file mode 100644 index 000000000000..ea6866632d17 --- /dev/null +++ b/definition/externals/meteor/routepolicy.d.ts @@ -0,0 +1,5 @@ +declare module 'meteor/routepolicy' { + export class RoutePolicy { + static declare(urlPrefix: string, type: string): void; + } +} diff --git a/definition/meteor-sha.d.ts b/definition/externals/meteor/sha.d.ts similarity index 100% rename from definition/meteor-sha.d.ts rename to definition/externals/meteor/sha.d.ts diff --git a/client/types/meteor-tracker.d.ts b/definition/externals/meteor/tracker.d.ts similarity index 100% rename from client/types/meteor-tracker.d.ts rename to definition/externals/meteor/tracker.d.ts diff --git a/mongodb.aug.d.ts b/definition/externals/mongodb.d.ts similarity index 100% rename from mongodb.aug.d.ts rename to definition/externals/mongodb.d.ts diff --git a/client/types/fuselage-tokens-colors.d.ts b/definition/externals/rocket.chat/fuselage-tokens/colors.d.ts similarity index 100% rename from client/types/fuselage-tokens-colors.d.ts rename to definition/externals/rocket.chat/fuselage-tokens/colors.d.ts diff --git a/client/types/fuselage-ui-kit.d.ts b/definition/externals/rocket.chat/fuselage-ui-kit.d.ts similarity index 100% rename from client/types/fuselage-ui-kit.d.ts rename to definition/externals/rocket.chat/fuselage-ui-kit.d.ts diff --git a/definition/externals/webdav.d.ts b/definition/externals/webdav.d.ts new file mode 100644 index 000000000000..c44ebda19adb --- /dev/null +++ b/definition/externals/webdav.d.ts @@ -0,0 +1,32 @@ +declare module 'webdav' { + type Stat = { + filename: string; + basename: string; + lastmod: string|null; + size: number; + type: string; + mime: string; + etag: string|null; + props: Record; + } + + type WebDavClient = { + copyFile(remotePath: string, targetRemotePath: string, options?: Record): Promise; + createDirectory(dirPath: string, options?: Record): Promise; + createReadStream(remoteFileName: string, options?: Record): ReadableStream; + createWriteStream(remoteFileName: string, options?: Record, callback?: Function): WritableStream; + customRequest(remotePath: string, requestOptions: Record, options?: Record): Promise; + deleteFile(remotePath: string, options?: Record): Promise; + exists(remotePath: string, options?: Record): Promise; + getDirectoryContents(remotePath: string, options?: Record): Promise>; + getFileContents(remoteFileName: string, options?: Record): Promise; + getFileDownloadLink(remoteFileName: string, options?: Record): string; + getFileUploadLink(remoteFileName: string, options?: Record): string; + getQuota(options?: Record): Promise; + moveFile(remotePath: string, targetRemotePath: string, options?: Record): Promise; + putFileContents(remoteFileName: string, data: string|Buffer, options?: Record): Promise; + stat(remotePath: string, options?: Record): Promise; + } + + export function createClient(remoteURL: string, opts?: Record): WebDavClient; +} diff --git a/definition/xml-encryption.ts b/definition/externals/xml-encryption.d.ts similarity index 100% rename from definition/xml-encryption.ts rename to definition/externals/xml-encryption.d.ts diff --git a/definition/rest/.eslintrc.js b/definition/rest/.eslintrc.js new file mode 120000 index 000000000000..4ce23c9428e7 --- /dev/null +++ b/definition/rest/.eslintrc.js @@ -0,0 +1 @@ +../../client/.eslintrc.js \ No newline at end of file diff --git a/definition/rest/.prettierrc b/definition/rest/.prettierrc new file mode 120000 index 000000000000..9d5a1f5613c4 --- /dev/null +++ b/definition/rest/.prettierrc @@ -0,0 +1 @@ +../../client/.prettierrc \ No newline at end of file diff --git a/client/contexts/ServerContext/endpoints/apps.ts b/definition/rest/apps/index.ts similarity index 100% rename from client/contexts/ServerContext/endpoints/apps.ts rename to definition/rest/apps/index.ts diff --git a/definition/rest/helpers/PaginatedRequest.ts b/definition/rest/helpers/PaginatedRequest.ts new file mode 100644 index 000000000000..3543675e3922 --- /dev/null +++ b/definition/rest/helpers/PaginatedRequest.ts @@ -0,0 +1,5 @@ +export type PaginatedRequest = { + count?: number; + offset?: number; + sort?: `{ "${S}": ${1 | -1} }` | string; +} & T; diff --git a/definition/rest/helpers/PaginatedResult.ts b/definition/rest/helpers/PaginatedResult.ts new file mode 100644 index 000000000000..ea153093cee2 --- /dev/null +++ b/definition/rest/helpers/PaginatedResult.ts @@ -0,0 +1,5 @@ +export type PaginatedResult = { + count: number; + offset: number; + total: number; +} & T; diff --git a/definition/rest/helpers/ReplacePlaceholders.ts b/definition/rest/helpers/ReplacePlaceholders.ts new file mode 100644 index 000000000000..96c910ce0ec3 --- /dev/null +++ b/definition/rest/helpers/ReplacePlaceholders.ts @@ -0,0 +1,7 @@ +export type ReplacePlaceholders = string extends TPath + ? TPath + : TPath extends `${infer Start}:${infer _Param}/${infer Rest}` + ? `${Start}${string}/${ReplacePlaceholders}` + : TPath extends `${infer Start}:${infer _Param}` + ? `${Start}${string}` + : TPath; diff --git a/definition/rest/index.ts b/definition/rest/index.ts new file mode 100644 index 000000000000..05d803a89c5a --- /dev/null +++ b/definition/rest/index.ts @@ -0,0 +1,130 @@ +import type { EnterpriseEndpoints } from '../../ee/definition/rest'; +import type { KeyOfEach } from '../utils'; +import type { AppsEndpoints } from './apps'; +import type { ReplacePlaceholders } from './helpers/ReplacePlaceholders'; +import type { BannersEndpoints } from './v1/banners'; +import type { ChannelsEndpoints } from './v1/channels'; +import type { ChatEndpoints } from './v1/chat'; +import type { CloudEndpoints } from './v1/cloud'; +import type { CustomUserStatusEndpoints } from './v1/customUserStatus'; +import type { DmEndpoints } from './v1/dm'; +import type { DnsEndpoints } from './v1/dns'; +import type { EmojiCustomEndpoints } from './v1/emojiCustom'; +import type { GroupsEndpoints } from './v1/groups'; +import type { ImEndpoints } from './v1/im'; +import type { InstancesEndpoints } from './v1/instances'; +import type { LDAPEndpoints } from './v1/ldap'; +import type { LicensesEndpoints } from './v1/licenses'; +import type { MiscEndpoints } from './v1/misc'; +import type { OmnichannelEndpoints } from './v1/omnichannel'; +import type { PermissionsEndpoints } from './v1/permissions'; +import type { RolesEndpoints } from './v1/roles'; +import type { RoomsEndpoints } from './v1/rooms'; +import type { SettingsEndpoints } from './v1/settings'; +import type { StatisticsEndpoints } from './v1/statistics'; +import type { TeamsEndpoints } from './v1/teams'; +import type { UsersEndpoints } from './v1/users'; + +type CommunityEndpoints = BannersEndpoints & + ChatEndpoints & + ChannelsEndpoints & + CloudEndpoints & + CustomUserStatusEndpoints & + DmEndpoints & + DnsEndpoints & + EmojiCustomEndpoints & + GroupsEndpoints & + ImEndpoints & + LDAPEndpoints & + RoomsEndpoints & + RolesEndpoints & + TeamsEndpoints & + SettingsEndpoints & + UsersEndpoints & + AppsEndpoints & + OmnichannelEndpoints & + StatisticsEndpoints & + LicensesEndpoints & + MiscEndpoints & + PermissionsEndpoints & + InstancesEndpoints; + +type Endpoints = CommunityEndpoints & EnterpriseEndpoints; + +type OperationsByPathPattern = TPathPattern extends any + ? OperationsByPathPatternAndMethod + : never; + +type OperationsByPathPatternAndMethod< + TPathPattern extends keyof Endpoints, + TMethod extends KeyOfEach = KeyOfEach, +> = TMethod extends any + ? { + pathPattern: TPathPattern; + method: TMethod; + path: ReplacePlaceholders; + params: GetParams; + result: GetResult; + } + : never; + +type Operations = OperationsByPathPattern; + +export type PathPattern = Operations['pathPattern']; + +export type Method = Operations['method']; + +export type Path = Operations['path']; + +export type MethodFor = TPath extends any + ? Extract['method'] + : never; + +export type PathFor = TMethod extends any + ? Extract['path'] + : never; + +export type MatchPathPattern = TPath extends any + ? Extract['pathPattern'] + : never; + +export type JoinPathPattern = Extract< + PathPattern, + `${TBasePath}/${TSubPathPattern}` | TSubPathPattern +>; + +type GetParams = TOperation extends (...args: any) => any + ? Parameters[0] extends void + ? void + : Parameters[0] + : never; + +type GetResult = TOperation extends (...args: any) => any + ? ReturnType + : never; + +export type OperationParams< + TMethod extends Method, + TPathPattern extends PathPattern, +> = TMethod extends keyof Endpoints[TPathPattern] + ? GetParams + : never; + +export type OperationResult< + TMethod extends Method, + TPathPattern extends PathPattern, +> = TMethod extends keyof Endpoints[TPathPattern] + ? GetResult + : never; + +export type UrlParams = string extends T + ? Record + : T extends `${infer _Start}:${infer Param}/${infer Rest}` + ? { [k in Param | keyof UrlParams]: string } + : T extends `${infer _Start}:${infer Param}` + ? { [k in Param]: string } + : {}; + +export type MethodOf = TPathPattern extends any + ? keyof Endpoints[TPathPattern] + : never; diff --git a/definition/rest/v1/banners.ts b/definition/rest/v1/banners.ts new file mode 100644 index 000000000000..e0c963a9ec76 --- /dev/null +++ b/definition/rest/v1/banners.ts @@ -0,0 +1,26 @@ +import type { BannerPlatform, IBanner } from '../../IBanner'; + +export type BannersEndpoints = { + /* @deprecated */ + 'banners.getNew': { + GET: (params: { platform: BannerPlatform; bid: IBanner['_id'] }) => { + banners: IBanner[]; + }; + }; + + 'banners/:id': { + GET: (params: { platform: BannerPlatform }) => { + banners: IBanner[]; + }; + }; + + 'banners': { + GET: (params: { platform: BannerPlatform }) => { + banners: IBanner[]; + }; + }; + + 'banners.dismiss': { + POST: (params: { bannerId: string }) => void; + }; +}; diff --git a/client/contexts/ServerContext/endpoints/v1/channels.ts b/definition/rest/v1/channels.ts similarity index 70% rename from client/contexts/ServerContext/endpoints/v1/channels.ts rename to definition/rest/v1/channels.ts index 0e2abbf7751c..1a5bbca27ba3 100644 --- a/client/contexts/ServerContext/endpoints/v1/channels.ts +++ b/definition/rest/v1/channels.ts @@ -1,6 +1,6 @@ -import type { IMessage } from '../../../../../definition/IMessage/IMessage'; -import type { IRoom } from '../../../../../definition/IRoom'; -import type { IUser } from '../../../../../definition/IUser'; +import type { IMessage } from '../../IMessage/IMessage'; +import type { IRoom } from '../../IRoom'; +import type { IUser } from '../../IUser'; export type ChannelsEndpoints = { 'channels.files': { diff --git a/client/contexts/ServerContext/endpoints/v1/chat.ts b/definition/rest/v1/chat.ts similarity index 84% rename from client/contexts/ServerContext/endpoints/v1/chat.ts rename to definition/rest/v1/chat.ts index 0ed5c1e02980..134f832b64ee 100644 --- a/client/contexts/ServerContext/endpoints/v1/chat.ts +++ b/definition/rest/v1/chat.ts @@ -1,5 +1,5 @@ -import type { IMessage } from '../../../../../definition/IMessage'; -import type { IRoom } from '../../../../../definition/IRoom'; +import type { IMessage } from '../../IMessage'; +import type { IRoom } from '../../IRoom'; export type ChatEndpoints = { 'chat.getMessage': { diff --git a/client/contexts/ServerContext/endpoints/v1/cloud.ts b/definition/rest/v1/cloud.ts similarity index 100% rename from client/contexts/ServerContext/endpoints/v1/cloud.ts rename to definition/rest/v1/cloud.ts diff --git a/client/contexts/ServerContext/endpoints/v1/customUserStatus.ts b/definition/rest/v1/customUserStatus.ts similarity index 100% rename from client/contexts/ServerContext/endpoints/v1/customUserStatus.ts rename to definition/rest/v1/customUserStatus.ts diff --git a/client/contexts/ServerContext/endpoints/v1/dm.ts b/definition/rest/v1/dm.ts similarity index 69% rename from client/contexts/ServerContext/endpoints/v1/dm.ts rename to definition/rest/v1/dm.ts index 0b20aad1819c..f19d000dbc3f 100644 --- a/client/contexts/ServerContext/endpoints/v1/dm.ts +++ b/definition/rest/v1/dm.ts @@ -1,5 +1,5 @@ -import type { IRoom } from '../../../../../definition/IRoom'; -import type { IUser } from '../../../../../definition/IUser'; +import type { IRoom } from '../../IRoom'; +import type { IUser } from '../../IUser'; export type DmEndpoints = { 'dm.create': { diff --git a/client/contexts/ServerContext/endpoints/v1/dns.ts b/definition/rest/v1/dns.ts similarity index 62% rename from client/contexts/ServerContext/endpoints/v1/dns.ts rename to definition/rest/v1/dns.ts index 136e8d698a98..b2d553e036f2 100644 --- a/client/contexts/ServerContext/endpoints/v1/dns.ts +++ b/definition/rest/v1/dns.ts @@ -5,8 +5,9 @@ export type DnsEndpoints = { }; }; 'dns.resolve.txt': { - GET: (params: { url: string }) => { - resolved: Record; + POST: (params: { url: string }) => { + resolved: string; + // resolved: Record; }; }; }; diff --git a/definition/rest/v1/emojiCustom.ts b/definition/rest/v1/emojiCustom.ts new file mode 100644 index 000000000000..8ef956e5acd1 --- /dev/null +++ b/definition/rest/v1/emojiCustom.ts @@ -0,0 +1,21 @@ +import type { ICustomEmojiDescriptor } from '../../ICustomEmojiDescriptor'; +import { PaginatedRequest } from '../helpers/PaginatedRequest'; +import { PaginatedResult } from '../helpers/PaginatedResult'; + +export type EmojiCustomEndpoints = { + 'emoji-custom.all': { + GET: (params: PaginatedRequest<{ query: string }, 'name'>) => { + emojis: ICustomEmojiDescriptor[]; + } & PaginatedResult; + }; + 'emoji-custom.list': { + GET: (params: { query: string }) => { + emojis?: { + update: ICustomEmojiDescriptor[]; + }; + }; + }; + 'emoji-custom.delete': { + POST: (params: { emojiId: ICustomEmojiDescriptor['_id'] }) => void; + }; +}; diff --git a/client/contexts/ServerContext/endpoints/v1/groups.ts b/definition/rest/v1/groups.ts similarity index 69% rename from client/contexts/ServerContext/endpoints/v1/groups.ts rename to definition/rest/v1/groups.ts index 5b01d443d451..3b5a584795ad 100644 --- a/client/contexts/ServerContext/endpoints/v1/groups.ts +++ b/definition/rest/v1/groups.ts @@ -1,6 +1,6 @@ -import type { IMessage } from '../../../../../definition/IMessage'; -import type { IRoom } from '../../../../../definition/IRoom'; -import type { IUser } from '../../../../../definition/IUser'; +import type { IMessage } from '../../IMessage'; +import type { IRoom } from '../../IRoom'; +import type { IUser } from '../../IUser'; export type GroupsEndpoints = { 'groups.files': { diff --git a/client/contexts/ServerContext/endpoints/v1/im.ts b/definition/rest/v1/im.ts similarity index 77% rename from client/contexts/ServerContext/endpoints/v1/im.ts rename to definition/rest/v1/im.ts index 700cdecfda44..6b9035e96ce1 100644 --- a/client/contexts/ServerContext/endpoints/v1/im.ts +++ b/definition/rest/v1/im.ts @@ -1,6 +1,6 @@ -import type { IMessage } from '../../../../../definition/IMessage'; -import type { IRoom } from '../../../../../definition/IRoom'; -import type { IUser } from '../../../../../definition/IUser'; +import type { IMessage } from '../../IMessage'; +import type { IRoom } from '../../IRoom'; +import type { IUser } from '../../IUser'; export type ImEndpoints = { 'im.create': { diff --git a/definition/rest/v1/instances.ts b/definition/rest/v1/instances.ts new file mode 100644 index 000000000000..f0a469d302a6 --- /dev/null +++ b/definition/rest/v1/instances.ts @@ -0,0 +1,19 @@ +import { IInstanceStatus } from '../../IInstanceStatus'; + +export type InstancesEndpoints = { + 'instances.get': { + GET: () => { + instances: ( + | IInstanceStatus + | { + connection: { + address: unknown; + currentStatus: unknown; + instanceRecord: unknown; + broadcastAuth: unknown; + }; + } + )[]; + }; + }; +}; diff --git a/client/contexts/ServerContext/endpoints/v1/ldap.ts b/definition/rest/v1/ldap.ts similarity index 77% rename from client/contexts/ServerContext/endpoints/v1/ldap.ts rename to definition/rest/v1/ldap.ts index 09b19d5637a3..5c83726b7da2 100644 --- a/client/contexts/ServerContext/endpoints/v1/ldap.ts +++ b/definition/rest/v1/ldap.ts @@ -1,4 +1,4 @@ -import type { TranslationKey } from '../../../TranslationContext'; +import type { TranslationKey } from '../../../client/contexts/TranslationContext'; export type LDAPEndpoints = { 'ldap.testConnection': { diff --git a/client/contexts/ServerContext/endpoints/v1/licenses.ts b/definition/rest/v1/licenses.ts similarity index 64% rename from client/contexts/ServerContext/endpoints/v1/licenses.ts rename to definition/rest/v1/licenses.ts index 5d78d69dd5ed..cc4b0dba981d 100644 --- a/client/contexts/ServerContext/endpoints/v1/licenses.ts +++ b/definition/rest/v1/licenses.ts @@ -1,9 +1,12 @@ -import type { ILicense } from '../../../../../ee/app/license/server/license'; +import type { ILicense } from '../../../ee/app/license/definitions/ILicense'; export type LicensesEndpoints = { 'licenses.get': { GET: () => { licenses: Array }; }; + 'licenses.add': { + POST: (params: { license: string }) => void; + }; 'licenses.maxActiveUsers': { GET: () => { maxActiveUsers: number | null; activeUsers: number }; }; diff --git a/client/contexts/ServerContext/endpoints/v1/misc.ts b/definition/rest/v1/misc.ts similarity index 100% rename from client/contexts/ServerContext/endpoints/v1/misc.ts rename to definition/rest/v1/misc.ts diff --git a/definition/rest/v1/omnichannel.ts b/definition/rest/v1/omnichannel.ts new file mode 100644 index 000000000000..1624a4be26a8 --- /dev/null +++ b/definition/rest/v1/omnichannel.ts @@ -0,0 +1,119 @@ +import { ILivechatDepartment } from '../../ILivechatDepartment'; +import { ILivechatMonitor } from '../../ILivechatMonitor'; +import { ILivechatTag } from '../../ILivechatTag'; +import { IMessage } from '../../IMessage'; +import { IOmnichannelRoom, IRoom } from '../../IRoom'; +import { ISetting } from '../../ISetting'; +import { PaginatedRequest } from '../helpers/PaginatedRequest'; +import { PaginatedResult } from '../helpers/PaginatedResult'; + +export type OmnichannelEndpoints = { + 'livechat/appearance': { + GET: () => { + appearance: ISetting[]; + }; + }; + 'livechat/visitors.info': { + GET: (params: { visitorId: string }) => { + visitor: { + visitorEmails: Array<{ + address: string; + }>; + }; + }; + }; + 'livechat/room.onHold': { + POST: (params: { roomId: IRoom['_id'] }) => void; + }; + 'livechat/monitors.list': { + GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ + monitors: ILivechatMonitor[]; + }>; + }; + 'livechat/tags.list': { + GET: (params: PaginatedRequest<{ text: string }, 'name'>) => PaginatedResult<{ + tags: ILivechatTag[]; + }>; + }; + 'livechat/department': { + GET: ( + params: PaginatedRequest<{ + text: string; + onlyMyDepartments?: boolean; + }>, + ) => PaginatedResult<{ + departments: ILivechatDepartment[]; + }>; + }; + 'livechat/department/:_id': { + GET: () => { + department: ILivechatDepartment; + }; + }; + 'livechat/departments.available-by-unit/:id': { + GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ + departments: ILivechatDepartment[]; + }>; + }; + 'livechat/departments.by-unit/': { + GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ + departments: ILivechatDepartment[]; + }>; + }; + + 'livechat/departments.by-unit/:id': { + GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ + departments: ILivechatDepartment[]; + }>; + }; + 'livechat/custom-fields': { + GET: (params: PaginatedRequest<{ text: string }>) => PaginatedResult<{ + customFields: [ + { + _id: string; + label: string; + }, + ]; + }>; + }; + 'livechat/rooms': { + GET: (params: { + guest: string; + fname: string; + servedBy: string[]; + status: string; + department: string; + from: string; + to: string; + customFields: any; + current: number; + itemsPerPage: number; + tags: string[]; + }) => PaginatedResult<{ + rooms: IOmnichannelRoom[]; + }>; + }; + 'livechat/:rid/messages': { + GET: (params: PaginatedRequest<{ query: string }>) => PaginatedResult<{ + messages: IMessage[]; + }>; + }; + 'livechat/users/agent': { + GET: (params: PaginatedRequest<{ text?: string }>) => PaginatedResult<{ + users: { + _id: string; + emails: { + address: string; + verified: boolean; + }[]; + status: string; + name: string; + username: string; + statusLivechat: string; + livechat: { + maxNumberSimultaneousChat: number; + }; + }[]; + }>; + }; +}; diff --git a/definition/rest/v1/permissions.ts b/definition/rest/v1/permissions.ts new file mode 100644 index 000000000000..d27a78c030ac --- /dev/null +++ b/definition/rest/v1/permissions.ts @@ -0,0 +1,43 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +import { IPermission } from '../../IPermission'; + +const ajv = new Ajv(); + +type PermissionsUpdateProps = { permissions: { _id: string; roles: string[] }[] }; + +const permissionUpdatePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + permissions: { + type: 'array', + items: { + type: 'object', + properties: { + _id: { type: 'string' }, + roles: { type: 'array', items: { type: 'string' }, uniqueItems: true }, + }, + additionalProperties: false, + required: ['_id', 'roles'], + }, + }, + }, + required: ['permissions'], + additionalProperties: false, +}; + +export const isBodyParamsValidPermissionUpdate = ajv.compile(permissionUpdatePropsSchema); + +export type PermissionsEndpoints = { + 'permissions.listAll': { + GET: (params: { updatedSince?: string }) => { + update: IPermission[]; + remove: IPermission[]; + }; + }; + 'permissions.update': { + POST: (params: PermissionsUpdateProps) => { + permissions: IPermission[]; + }; + }; +}; diff --git a/definition/rest/v1/roles.ts b/definition/rest/v1/roles.ts new file mode 100644 index 000000000000..ff3bee772890 --- /dev/null +++ b/definition/rest/v1/roles.ts @@ -0,0 +1,192 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +import { RocketChatRecordDeleted } from '../../IRocketChatRecord'; +import { IRole, IUser } from '../../IUser'; + +const ajv = new Ajv(); + +type RoleCreateProps = Pick & + Partial>; + +const roleCreatePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + name: { + type: 'string', + }, + description: { + type: 'string', + nullable: true, + }, + scope: { + type: 'string', + enum: ['Users', 'Subscriptions'], + nullable: true, + }, + mandatory2fa: { + type: 'boolean', + nullable: true, + }, + }, + required: ['name'], + additionalProperties: false, +}; + +export const isRoleCreateProps = ajv.compile(roleCreatePropsSchema); + +type RoleUpdateProps = { roleId: IRole['_id']; name: IRole['name'] } & Partial; + +const roleUpdatePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + roleId: { + type: 'string', + }, + name: { + type: 'string', + }, + description: { + type: 'string', + nullable: true, + }, + scope: { + type: 'string', + enum: ['Users', 'Subscriptions'], + nullable: true, + }, + mandatory2fa: { + type: 'boolean', + nullable: true, + }, + }, + required: ['roleId', 'name'], + additionalProperties: false, +}; + +export const isRoleUpdateProps = ajv.compile(roleUpdatePropsSchema); + +type RoleDeleteProps = { roleId: IRole['_id'] }; + +const roleDeletePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + roleId: { + type: 'string', + }, + }, + required: ['roleId'], + additionalProperties: false, +}; + +export const isRoleDeleteProps = ajv.compile(roleDeletePropsSchema); + +type RoleAddUserToRoleProps = { + username: string; + roleName: string; + roomId?: string; +}; + +const roleAddUserToRolePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + username: { + type: 'string', + }, + roleName: { + type: 'string', + }, + roomId: { + type: 'string', + nullable: true, + }, + }, + required: ['username', 'roleName'], + additionalProperties: false, +}; + +export const isRoleAddUserToRoleProps = ajv.compile(roleAddUserToRolePropsSchema); + +type RoleRemoveUserFromRoleProps = { + username: string; + roleName: string; + roomId?: string; + scope?: string; +}; + +const roleRemoveUserFromRolePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + username: { + type: 'string', + }, + roleName: { + type: 'string', + }, + roomId: { + type: 'string', + nullable: true, + }, + scope: { + type: 'string', + nullable: true, + }, + }, + required: ['username', 'roleName'], + additionalProperties: false, +}; + +export const isRoleRemoveUserFromRoleProps = ajv.compile(roleRemoveUserFromRolePropsSchema); + +type RoleSyncProps = { + updatedSince?: string; +}; + +export type RolesEndpoints = { + 'roles.list': { + GET: () => { + roles: IRole[]; + }; + }; + 'roles.sync': { + GET: (params: RoleSyncProps) => { + roles: { + update: IRole[]; + remove: RocketChatRecordDeleted[]; + }; + }; + }; + 'roles.create': { + POST: (params: RoleCreateProps) => { + role: IRole; + }; + }; + + 'roles.addUserToRole': { + POST: (params: RoleAddUserToRoleProps) => { + role: IRole; + }; + }; + + 'roles.getUsersInRole': { + GET: (params: { roomId: string; role: string; offset: number; count: number }) => { + users: IUser[]; + total: number; + }; + }; + + 'roles.update': { + POST: (role: RoleUpdateProps) => { + role: IRole; + }; + }; + + 'roles.delete': { + POST: (prop: RoleDeleteProps) => void; + }; + + 'roles.removeUserFromRole': { + POST: (props: RoleRemoveUserFromRoleProps) => { + role: IRole; + }; + }; +}; diff --git a/client/contexts/ServerContext/endpoints/v1/rooms.ts b/definition/rest/v1/rooms.ts similarity index 81% rename from client/contexts/ServerContext/endpoints/v1/rooms.ts rename to definition/rest/v1/rooms.ts index 960610ec558a..92f0bc5895cb 100644 --- a/client/contexts/ServerContext/endpoints/v1/rooms.ts +++ b/definition/rest/v1/rooms.ts @@ -1,6 +1,6 @@ -import type { IMessage } from '../../../../../definition/IMessage'; -import type { IRoom } from '../../../../../definition/IRoom'; -import type { IUser } from '../../../../../definition/IUser'; +import type { IMessage } from '../../IMessage'; +import type { IRoom } from '../../IRoom'; +import type { IUser } from '../../IUser'; export type RoomsEndpoints = { 'rooms.autocomplete.channelAndPrivate': { diff --git a/definition/rest/v1/settings.ts b/definition/rest/v1/settings.ts new file mode 100644 index 000000000000..e88a6d144d67 --- /dev/null +++ b/definition/rest/v1/settings.ts @@ -0,0 +1,110 @@ +import { ISetting, ISettingColor } from '../../ISetting'; +import { PaginatedResult } from '../helpers/PaginatedResult'; + +type SettingsUpdateProps = + | SettingsUpdatePropDefault + | SettingsUpdatePropsActions + | SettingsUpdatePropsColor; + +type SettingsUpdatePropsActions = { + execute: boolean; +}; + +export type OauthCustomConfiguration = { + _id: string; + clientId?: string; + custom: unknown; + service?: string; + serverURL: unknown; + tokenPath: unknown; + identityPath: unknown; + authorizePath: unknown; + scope: unknown; + loginStyle: unknown; + tokenSentVia: unknown; + identityTokenSentVia: unknown; + keyField: unknown; + usernameField: unknown; + emailField: unknown; + nameField: unknown; + avatarField: unknown; + rolesClaim: unknown; + groupsClaim: unknown; + mapChannels: unknown; + channelsMap: unknown; + channelsAdmin: unknown; + mergeUsers: unknown; + mergeRoles: unknown; + accessTokenParam: unknown; + showButton: unknown; + + appId: unknown; + consumerKey?: string; + + clientConfig: unknown; + buttonLabelText: unknown; + buttonLabelColor: unknown; + buttonColor: unknown; +}; + +export const isOauthCustomConfiguration = (config: any): config is OauthCustomConfiguration => + Boolean(config); + +export const isSettingsUpdatePropsActions = ( + props: Partial, +): props is SettingsUpdatePropsActions => 'execute' in props; + +type SettingsUpdatePropsColor = { + editor: ISettingColor['editor']; + value: ISetting['value']; +}; + +export const isSettingsUpdatePropsColor = ( + props: Partial, +): props is SettingsUpdatePropsColor => 'editor' in props && 'value' in props; + +type SettingsUpdatePropDefault = { + value: ISetting['value']; +}; + +export const isSettingsUpdatePropDefault = ( + props: Partial, +): props is SettingsUpdatePropDefault => 'value' in props; + +export type SettingsEndpoints = { + 'settings.public': { + GET: () => PaginatedResult & { + settings: Array; + }; + }; + + 'settings.oauth': { + GET: () => { + services: Partial[]; + }; + }; + + 'settings.addCustomOAuth': { + POST: (params: { name: string }) => void; + }; + + 'settings': { + GET: () => { + settings: ISetting[]; + }; + }; + + 'settings/:_id': { + GET: () => Pick; + POST: (params: SettingsUpdateProps) => void; + }; + + 'service.configurations': { + GET: () => { + configurations: Array<{ + appId: string; + secret: string; + }>; + }; + }; +}; diff --git a/client/contexts/ServerContext/endpoints/v1/statistics.ts b/definition/rest/v1/statistics.ts similarity index 63% rename from client/contexts/ServerContext/endpoints/v1/statistics.ts rename to definition/rest/v1/statistics.ts index 178d8f5d66b8..5820d2be290b 100644 --- a/client/contexts/ServerContext/endpoints/v1/statistics.ts +++ b/definition/rest/v1/statistics.ts @@ -1,4 +1,4 @@ -import type { IStats } from '../../../../../definition/IStats'; +import type { IStats } from '../../IStats'; export type StatisticsEndpoints = { statistics: { diff --git a/definition/rest/v1/teams/TeamsAddMembersProps.spec.ts b/definition/rest/v1/teams/TeamsAddMembersProps.spec.ts new file mode 100644 index 000000000000..cc35d52bc5d9 --- /dev/null +++ b/definition/rest/v1/teams/TeamsAddMembersProps.spec.ts @@ -0,0 +1,96 @@ +import { assert } from 'chai'; + +import { isTeamsAddMembersProps } from './TeamsAddMembersProps'; + +describe('TeamsAddMemberProps (definition/rest/v1)', () => { + describe('isTeamsAddMembersProps', () => { + it('should be a function', () => { + assert.isFunction(isTeamsAddMembersProps); + }); + it('should return false if the parameter is empty', () => { + assert.isFalse(isTeamsAddMembersProps({})); + }); + + it('should return false if teamId is provided but no member was provided', () => { + assert.isFalse(isTeamsAddMembersProps({ teamId: '123' })); + }); + + it('should return false if teamName is provided but no member was provided', () => { + assert.isFalse(isTeamsAddMembersProps({ teamName: '123' })); + }); + + it('should return false if members is provided but no teamId or teamName were provided', () => { + assert.isFalse(isTeamsAddMembersProps({ members: [{ userId: '123' }] })); + }); + + it('should return false if teamName was provided but members are empty', () => { + assert.isFalse(isTeamsAddMembersProps({ teamName: '123', members: [] })); + }); + + it('should return false if teamId was provided but members are empty', () => { + assert.isFalse(isTeamsAddMembersProps({ teamId: '123', members: [] })); + }); + + it('should return false if members with role is provided but no teamId or teamName were provided', () => { + assert.isFalse(isTeamsAddMembersProps({ members: [{ userId: '123', roles: ['123'] }] })); + }); + + it('should return true if members is provided and teamId is provided', () => { + assert.isTrue(isTeamsAddMembersProps({ members: [{ userId: '123' }], teamId: '123' })); + }); + + it('should return true if members is provided and teamName is provided', () => { + assert.isTrue(isTeamsAddMembersProps({ members: [{ userId: '123' }], teamName: '123' })); + }); + + it('should return true if members with role is provided and teamId is provided', () => { + assert.isTrue( + isTeamsAddMembersProps({ members: [{ userId: '123', roles: ['123'] }], teamId: '123' }), + ); + }); + + it('should return true if members with role is provided and teamName is provided', () => { + assert.isTrue( + isTeamsAddMembersProps({ members: [{ userId: '123', roles: ['123'] }], teamName: '123' }), + ); + }); + + it('should return false if teamName was provided and members contains an invalid property', () => { + assert.isFalse( + isTeamsAddMembersProps({ + teamName: '123', + members: [{ userId: '123', roles: ['123'], invalid: true }], + }), + ); + }); + + it('should return false if teamId was provided and members contains an invalid property', () => { + assert.isFalse( + isTeamsAddMembersProps({ + teamId: '123', + members: [{ userId: '123', roles: ['123'], invalid: true }], + }), + ); + }); + + it('should return false if teamName informed but contains an invalid property', () => { + assert.isFalse( + isTeamsAddMembersProps({ + member: [{ userId: '123', roles: ['123'] }], + teamName: '123', + invalid: true, + }), + ); + }); + + it('should return false if teamId informed but contains an invalid property', () => { + assert.isFalse( + isTeamsAddMembersProps({ + member: [{ userId: '123', roles: ['123'] }], + teamId: '123', + invalid: true, + }), + ); + }); + }); +}); diff --git a/definition/rest/v1/teams/TeamsAddMembersProps.ts b/definition/rest/v1/teams/TeamsAddMembersProps.ts new file mode 100644 index 000000000000..dfcf7da70f87 --- /dev/null +++ b/definition/rest/v1/teams/TeamsAddMembersProps.ts @@ -0,0 +1,80 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +import { ITeamMemberParams } from '../../../../server/sdk/types/ITeamService'; + +const ajv = new Ajv(); + +export type TeamsAddMembersProps = ({ teamId: string } | { teamName: string }) & { + members: ITeamMemberParams[]; +}; + +const teamsAddMembersPropsSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + properties: { + teamId: { + type: 'string', + }, + members: { + type: 'array', + items: { + type: 'object', + properties: { + userId: { + type: 'string', + }, + roles: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + }, + required: ['userId'], + additionalProperties: false, + }, + minItems: 1, + uniqueItems: true, + }, + }, + required: ['teamId', 'members'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + teamName: { + type: 'string', + }, + members: { + type: 'array', + items: { + type: 'object', + properties: { + userId: { + type: 'string', + }, + roles: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + }, + required: ['userId'], + additionalProperties: false, + }, + minItems: 1, + uniqueItems: true, + }, + }, + required: ['teamName', 'members'], + additionalProperties: false, + }, + ], +}; + +export const isTeamsAddMembersProps = ajv.compile(teamsAddMembersPropsSchema); diff --git a/definition/rest/v1/teams/TeamsConvertToChannelProps.spec.ts b/definition/rest/v1/teams/TeamsConvertToChannelProps.spec.ts new file mode 100644 index 000000000000..2167daf64991 --- /dev/null +++ b/definition/rest/v1/teams/TeamsConvertToChannelProps.spec.ts @@ -0,0 +1,43 @@ +import { assert } from 'chai'; + +import { isTeamsConvertToChannelProps } from './TeamsConvertToChannelProps'; + +describe('TeamsConvertToChannelProps (definition/rest/v1)', () => { + describe('isTeamsConvertToChannelProps', () => { + it('should be a function', () => { + assert.isFunction(isTeamsConvertToChannelProps); + }); + it('should return false if neither teamName or teamId is provided', () => { + assert.isFalse(isTeamsConvertToChannelProps({})); + }); + + it('should return true if teamName is provided', () => { + assert.isTrue(isTeamsConvertToChannelProps({ teamName: 'teamName' })); + }); + + it('should return true if teamId is provided', () => { + assert.isTrue(isTeamsConvertToChannelProps({ teamId: 'teamId' })); + }); + + it('should return false if both teamName and teamId are provided', () => { + assert.isFalse(isTeamsConvertToChannelProps({ teamName: 'teamName', teamId: 'teamId' })); + }); + + it('should return false if teamName is not a string', () => { + assert.isFalse(isTeamsConvertToChannelProps({ teamName: 1 })); + }); + + it('should return false if teamId is not a string', () => { + assert.isFalse(isTeamsConvertToChannelProps({ teamId: 1 })); + }); + + it('should return false if an additionalProperties is provided', () => { + assert.isFalse( + isTeamsConvertToChannelProps({ + teamName: 'teamName', + additionalProperties: 'additionalProperties', + }), + ); + }); + }); +}); diff --git a/definition/rest/v1/teams/TeamsConvertToChannelProps.ts b/definition/rest/v1/teams/TeamsConvertToChannelProps.ts new file mode 100644 index 000000000000..53455e986622 --- /dev/null +++ b/definition/rest/v1/teams/TeamsConvertToChannelProps.ts @@ -0,0 +1,49 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +const ajv = new Ajv(); + +export type TeamsConvertToChannelProps = { + roomsToRemove?: string[]; +} & ({ teamId: string } | { teamName: string }); + +const teamsConvertToTeamsPropsSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + + properties: { + roomsToRemove: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + teamId: { + type: 'string', + }, + }, + required: ['teamId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + roomsToRemove: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + teamName: { + type: 'string', + }, + }, + required: ['teamName'], + additionalProperties: false, + }, + ], +}; + +export const isTeamsConvertToChannelProps = ajv.compile(teamsConvertToTeamsPropsSchema); diff --git a/definition/rest/v1/teams/TeamsDeleteProps.spec.ts b/definition/rest/v1/teams/TeamsDeleteProps.spec.ts new file mode 100644 index 000000000000..ee1cae358545 --- /dev/null +++ b/definition/rest/v1/teams/TeamsDeleteProps.spec.ts @@ -0,0 +1,71 @@ +import { assert } from 'chai'; + +import { isTeamsDeleteProps } from './TeamsDeleteProps'; + +describe('TeamsDeleteProps (definition/rest/v1)', () => { + describe('isTeamsDeleteProps', () => { + it('should be a function', () => { + assert.isFunction(isTeamsDeleteProps); + }); + + it('should return false if neither teamName or teamId is provided', () => { + assert.isFalse(isTeamsDeleteProps({})); + }); + + it('should return true if teamId is provided', () => { + assert.isTrue(isTeamsDeleteProps({ teamId: 'teamId' })); + }); + + it('should return true if teamName is provided', () => { + assert.isTrue(isTeamsDeleteProps({ teamName: 'teamName' })); + }); + + it('should return false if teamId and roomsToRemove are provided, but roomsToRemove is empty', () => { + assert.isFalse(isTeamsDeleteProps({ teamId: 'teamId', roomsToRemove: [] })); + }); + + it('should return false if teamName and roomsToRemove are provided, but roomsToRemove is empty', () => { + assert.isFalse(isTeamsDeleteProps({ teamName: 'teamName', roomsToRemove: [] })); + }); + + it('should return true if teamId and roomsToRemove are provided', () => { + assert.isTrue(isTeamsDeleteProps({ teamId: 'teamId', roomsToRemove: ['roomId'] })); + }); + + it('should return true if teamName and roomsToRemove are provided', () => { + assert.isTrue(isTeamsDeleteProps({ teamName: 'teamName', roomsToRemove: ['roomId'] })); + }); + + it('should return false if teamId and roomsToRemove are provided, but roomsToRemove is not an array', () => { + assert.isFalse(isTeamsDeleteProps({ teamId: 'teamId', roomsToRemove: {} })); + }); + + it('should return false if teamName and roomsToRemove are provided, but roomsToRemove is not an array', () => { + assert.isFalse(isTeamsDeleteProps({ teamName: 'teamName', roomsToRemove: {} })); + }); + + it('should return false if teamId and roomsToRemove are provided, but roomsToRemove is not an array of strings', () => { + assert.isFalse(isTeamsDeleteProps({ teamId: 'teamId', roomsToRemove: [1] })); + }); + + it('should return false if teamName and roomsToRemove are provided, but roomsToRemove is not an array of strings', () => { + assert.isFalse(isTeamsDeleteProps({ teamName: 'teamName', roomsToRemove: [1] })); + }); + + it('should return false if teamName and rooms are provided but an extra property is provided', () => { + assert.isFalse( + isTeamsDeleteProps({ + teamName: 'teamName', + roomsToRemove: ['roomsToRemove'], + extra: 'extra', + }), + ); + }); + + it('should return false if teamId and rooms are provided but an extra property is provided', () => { + assert.isFalse( + isTeamsDeleteProps({ teamId: 'teamId', roomsToRemove: ['roomsToRemove'], extra: 'extra' }), + ); + }); + }); +}); diff --git a/definition/rest/v1/teams/TeamsDeleteProps.ts b/definition/rest/v1/teams/TeamsDeleteProps.ts new file mode 100644 index 000000000000..8e194032746d --- /dev/null +++ b/definition/rest/v1/teams/TeamsDeleteProps.ts @@ -0,0 +1,52 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +const ajv = new Ajv(); + +export type TeamsDeleteProps = ({ teamId: string } | { teamName: string }) & { + roomsToRemove?: string[]; +}; + +const teamsDeletePropsSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + properties: { + teamId: { + type: 'string', + }, + roomsToRemove: { + type: 'array', + items: { + type: 'string', + }, + minItems: 1, + uniqueItems: true, + nullable: true, + }, + }, + required: ['teamId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + teamName: { + type: 'string', + }, + roomsToRemove: { + type: 'array', + items: { + type: 'string', + }, + minItems: 1, + uniqueItems: true, + nullable: true, + }, + }, + required: ['teamName'], + additionalProperties: false, + }, + ], +}; + +export const isTeamsDeleteProps = ajv.compile(teamsDeletePropsSchema); diff --git a/definition/rest/v1/teams/TeamsLeaveProps.spec.ts b/definition/rest/v1/teams/TeamsLeaveProps.spec.ts new file mode 100644 index 000000000000..6661ab100bf1 --- /dev/null +++ b/definition/rest/v1/teams/TeamsLeaveProps.spec.ts @@ -0,0 +1,63 @@ +import { assert } from 'chai'; + +import { isTeamsLeaveProps } from './TeamsLeaveProps'; + +describe('TeamsLeaveProps (definition/rest/v1)', () => { + describe('isTeamsLeaveProps', () => { + it('should be a function', () => { + assert.isFunction(isTeamsLeaveProps); + }); + + it('should return false if neither teamName or teamId is provided', () => { + assert.isFalse(isTeamsLeaveProps({})); + }); + + it('should return true if teamId is provided', () => { + assert.isTrue(isTeamsLeaveProps({ teamId: 'teamId' })); + }); + + it('should return true if teamName is provided', () => { + assert.isTrue(isTeamsLeaveProps({ teamName: 'teamName' })); + }); + + it('should return false if teamId and roomsToRemove are provided, but roomsToRemove is empty', () => { + assert.isFalse(isTeamsLeaveProps({ teamId: 'teamId', rooms: [] })); + }); + + it('should return false if teamName and rooms are provided, but rooms is empty', () => { + assert.isFalse(isTeamsLeaveProps({ teamName: 'teamName', rooms: [] })); + }); + + it('should return true if teamId and rooms are provided', () => { + assert.isTrue(isTeamsLeaveProps({ teamId: 'teamId', rooms: ['roomId'] })); + }); + + it('should return true if teamName and rooms are provided', () => { + assert.isTrue(isTeamsLeaveProps({ teamName: 'teamName', rooms: ['roomId'] })); + }); + + it('should return false if teamId and rooms are provided, but rooms is not an array', () => { + assert.isFalse(isTeamsLeaveProps({ teamId: 'teamId', rooms: {} })); + }); + + it('should return false if teamName and rooms are provided, but rooms is not an array', () => { + assert.isFalse(isTeamsLeaveProps({ teamName: 'teamName', rooms: {} })); + }); + + it('should return false if teamId and rooms are provided, but rooms is not an array of strings', () => { + assert.isFalse(isTeamsLeaveProps({ teamId: 'teamId', rooms: [1] })); + }); + + it('should return false if teamName and rooms are provided, but rooms is not an array of strings', () => { + assert.isFalse(isTeamsLeaveProps({ teamName: 'teamName', rooms: [1] })); + }); + + it('should return false if teamName and rooms are provided but an extra property is provided', () => { + assert.isFalse(isTeamsLeaveProps({ teamName: 'teamName', rooms: ['rooms'], extra: 'extra' })); + }); + + it('should return false if teamId and rooms are provided but an extra property is provided', () => { + assert.isFalse(isTeamsLeaveProps({ teamId: 'teamId', rooms: ['rooms'], extra: 'extra' })); + }); + }); +}); diff --git a/definition/rest/v1/teams/TeamsLeaveProps.ts b/definition/rest/v1/teams/TeamsLeaveProps.ts new file mode 100644 index 000000000000..edda273d93c2 --- /dev/null +++ b/definition/rest/v1/teams/TeamsLeaveProps.ts @@ -0,0 +1,50 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +const ajv = new Ajv(); + +export type TeamsLeaveProps = ({ teamId: string } | { teamName: string }) & { rooms?: string[] }; + +const teamsLeavePropsSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + properties: { + teamId: { + type: 'string', + }, + rooms: { + type: 'array', + items: { + type: 'string', + }, + minItems: 1, + uniqueItems: true, + nullable: true, + }, + }, + required: ['teamId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + teamName: { + type: 'string', + }, + rooms: { + type: 'array', + items: { + type: 'string', + }, + minItems: 1, + uniqueItems: true, + nullable: true, + }, + }, + required: ['teamName'], + additionalProperties: false, + }, + ], +}; + +export const isTeamsLeaveProps = ajv.compile(teamsLeavePropsSchema); diff --git a/definition/rest/v1/teams/TeamsRemoveMemberProps.spec.ts b/definition/rest/v1/teams/TeamsRemoveMemberProps.spec.ts new file mode 100644 index 000000000000..351e9a9f474b --- /dev/null +++ b/definition/rest/v1/teams/TeamsRemoveMemberProps.spec.ts @@ -0,0 +1,86 @@ +import { assert } from 'chai'; + +import { isTeamsRemoveMemberProps } from './TeamsRemoveMemberProps'; + +describe('Teams (definition/rest/v1)', () => { + describe('isTeamsRemoveMemberProps', () => { + it('should be a function', () => { + assert.isFunction(isTeamsRemoveMemberProps); + }); + it('should return false if parameter is empty', () => { + assert.isFalse(isTeamsRemoveMemberProps({})); + }); + it('should return false if teamId is is informed but missing userId', () => { + assert.isFalse(isTeamsRemoveMemberProps({ teamId: 'teamId' })); + }); + it('should return false if teamName is is informed but missing userId', () => { + assert.isFalse(isTeamsRemoveMemberProps({ teamName: 'teamName' })); + }); + + it('should return true if teamId and userId are informed', () => { + assert.isTrue(isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId' })); + }); + it('should return true if teamName and userId are informed', () => { + assert.isTrue(isTeamsRemoveMemberProps({ teamName: 'teamName', userId: 'userId' })); + }); + + it('should return false if teamName and userId are informed but rooms are empty', () => { + assert.isFalse( + isTeamsRemoveMemberProps({ teamName: 'teamName', userId: 'userId', rooms: [] }), + ); + }); + + it('should return false if teamId and userId are informed and rooms are empty', () => { + assert.isFalse(isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId', rooms: [] })); + }); + + it('should return false if teamId and userId are informed but rooms are empty', () => { + assert.isFalse(isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId', rooms: [] })); + }); + + it('should return true if teamId and userId are informed and rooms are informed', () => { + assert.isTrue( + isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId', rooms: ['room'] }), + ); + }); + + it('should return false if teamId and userId are informed and rooms are informed but rooms is not an array of strings', () => { + assert.isFalse( + isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId', rooms: [123] }), + ); + }); + + it('should return false if teamName and userId are informed and rooms are informed but there is an extra property', () => { + assert.isFalse( + isTeamsRemoveMemberProps({ + teamName: 'teamName', + userId: 'userId', + rooms: ['room'], + extra: 'extra', + }), + ); + }); + + it('should return false if teamId and userId are informed and rooms are informed but there is an extra property', () => { + assert.isFalse( + isTeamsRemoveMemberProps({ + teamId: 'teamId', + userId: 'userId', + rooms: ['room'], + extra: 'extra', + }), + ); + }); + + it('should return false if teamName and userId are informed and rooms are informed but there is an extra property', () => { + assert.isFalse( + isTeamsRemoveMemberProps({ + teamName: 'teamName', + userId: 'userId', + rooms: ['room'], + extra: 'extra', + }), + ); + }); + }); +}); diff --git a/definition/rest/v1/teams/TeamsRemoveMemberProps.ts b/definition/rest/v1/teams/TeamsRemoveMemberProps.ts new file mode 100644 index 000000000000..04de75b1d3fe --- /dev/null +++ b/definition/rest/v1/teams/TeamsRemoveMemberProps.ts @@ -0,0 +1,59 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +const ajv = new Ajv(); + +export type TeamsRemoveMemberProps = ({ teamId: string } | { teamName: string }) & { + userId: string; + rooms?: Array; +}; + +const teamsRemoveMemberPropsSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + properties: { + teamId: { + type: 'string', + }, + userId: { + type: 'string', + }, + rooms: { + type: 'array', + items: { + type: 'string', + }, + minItems: 1, + uniqueItems: true, + nullable: true, + }, + }, + required: ['teamId', 'userId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + teamName: { + type: 'string', + }, + userId: { + type: 'string', + }, + rooms: { + type: 'array', + items: { + type: 'string', + }, + minItems: 1, + uniqueItems: true, + nullable: true, + }, + }, + required: ['teamName', 'userId'], + additionalProperties: false, + }, + ], +}; + +export const isTeamsRemoveMemberProps = ajv.compile(teamsRemoveMemberPropsSchema); diff --git a/definition/rest/v1/teams/TeamsRemoveRoomProps.spec.ts b/definition/rest/v1/teams/TeamsRemoveRoomProps.spec.ts new file mode 100644 index 000000000000..a6f9dc22e109 --- /dev/null +++ b/definition/rest/v1/teams/TeamsRemoveRoomProps.spec.ts @@ -0,0 +1,28 @@ +import { assert } from 'chai'; + +import { isTeamsRemoveRoomProps } from './TeamsRemoveRoomProps'; + +describe('TeamsRemoveRoomProps (definition/rest/v1)', () => { + describe('isTeamsRemoveRoomProps', () => { + it('should be a function', () => { + assert.isFunction(isTeamsRemoveRoomProps); + }); + it('should return false if roomId is not provided', () => { + assert.isFalse(isTeamsRemoveRoomProps({})); + }); + it('should return false if roomId is provided but no teamId or teamName were provided', () => { + assert.isFalse(isTeamsRemoveRoomProps({ roomId: 'roomId' })); + }); + it('should return false if roomId is provided and teamId is provided', () => { + assert.isTrue(isTeamsRemoveRoomProps({ roomId: 'roomId', teamId: 'teamId' })); + }); + it('should return true if roomId is provided and teamName is provided', () => { + assert.isTrue(isTeamsRemoveRoomProps({ roomId: 'roomId', teamName: 'teamName' })); + }); + it('should return false if roomId and teamName are provided but an additional property is provided', () => { + assert.isFalse( + isTeamsRemoveRoomProps({ roomId: 'roomId', teamName: 'teamName', foo: 'bar' }), + ); + }); + }); +}); diff --git a/definition/rest/v1/teams/TeamsRemoveRoomProps.ts b/definition/rest/v1/teams/TeamsRemoveRoomProps.ts new file mode 100644 index 000000000000..643799d3cf6e --- /dev/null +++ b/definition/rest/v1/teams/TeamsRemoveRoomProps.ts @@ -0,0 +1,42 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +import type { IRoom } from '../../../IRoom'; + +const ajv = new Ajv(); + +export type TeamsRemoveRoomProps = ({ teamId: string } | { teamName: string }) & { + roomId: IRoom['_id']; +}; + +export const teamsRemoveRoomPropsSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + properties: { + teamId: { + type: 'string', + }, + roomId: { + type: 'string', + }, + }, + required: ['teamId', 'roomId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + teamName: { + type: 'string', + }, + roomId: { + type: 'string', + }, + }, + required: ['teamName', 'roomId'], + additionalProperties: false, + }, + ], +}; + +export const isTeamsRemoveRoomProps = ajv.compile(teamsRemoveRoomPropsSchema); diff --git a/definition/rest/v1/teams/TeamsUpdateMemberProps.spec.ts b/definition/rest/v1/teams/TeamsUpdateMemberProps.spec.ts new file mode 100644 index 000000000000..838a9f95ca02 --- /dev/null +++ b/definition/rest/v1/teams/TeamsUpdateMemberProps.spec.ts @@ -0,0 +1,72 @@ +import { assert } from 'chai'; + +import { isTeamsUpdateMemberProps } from './TeamsUpdateMemberProps'; + +describe('TeamsUpdateMemberProps (definition/rest/v1)', () => { + describe('isTeamsUpdateMemberProps', () => { + it('should be a function', () => { + assert.isFunction(isTeamsUpdateMemberProps); + }); + it('should return false if the parameter is empty', () => { + assert.isFalse(isTeamsUpdateMemberProps({})); + }); + + it('should return false if teamId is provided but no member was provided', () => { + assert.isFalse(isTeamsUpdateMemberProps({ teamId: '123' })); + }); + + it('should return false if teamName is provided but no member was provided', () => { + assert.isFalse(isTeamsUpdateMemberProps({ teamName: '123' })); + }); + + it('should return false if member is provided but no teamId or teamName were provided', () => { + assert.isFalse(isTeamsUpdateMemberProps({ member: { userId: '123' } })); + }); + + it('should return false if member with role is provided but no teamId or teamName were provided', () => { + assert.isFalse(isTeamsUpdateMemberProps({ member: { userId: '123', roles: ['123'] } })); + }); + + it('should return true if member is provided and teamId is provided', () => { + assert.isTrue(isTeamsUpdateMemberProps({ member: { userId: '123' }, teamId: '123' })); + }); + + it('should return true if member is provided and teamName is provided', () => { + assert.isTrue(isTeamsUpdateMemberProps({ member: { userId: '123' }, teamName: '123' })); + }); + + it('should return true if member with role is provided and teamId is provided', () => { + assert.isTrue( + isTeamsUpdateMemberProps({ member: { userId: '123', roles: ['123'] }, teamId: '123' }), + ); + }); + + it('should return true if member with role is provided and teamName is provided', () => { + assert.isTrue( + isTeamsUpdateMemberProps({ member: { userId: '123', roles: ['123'] }, teamName: '123' }), + ); + }); + + it('should return false if teamName was provided and member contains an invalid property', () => { + assert.isFalse( + isTeamsUpdateMemberProps({ member: { userId: '123', invalid: '123' }, teamName: '123' }), + ); + }); + + it('should return false if teamId was provided and member contains an invalid property', () => { + assert.isFalse( + isTeamsUpdateMemberProps({ member: { userId: '123', invalid: '123' }, teamId: '123' }), + ); + }); + + it('should return false if contains an invalid property', () => { + assert.isFalse( + isTeamsUpdateMemberProps({ + member: { userId: '123', roles: ['123'] }, + teamName: '123', + invalid: true, + }), + ); + }); + }); +}); diff --git a/definition/rest/v1/teams/TeamsUpdateMemberProps.ts b/definition/rest/v1/teams/TeamsUpdateMemberProps.ts new file mode 100644 index 000000000000..a107bc4ce83b --- /dev/null +++ b/definition/rest/v1/teams/TeamsUpdateMemberProps.ts @@ -0,0 +1,70 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +import { ITeamMemberParams } from '../../../../server/sdk/types/ITeamService'; + +const ajv = new Ajv(); + +export type TeamsUpdateMemberProps = ({ teamId: string } | { teamName: string }) & { + member: ITeamMemberParams; +}; + +const teamsUpdateMemberPropsSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + properties: { + teamId: { + type: 'string', + }, + member: { + type: 'object', + properties: { + userId: { + type: 'string', + }, + roles: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + }, + required: ['userId'], + additionalProperties: false, + }, + }, + required: ['teamId', 'member'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + teamName: { + type: 'string', + }, + member: { + type: 'object', + properties: { + userId: { + type: 'string', + }, + roles: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + }, + required: ['userId'], + additionalProperties: false, + }, + }, + required: ['teamName', 'member'], + additionalProperties: false, + }, + ], +}; + +export const isTeamsUpdateMemberProps = ajv.compile(teamsUpdateMemberPropsSchema); diff --git a/definition/rest/v1/teams/TeamsUpdateProps.spec.ts b/definition/rest/v1/teams/TeamsUpdateProps.spec.ts new file mode 100644 index 000000000000..97bbca49a986 --- /dev/null +++ b/definition/rest/v1/teams/TeamsUpdateProps.spec.ts @@ -0,0 +1,192 @@ +import { assert } from 'chai'; + +import { isTeamsUpdateProps } from './TeamsUpdateProps'; + +describe('TeamsUpdateMemberProps (definition/rest/v1)', () => { + describe('isTeamsUpdateProps', () => { + it('should be a function', () => { + assert.isFunction(isTeamsUpdateProps); + }); + it('should return false when provided anything that is not an TeamsUpdateProps', () => { + assert.isFalse(isTeamsUpdateProps(undefined)); + assert.isFalse(isTeamsUpdateProps(null)); + assert.isFalse(isTeamsUpdateProps('')); + assert.isFalse(isTeamsUpdateProps(123)); + assert.isFalse(isTeamsUpdateProps({})); + assert.isFalse(isTeamsUpdateProps([])); + assert.isFalse(isTeamsUpdateProps(new Date())); + assert.isFalse(isTeamsUpdateProps(new Error())); + }); + it('should return false when only teamName is provided to TeamsUpdateProps', () => { + assert.isFalse( + isTeamsUpdateProps({ + teamName: 'teamName', + }), + ); + }); + + it('should return false when only teamId is provided to TeamsUpdateProps', () => { + assert.isFalse( + isTeamsUpdateProps({ + teamId: 'teamId', + }), + ); + }); + + it('should return false when teamName and data are provided to TeamsUpdateProps but data is an empty object', () => { + assert.isFalse( + isTeamsUpdateProps({ + teamName: 'teamName', + data: {}, + }), + ); + }); + + it('should return false when teamId and data are provided to TeamsUpdateProps but data is an empty object', () => { + assert.isFalse( + isTeamsUpdateProps({ + teamId: 'teamId', + data: {}, + }), + ); + }); + + it('should return false when teamName and data are provided to TeamsUpdateProps but data is not an object', () => { + assert.isFalse( + isTeamsUpdateProps({ + teamName: 'teamName', + data: 'data', + }), + ); + }); + + it('should return false when teamId and data are provided to TeamsUpdateProps but data is not an object', () => { + assert.isFalse( + isTeamsUpdateProps({ + teamId: 'teamId', + data: 'data', + }), + ); + }); + + it('should return true when teamName and data.name are provided to TeamsUpdateProps', () => { + assert.isTrue( + isTeamsUpdateProps({ + teamName: 'teamName', + data: { + name: 'name', + }, + }), + ); + }); + + it('should return true when teamId and data.name are provided to TeamsUpdateProps', () => { + assert.isTrue( + isTeamsUpdateProps({ + teamId: 'teamId', + data: { + name: 'name', + }, + }), + ); + }); + + it('should return true when teamName and data.type are provided to TeamsUpdateProps', () => { + assert.isTrue( + isTeamsUpdateProps({ + teamName: 'teamName', + data: { + type: 0, + }, + }), + ); + }); + + it('should return true when teamId and data.type are provided to TeamsUpdateProps', () => { + assert.isTrue( + isTeamsUpdateProps({ + teamId: 'teamId', + data: { + type: 0, + }, + }), + ); + }); + + it('should return true when teamName and data.name and data.type are provided to TeamsUpdateProps', () => { + assert.isTrue( + isTeamsUpdateProps({ + teamName: 'teamName', + data: { + name: 'name', + type: 0, + }, + }), + ); + }); + + it('should return true when teamId and data.name and data.type are provided to TeamsUpdateProps', () => { + assert.isTrue( + isTeamsUpdateProps({ + teamId: 'teamId', + data: { + name: 'name', + type: 0, + }, + }), + ); + }); + + it('should return false when teamName, data.name, data.type are some more extra data are provided to TeamsUpdateProps', () => { + assert.isFalse( + isTeamsUpdateProps({ + teamName: 'teamName', + data: { + name: 'name', + type: 0, + extra: 'extra', + }, + }), + ); + }); + + it('should return false when teamId, data.name, data.type are some more extra data are provided to TeamsUpdateProps', () => { + assert.isFalse( + isTeamsUpdateProps({ + teamId: 'teamId', + data: { + name: 'name', + type: 0, + extra: 'extra', + }, + }), + ); + }); + + it('should return false when teamName, data.name, data.type are some more extra parameter are provided to TeamsUpdateProps', () => { + assert.isFalse( + isTeamsUpdateProps({ + teamName: 'teamName', + extra: 'extra', + data: { + name: 'name', + type: 0, + }, + }), + ); + }); + + it('should return false when teamId, data.name, data.type are some more extra parameter are provided to TeamsUpdateProps', () => { + assert.isFalse( + isTeamsUpdateProps({ + teamId: 'teamId', + extra: 'extra', + data: { + name: 'name', + type: 0, + }, + }), + ); + }); + }); +}); diff --git a/definition/rest/v1/teams/TeamsUpdateProps.ts b/definition/rest/v1/teams/TeamsUpdateProps.ts new file mode 100644 index 000000000000..b5f92e10da7e --- /dev/null +++ b/definition/rest/v1/teams/TeamsUpdateProps.ts @@ -0,0 +1,74 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +import { TEAM_TYPE } from '../../../ITeam'; + +const ajv = new Ajv(); + +export type TeamsUpdateProps = ({ teamId: string } | { teamName: string }) & { + data: + | { + name: string; + type?: TEAM_TYPE; + } + | { + name?: string; + type: TEAM_TYPE; + }; +}; + +const teamsUpdatePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + updateRoom: { + type: 'boolean', + nullable: true, + }, + teamId: { + type: 'string', + nullable: true, + }, + teamName: { + type: 'string', + nullable: true, + }, + data: { + type: 'object', + properties: { + name: { + type: 'string', + nullable: true, + }, + type: { + type: 'number', + enum: [TEAM_TYPE.PUBLIC, TEAM_TYPE.PRIVATE], + }, + }, + additionalProperties: false, + required: [], + anyOf: [ + { + required: ['name'], + }, + { + required: ['type'], + }, + ], + }, + name: { + type: 'string', + nullable: true, + }, + }, + required: [], + oneOf: [ + { + required: ['teamId', 'data'], + }, + { + required: ['teamName', 'data'], + }, + ], + additionalProperties: false, +}; + +export const isTeamsUpdateProps = ajv.compile(teamsUpdatePropsSchema); diff --git a/definition/rest/v1/teams/index.ts b/definition/rest/v1/teams/index.ts new file mode 100644 index 000000000000..504617938512 --- /dev/null +++ b/definition/rest/v1/teams/index.ts @@ -0,0 +1,166 @@ +import type { + ITeamAutocompleteResult, + ITeamMemberInfo, +} from '../../../../server/sdk/types/ITeamService'; +import type { IRoom } from '../../../IRoom'; +import type { ITeam } from '../../../ITeam'; +import type { IUser } from '../../../IUser'; +import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; +import type { PaginatedResult } from '../../helpers/PaginatedResult'; +import type { TeamsAddMembersProps } from './TeamsAddMembersProps'; +import type { TeamsConvertToChannelProps } from './TeamsConvertToChannelProps'; +import type { TeamsDeleteProps } from './TeamsDeleteProps'; +import type { TeamsLeaveProps } from './TeamsLeaveProps'; +import type { TeamsRemoveMemberProps } from './TeamsRemoveMemberProps'; +import type { TeamsRemoveRoomProps } from './TeamsRemoveRoomProps'; +import type { TeamsUpdateMemberProps } from './TeamsUpdateMemberProps'; +import type { TeamsUpdateProps } from './TeamsUpdateProps'; + +type TeamProps = + | TeamsRemoveRoomProps + | TeamsConvertToChannelProps + | TeamsUpdateMemberProps + | TeamsAddMembersProps + | TeamsRemoveMemberProps + | TeamsDeleteProps + | TeamsLeaveProps + | TeamsUpdateProps; + +export const isTeamPropsWithTeamName = ( + props: T, +): props is T & { teamName: string } => 'teamName' in props; + +export const isTeamPropsWithTeamId = ( + props: T, +): props is T & { teamId: string } => 'teamId' in props; + +export type TeamsEndpoints = { + 'teams.list': { + GET: () => PaginatedResult & { teams: ITeam[] }; + }; + 'teams.listAll': { + GET: () => { teams: ITeam[] } & PaginatedResult; + }; + 'teams.create': { + POST: (params: { + name: ITeam['name']; + type: ITeam['type']; + members?: IUser['_id'][]; + room: { + id?: string; + name?: IRoom['name']; + members?: IUser['_id'][]; + readOnly?: boolean; + extraData?: { + teamId?: string; + teamMain?: boolean; + } & { [key: string]: string | boolean }; + options?: { + nameValidationRegex?: string; + creator: string; + subscriptionExtra?: { + open: boolean; + ls: Date; + prid: IRoom['_id']; + }; + } & { + [key: string]: + | string + | { + open: boolean; + ls: Date; + prid: IRoom['_id']; + }; + }; + }; + owner?: IUser['_id']; + }) => { + team: ITeam; + }; + }; + + 'teams.convertToChannel': { + POST: (params: TeamsConvertToChannelProps) => void; + }; + + 'teams.addRooms': { + POST: ( + params: + | { rooms: IRoom['_id'][]; teamId: string } + | { rooms: IRoom['_id'][]; teamName: string }, + ) => { rooms: IRoom[] }; + }; + + 'teams.removeRoom': { + POST: (params: TeamsRemoveRoomProps) => { room: IRoom }; + }; + + 'teams.members': { + GET: ( + params: ({ teamId: string } | { teamName: string }) & { + status?: string[]; + username?: string; + name?: string; + }, + ) => PaginatedResult & { members: ITeamMemberInfo[] }; + }; + + 'teams.addMembers': { + POST: (params: TeamsAddMembersProps) => void; + }; + + 'teams.updateMember': { + POST: (params: TeamsUpdateMemberProps) => void; + }; + + 'teams.removeMember': { + POST: (params: TeamsRemoveMemberProps) => void; + }; + + 'teams.leave': { + POST: (params: TeamsLeaveProps) => void; + }; + + 'teams.info': { + GET: (params: ({ teamId: string } | { teamName: string }) & {}) => { teamInfo: Partial }; + }; + + 'teams.autocomplete': { + GET: (params: { name: string }) => { teams: ITeamAutocompleteResult[] }; + }; + + 'teams.update': { + POST: (params: TeamsUpdateProps) => void; + }; + + 'teams.delete': { + POST: (params: TeamsDeleteProps) => void; + }; + + 'teams.listRoomsOfUser': { + GET: ( + params: + | { + teamId: ITeam['_id']; + userId: IUser['_id']; + canUserDelete?: boolean; + } + | { + teamName: ITeam['name']; + userId: IUser['_id']; + canUserDelete?: boolean; + }, + ) => PaginatedResult & { rooms: IRoom[] }; + }; + + 'teams.listRooms': { + GET: ( + params: PaginatedRequest & + ({ teamId: string } | { teamName: string }) & { filter?: string; type?: string }, + ) => PaginatedResult & { rooms: IRoom[] }; + }; + + 'teams.updateRoom': { + POST: (params: { roomId: IRoom['_id']; isDefault: boolean }) => { room: IRoom }; + }; +}; diff --git a/client/contexts/ServerContext/endpoints/v1/users.ts b/definition/rest/v1/users.ts similarity index 71% rename from client/contexts/ServerContext/endpoints/v1/users.ts rename to definition/rest/v1/users.ts index cbe8a74c8d40..337a2182f2e3 100644 --- a/client/contexts/ServerContext/endpoints/v1/users.ts +++ b/definition/rest/v1/users.ts @@ -1,5 +1,5 @@ -import type { ITeam } from '../../../../../definition/ITeam'; -import type { IUser } from '../../../../../definition/IUser'; +import type { ITeam } from '../../ITeam'; +import type { IUser } from '../../IUser'; export type UsersEndpoints = { 'users.2fa.sendEmailCode': { diff --git a/definition/utils.ts b/definition/utils.ts index 90e7f59df57d..211bcd5234de 100644 --- a/definition/utils.ts +++ b/definition/utils.ts @@ -7,3 +7,8 @@ export type ValueOf = T[keyof T]; export type UnionToIntersection = (T extends any ? (x: T) => void : never) extends (x: infer U) => void ? U : never; + +export type Awaited = T extends PromiseLike ? Awaited : T; + +// `T extends any` is a trick to apply a operator to each member of a union +export type KeyOfEach = T extends any ? keyof T : never; diff --git a/definition/webdav.ts b/definition/webdav.ts deleted file mode 100644 index afc380b8330e..000000000000 --- a/definition/webdav.ts +++ /dev/null @@ -1,32 +0,0 @@ -export type Stat = { - filename: string; - basename: string; - lastmod: string|null; - size: number; - type: string; - mime: string; - etag: string|null; - props: Record; -} - -export type WebDavClient = { - copyFile(remotePath: string, targetRemotePath: string, options?: Record): Promise; - createDirectory(dirPath: string, options?: Record): Promise; - createReadStream(remoteFileName: string, options?: Record): ReadableStream; - createWriteStream(remoteFileName: string, options?: Record, callback?: Function): WritableStream; - customRequest(remotePath: string, requestOptions: Record, options?: Record): Promise; - deleteFile(remotePath: string, options?: Record): Promise; - exists(remotePath: string, options?: Record): Promise; - getDirectoryContents(remotePath: string, options?: Record): Promise>; - getFileContents(remoteFileName: string, options?: Record): Promise; - getFileDownloadLink(remoteFileName: string, options?: Record): string; - getFileUploadLink(remoteFileName: string, options?: Record): string; - getQuota(options?: Record): Promise; - moveFile(remotePath: string, targetRemotePath: string, options?: Record): Promise; - putFileContents(remoteFileName: string, data: string|Buffer, options?: Record): Promise; - stat(remotePath: string, options?: Record): Promise; -} - -declare module 'webdav' { - export function createClient(remoteURL: string, opts?: Record): WebDavClient; -} diff --git a/ee/app/auditing/server/index.js b/ee/app/auditing/server/index.ts similarity index 79% rename from ee/app/auditing/server/index.js rename to ee/app/auditing/server/index.ts index a816e3a1bc2c..57ac621de7dd 100644 --- a/ee/app/auditing/server/index.js +++ b/ee/app/auditing/server/index.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { onLicense } from '../../license/server'; -import { Permissions, Roles } from '../../../../app/models/server'; +import { Permissions, Roles } from '../../../../app/models/server/raw'; onLicense('auditing', () => { require('./methods'); @@ -14,8 +14,8 @@ onLicense('auditing', () => { ]; const defaultRoles = [ - { name: 'auditor', scope: 'Users' }, - { name: 'auditor-log', scope: 'Users' }, + { name: 'auditor', scope: 'Users' as const }, + { name: 'auditor-log', scope: 'Users' as const }, ]; permissions.forEach((permission) => { @@ -23,7 +23,7 @@ onLicense('auditing', () => { }); defaultRoles.forEach((role) => - Roles.createOrUpdate(role.name, role.scope, role.description), + Roles.createOrUpdate(role.name, role.scope), ); }); }); diff --git a/ee/app/authorization/server/resetEnterprisePermissions.js b/ee/app/authorization/server/resetEnterprisePermissions.js deleted file mode 100644 index cebc88ba83be..000000000000 --- a/ee/app/authorization/server/resetEnterprisePermissions.js +++ /dev/null @@ -1,6 +0,0 @@ -import { Permissions } from '../../../../app/models/server'; -import { guestPermissions } from '../lib/guestPermissions'; - -export const resetEnterprisePermissions = function() { - Permissions.update({ _id: { $nin: guestPermissions } }, { $pull: { roles: 'guest' } }, { multi: true }); -}; diff --git a/ee/app/authorization/server/resetEnterprisePermissions.ts b/ee/app/authorization/server/resetEnterprisePermissions.ts new file mode 100644 index 000000000000..2c7cd9def87e --- /dev/null +++ b/ee/app/authorization/server/resetEnterprisePermissions.ts @@ -0,0 +1,7 @@ + +import { Permissions } from '../../../../app/models/server/raw'; +import { guestPermissions } from '../lib/guestPermissions'; + +export const resetEnterprisePermissions = async function(): Promise { + await Permissions.update({ _id: { $nin: guestPermissions } }, { $pull: { roles: 'guest' } }, { multi: true }); +}; diff --git a/ee/app/canned-responses/server/hooks/onMessageSentParsePlaceholder.ts b/ee/app/canned-responses/server/hooks/onMessageSentParsePlaceholder.ts index 7afe45463a93..c8a0006c7c07 100644 --- a/ee/app/canned-responses/server/hooks/onMessageSentParsePlaceholder.ts +++ b/ee/app/canned-responses/server/hooks/onMessageSentParsePlaceholder.ts @@ -29,6 +29,8 @@ const placeholderFields = { }, }; +const replaceAll = (text: string, old: string, replace: string): string => text.replace(new RegExp(old, 'g'), replace); + const handleBeforeSaveMessage = (message: IMessage, room: IOmnichannelRoom): any => { if (!message.msg || message.msg === '') { return message; @@ -50,7 +52,7 @@ const handleBeforeSaveMessage = (message: IMessage, room: IOmnichannelRoom): any const placeholderConfig = placeholderFields[field as keyof typeof placeholderFields]; const from = placeholderConfig.from === 'agent' ? agent : visitor; const data = get(from, placeholderConfig.dataKey, ''); - messageText = messageText.replace(templateKey, data); + messageText = replaceAll(messageText, templateKey, data); return messageText; }); diff --git a/ee/app/canned-responses/server/permissions.js b/ee/app/canned-responses/server/permissions.ts similarity index 90% rename from ee/app/canned-responses/server/permissions.js rename to ee/app/canned-responses/server/permissions.ts index 26e838540df1..f32650dd09b9 100644 --- a/ee/app/canned-responses/server/permissions.js +++ b/ee/app/canned-responses/server/permissions.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import Permissions from '../../../../app/models/server/models/Permissions'; +import { Permissions } from '../../../../app/models/server/raw'; Meteor.startup(() => { Permissions.create('view-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']); diff --git a/ee/app/engagement-dashboard/client/components/ChannelsTab/ChannelsTab.stories.js b/ee/app/engagement-dashboard/client/components/ChannelsTab/ChannelsTab.stories.js deleted file mode 100644 index 420ecaa95630..000000000000 --- a/ee/app/engagement-dashboard/client/components/ChannelsTab/ChannelsTab.stories.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Margins } from '@rocket.chat/fuselage'; -import React from 'react'; - -import ChannelsTab from '.'; - -export default { - title: 'admin/enterprise/engagement/ChannelsTab', - component: ChannelsTab, - decorators: [ - (fn) => , - ], -}; - -export const _default = () => ; diff --git a/ee/app/engagement-dashboard/client/components/ChannelsTab/TableSection.js b/ee/app/engagement-dashboard/client/components/ChannelsTab/TableSection.js deleted file mode 100644 index 9fbf33d21ac4..000000000000 --- a/ee/app/engagement-dashboard/client/components/ChannelsTab/TableSection.js +++ /dev/null @@ -1,163 +0,0 @@ -import { Box, Icon, Margins, Pagination, Select, Skeleton, Table, Tile, ActionButton } from '@rocket.chat/fuselage'; -import moment from 'moment'; -import React, { useMemo, useState } from 'react'; - -import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; -import { useEndpointData } from '../../../../../../client/hooks/useEndpointData'; -import Growth from '../../../../../../client/components/data/Growth'; -import { Section } from '../Section'; -import { downloadCsvAs } from '../../../../../../client/lib/download'; - -const TableSection = () => { - const t = useTranslation(); - - const periodOptions = useMemo(() => [ - ['last 7 days', t('Last_7_days')], - ['last 30 days', t('Last_30_days')], - ['last 90 days', t('Last_90_days')], - ], [t]); - - const [periodId, setPeriodId] = useState('last 7 days'); - - const period = useMemo(() => { - switch (periodId) { - case 'last 7 days': - return { - start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(7, 'days'), - end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), - }; - - case 'last 30 days': - return { - start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(30, 'days'), - end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), - }; - - case 'last 90 days': - return { - start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(90, 'days'), - end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), - }; - } - }, [periodId]); - - const handlePeriodChange = (periodId) => setPeriodId(periodId); - - const [current, setCurrent] = useState(0); - const [itemsPerPage, setItemsPerPage] = useState(25); - - const params = useMemo(() => ({ - start: period.start.toISOString(), - end: period.end.toISOString(), - offset: current, - count: itemsPerPage, - }), [period, current, itemsPerPage]); - - const { value: data } = useEndpointData('engagement-dashboard/channels/list', params); - - const channels = useMemo(() => { - if (!data) { - return; - } - - return data.channels.map(({ - room: { t, name, usernames, ts, _updatedAt }, - messages, - diffFromLastWeek, - }) => ({ - t, - name: name || usernames.join(' × '), - createdAt: ts, - updatedAt: _updatedAt, - messagesCount: messages, - messagesVariation: diffFromLastWeek, - })); - }, [data]); - - const downloadData = () => { - const data = [ - ['Room type', 'Name', 'Messages', 'Last Update Date', 'Creation Date'], - ...channels.map(({ - createdAt, - messagesCount, - name, - t, - updatedAt, - }) => [t, name, messagesCount, updatedAt, createdAt]), - ]; - downloadCsvAs(data, `Channels_start_${ params.start }_end_${ params.end }`); - }; - - return
    - - - {t('Users')} - {t('Messages')} - {t('Channels')} - - - - {(tab === 'users' && ) - || (tab === 'messages' && ) - || (tab === 'channels' && )} - - - ; -}; diff --git a/ee/app/engagement-dashboard/client/components/EngagementDashboardPage.stories.js b/ee/app/engagement-dashboard/client/components/EngagementDashboardPage.stories.js deleted file mode 100644 index f8b0af335c0e..000000000000 --- a/ee/app/engagement-dashboard/client/components/EngagementDashboardPage.stories.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -import { EngagementDashboardPage } from './EngagementDashboardPage'; - -export default { - title: 'admin/enterprise/engagement/EngagementDashboardPage', - component: EngagementDashboardPage, - decorators: [(fn) =>
    ], -}; - -export const _default = () => ; diff --git a/ee/app/engagement-dashboard/client/components/EngagementDashboardRoute.js b/ee/app/engagement-dashboard/client/components/EngagementDashboardRoute.js deleted file mode 100644 index 3122ea19a3ef..000000000000 --- a/ee/app/engagement-dashboard/client/components/EngagementDashboardRoute.js +++ /dev/null @@ -1,26 +0,0 @@ -import React, { useEffect } from 'react'; - -import { useCurrentRoute, useRoute } from '../../../../../client/contexts/RouterContext'; -import { EngagementDashboardPage } from './EngagementDashboardPage'; - -export function EngagementDashboardRoute() { - const engagementDashboardRoute = useRoute('engagement-dashboard'); - const [routeName, { tab }] = useCurrentRoute(); - - useEffect(() => { - if (routeName !== 'engagement-dashboard') { - return; - } - - if (!tab) { - engagementDashboardRoute.replace({ tab: 'users' }); - } - }, [routeName, engagementDashboardRoute, tab]); - - return engagementDashboardRoute.push({ tab })} - />; -} - -export default EngagementDashboardRoute; diff --git a/ee/app/engagement-dashboard/client/components/MessagesTab/MessagesPerChannelSection.js b/ee/app/engagement-dashboard/client/components/MessagesTab/MessagesPerChannelSection.js deleted file mode 100644 index ca54427e1d16..000000000000 --- a/ee/app/engagement-dashboard/client/components/MessagesTab/MessagesPerChannelSection.js +++ /dev/null @@ -1,236 +0,0 @@ -import { ResponsivePie } from '@nivo/pie'; -import { Box, Flex, Icon, Margins, Select, Skeleton, Table, Tile, ActionButton } from '@rocket.chat/fuselage'; -import moment from 'moment'; -import React, { useMemo, useState } from 'react'; - -import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; -import { useEndpointData } from '../../../../../../client/hooks/useEndpointData'; -import { LegendSymbol } from '../data/LegendSymbol'; -import { Section } from '../Section'; -import { downloadCsvAs } from '../../../../../../client/lib/download'; - -const MessagesPerChannelSection = () => { - const t = useTranslation(); - - const periodOptions = useMemo(() => [ - ['last 7 days', t('Last_7_days')], - ['last 30 days', t('Last_30_days')], - ['last 90 days', t('Last_90_days')], - ], [t]); - - const [periodId, setPeriodId] = useState('last 7 days'); - - const period = useMemo(() => { - switch (periodId) { - case 'last 7 days': - return { - start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(7, 'days'), - end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), - }; - - case 'last 30 days': - return { - start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(30, 'days'), - end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), - }; - - case 'last 90 days': - return { - start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(90, 'days'), - end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), - }; - } - }, [periodId]); - - const handlePeriodChange = (periodId) => setPeriodId(periodId); - - const params = useMemo(() => ({ - start: period.start.toISOString(), - end: period.end.toISOString(), - }), [period]); - - const { value: pieData } = useEndpointData('engagement-dashboard/messages/origin', params); - const { value: tableData } = useEndpointData('engagement-dashboard/messages/top-five-popular-channels', params); - - const [pie, table] = useMemo(() => { - if (!pieData || !tableData) { - return []; - } - - const pie = pieData.origins.reduce((obj, { messages, t }) => ({ ...obj, [t]: messages }), {}); - - const table = tableData.channels.reduce((entries, { t, messages, name, usernames }, i) => - [...entries, { i, t, name: name || usernames.join(' × '), messages }], []); - - return [pie, table]; - }, [pieData, tableData]); - - const downloadData = () => { - const data = [ - ['Room Type', 'Messages'], - ...pieData.origins.map(({ t, messages }) => [t, messages]), - ]; - downloadCsvAs(data, `MessagesPerChannelSection_start_${ params.start }_end_${ params.end }`); - }; - - - return
    } - > - , - variation: data ? variatonFromPeriod : 0, - description: periodOptions.find(([id]) => id === periodId)[1], - }, - { - count: data ? countFromYesterday : , - variation: data ? variationFromYesterday : 0, - description: t('Yesterday'), - }, - ]} - /> - - {data - ? - - - - moment(date).format('dddd'), - }) || null } - axisLeft={null} - animate={true} - motionStiffness={90} - motionDamping={15} - theme={{ - // TODO: Get it from theme - axis: { - ticks: { - text: { - fill: '#9EA2A8', - fontFamily: 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', - fontSize: '10px', - fontStyle: 'normal', - fontWeight: '600', - letterSpacing: '0.2px', - lineHeight: '12px', - }, - }, - }, - tooltip: { - container: { - backgroundColor: '#1F2329', - boxShadow: '0px 0px 12px rgba(47, 52, 61, 0.12), 0px 0px 2px rgba(47, 52, 61, 0.08)', - borderRadius: 2, - }, - }, - }} - tooltip={({ value }) => - {t('Value_messages', { value })} - } - /> - - - - - : } - -
    ; -}; - -export default MessagesSentSection; diff --git a/ee/app/engagement-dashboard/client/components/MessagesTab/MessagesTab.stories.js b/ee/app/engagement-dashboard/client/components/MessagesTab/MessagesTab.stories.js deleted file mode 100644 index aa3e886b3e43..000000000000 --- a/ee/app/engagement-dashboard/client/components/MessagesTab/MessagesTab.stories.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Margins } from '@rocket.chat/fuselage'; -import React from 'react'; - -import MessagesTab from '.'; - -export default { - title: 'admin/enterprise/engagement/MessagesTab', - component: MessagesTab, - decorators: [ - (fn) => , - ], -}; - -export const _default = () => ; diff --git a/ee/app/engagement-dashboard/client/components/Section.js b/ee/app/engagement-dashboard/client/components/Section.js deleted file mode 100644 index 754ec689b2b0..000000000000 --- a/ee/app/engagement-dashboard/client/components/Section.js +++ /dev/null @@ -1,22 +0,0 @@ -import { Box, Flex, InputBox, Margins } from '@rocket.chat/fuselage'; -import React from 'react'; - -export function Section({ - children, - title, - filter = , -}) { - return - - - {title} - {filter && - - {filter} - - } - - {children} - - ; -} diff --git a/ee/app/engagement-dashboard/client/components/UsersTab/ActiveUsersSection.js b/ee/app/engagement-dashboard/client/components/UsersTab/ActiveUsersSection.js deleted file mode 100644 index ac5f42c33e64..000000000000 --- a/ee/app/engagement-dashboard/client/components/UsersTab/ActiveUsersSection.js +++ /dev/null @@ -1,259 +0,0 @@ -import { ResponsiveLine } from '@nivo/line'; -import { Box, Flex, Skeleton, Tile, ActionButton } from '@rocket.chat/fuselage'; -import moment from 'moment'; -import React, { useMemo } from 'react'; - -import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; -import { useEndpointData } from '../../../../../../client/hooks/useEndpointData'; -import { useFormatDate } from '../../../../../../client/hooks/useFormatDate'; -import CounterSet from '../../../../../../client/components/data/CounterSet'; -import { LegendSymbol } from '../data/LegendSymbol'; -import { Section } from '../Section'; -import { downloadCsvAs } from '../../../../../../client/lib/download'; - -const ActiveUsersSection = ({ timezone }) => { - const t = useTranslation(); - const utc = timezone === 'utc'; - const formatDate = useFormatDate(); - const period = useMemo(() => ({ - start: utc - ? moment.utc().subtract(30, 'days') - : moment().subtract(30, 'days'), - end: utc - ? moment.utc().subtract(1, 'days') - : moment().subtract(1, 'days'), - }), [utc]); - - const params = useMemo(() => ({ - start: period.start.clone().subtract(29, 'days').toISOString(), - end: period.end.toISOString(), - }), [period]); - - const { value: data } = useEndpointData('engagement-dashboard/users/active-users', useMemo(() => params, [params])); - - const [ - countDailyActiveUsers, - diffDailyActiveUsers, - countWeeklyActiveUsers, - diffWeeklyActiveUsers, - countMonthlyActiveUsers, - diffMonthlyActiveUsers, - dauValues, - wauValues, - mauValues, - ] = useMemo(() => { - if (!data) { - return []; - } - - const createPoint = (i) => ({ - x: moment(period.start).add(i, 'days').toDate(), - y: 0, - }); - - const createPoints = () => Array.from({ length: moment(period.end).diff(period.start, 'days') + 1 }, (_, i) => createPoint(i)); - - const dauValues = createPoints(); - const prevDauValue = createPoint(-1); - const wauValues = createPoints(); - const prevWauValue = createPoint(-1); - const mauValues = createPoints(); - const prevMauValue = createPoint(-1); - - const usersListsMap = data.month.reduce((map, dayData) => { - const date = utc - ? moment.utc({ year: dayData.year, month: dayData.month - 1, day: dayData.day }).endOf('day') - : moment({ year: dayData.year, month: dayData.month - 1, day: dayData.day }).endOf('day'); - const dateOffset = date.diff(period.start, 'days'); - if (dateOffset >= 0) { - map[dateOffset] = dayData.usersList; - dauValues[dateOffset].y = dayData.users; - } - return map; - }, {}); - - const distributeValueOverPoints = (usersListsMap, dateOffset, T, array) => { - const usersSet = new Set(); - for (let k = dateOffset; T > 0; k--, T--) { - if (usersListsMap[k]) { - usersListsMap[k].forEach((userId) => usersSet.add(userId)); - } - } - array[dateOffset].y = usersSet.size; - }; - - for (let i = 0; i < 30; i++) { - distributeValueOverPoints(usersListsMap, i, 7, wauValues); - distributeValueOverPoints(usersListsMap, i, 30, mauValues); - } - prevWauValue.y = wauValues[28].y; - prevMauValue.y = mauValues[28].y; - prevDauValue.y = dauValues[28].y; - - return [ - dauValues[dauValues.length - 1].y, - dauValues[dauValues.length - 1].y - prevDauValue.y, - wauValues[wauValues.length - 1].y, - wauValues[wauValues.length - 1].y - prevWauValue.y, - mauValues[mauValues.length - 1].y, - mauValues[mauValues.length - 1].y - prevMauValue.y, - dauValues, - wauValues, - mauValues, - ]; - }, [data, period.end, period.start, utc]); - - const downloadData = () => { - const values = []; - - for (let i = 0; i < 30; i++) { - values.push([moment(dauValues[i].x).format('YYYY-MM-DD'), dauValues[i].y, wauValues[i].y, mauValues[i].y]); - } - - const data = [ - ['Date', 'DAU', 'WAU', 'MAU'], - ...values, - ]; - downloadCsvAs(data, `ActiveUsersSection_start_${ params.start }_end_${ params.end }`); - }; - - - return
    }> - , - variation: data ? diffDailyActiveUsers : 0, - description: <> {t('Daily_Active_Users')}, - }, - { - count: data ? countWeeklyActiveUsers : , - variation: data ? diffWeeklyActiveUsers : 0, - description: <> {t('Weekly_Active_Users')}, - }, - { - count: data ? countMonthlyActiveUsers : , - variation: data ? diffMonthlyActiveUsers : 0, - description: <> {t('Monthly_Active_Users')}, - }, - ]} - /> - - {data - ? - - - - moment(date).format(dauValues.length === 7 ? 'dddd' : 'L'), - }} - animate={true} - motionStiffness={90} - motionDamping={15} - theme={{ - // TODO: Get it from theme - axis: { - ticks: { - text: { - fill: '#9EA2A8', - fontFamily: 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', - fontSize: '10px', - fontStyle: 'normal', - fontWeight: '600', - letterSpacing: '0.2px', - lineHeight: '12px', - }, - }, - }, - tooltip: { - container: { - backgroundColor: '#1F2329', - boxShadow: '0px 0px 12px rgba(47, 52, 61, 0.12), 0px 0px 2px rgba(47, 52, 61, 0.08)', - borderRadius: 2, - }, - }, - }} - enableSlices='x' - sliceTooltip={({ slice: { points } }) => - - {formatDate(points[0].data.x)} - {points.map(({ serieId, data: { y: activeUsers } }) => - - {(serieId === 'dau' && t('DAU_value', { value: activeUsers })) - || (serieId === 'wau' && t('WAU_value', { value: activeUsers })) - || (serieId === 'mau' && t('MAU_value', { value: activeUsers }))} - )} - - } - /> - - - - - : } - -
    ; -}; - -export default ActiveUsersSection; diff --git a/ee/app/engagement-dashboard/client/components/UsersTab/BusiestChatTimesSection.js b/ee/app/engagement-dashboard/client/components/UsersTab/BusiestChatTimesSection.js deleted file mode 100644 index 34d22838cf25..000000000000 --- a/ee/app/engagement-dashboard/client/components/UsersTab/BusiestChatTimesSection.js +++ /dev/null @@ -1,261 +0,0 @@ -import { ResponsiveBar } from '@nivo/bar'; -import { Box, Button, Chevron, Flex, Margins, Select, Skeleton } from '@rocket.chat/fuselage'; -import { useBreakpoints } from '@rocket.chat/fuselage-hooks'; -import moment from 'moment'; -import React, { useMemo, useState } from 'react'; - -import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; -import { useEndpointData } from '../../../../../../client/hooks/useEndpointData'; -import { Section } from '../Section'; - -const ContentForHours = ({ displacement, onPreviousDateClick, onNextDateClick, timezone }) => { - const t = useTranslation(); - const isLgScreen = useBreakpoints().includes('lg'); - const utc = timezone === 'utc'; - - const currentDate = useMemo(() => { - if (utc) { - return moment().utc({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1, 'days').subtract(displacement, 'days'); - } - return moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1).subtract(displacement, 'days'); - }, [displacement, utc]); - - const params = useMemo(() => ({ start: currentDate.toISOString() }), [currentDate]); - const { value: data } = useEndpointData('engagement-dashboard/users/chat-busier/hourly-data', useMemo(() => params, [params])); - const values = useMemo(() => { - if (!data) { - return []; - } - - const divider = 2; - const values = Array.from({ length: 24 / divider }, (_, i) => ({ - hour: String(divider * i), - users: 0, - })); - for (const { hour, users } of data.hours) { - const i = Math.floor(hour / divider); - values[i] = values[i] || { hour: String(divider * i), users: 0 }; - values[i].users += users; - } - - return values; - }, [data]); - - return <> - - - - {currentDate.format(displacement < 7 ? 'dddd' : 'L')} - - - - {data - ? - - - moment().set({ hour, minute: 0, second: 0 }).format('LT'), - }} - axisLeft={null} - animate={true} - motionStiffness={90} - motionDamping={15} - theme={{ - // TODO: Get it from theme - axis: { - ticks: { - text: { - fill: '#9EA2A8', - fontFamily: 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', - fontSize: '10px', - fontStyle: 'normal', - fontWeight: '600', - letterSpacing: '0.2px', - lineHeight: '12px', - }, - }, - }, - tooltip: { - backgroundColor: '#1F2329', - boxShadow: '0px 0px 12px rgba(47, 52, 61, 0.12), 0px 0px 2px rgba(47, 52, 61, 0.08)', - borderRadius: 2, - padding: 4, - }, - }} - tooltip={({ value }) => - {t('Value_users', { value })} - } - /> - - - - : } - ; -}; - -const ContentForDays = ({ displacement, onPreviousDateClick, onNextDateClick, timezone }) => { - const utc = timezone === 'utc'; - const currentDate = useMemo(() => { - if (utc) { - return moment.utc().subtract(displacement, 'weeks'); - } - return moment().subtract(displacement, 'weeks'); - }, [displacement, utc]); - const formattedCurrentDate = useMemo(() => { - const startOfWeekDate = currentDate.clone().subtract(6, 'days'); - return `${ startOfWeekDate.format('L') } - ${ currentDate.format('L') }`; - }, [currentDate]); - const params = useMemo(() => ({ start: currentDate.toISOString() }), [currentDate]); - const { value: data } = useEndpointData('engagement-dashboard/users/chat-busier/weekly-data', useMemo(() => params, [params])); - const values = useMemo(() => (data ? data.month.map(({ users, day, month, year }) => ({ - users, - day: String(moment({ year, month: month - 1, day }).valueOf()), - })).sort(({ day: a }, { day: b }) => a - b) : []), [data]); - - return <> - - - - - - - {formattedCurrentDate} - - - - - - - - {data - ? - - - - moment(parseInt(timestamp, 10)).format('L'), - }} - axisLeft={null} - animate={true} - motionStiffness={90} - motionDamping={15} - theme={{ - // TODO: Get it from theme - axis: { - ticks: { - text: { - fill: '#9EA2A8', - fontFamily: 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', - fontSize: '10px', - fontStyle: 'normal', - fontWeight: '600', - letterSpacing: '0.2px', - lineHeight: '12px', - }, - }, - }, - }} - /> - - - - - : } - - ; -}; - -const BusiestChatTimesSection = ({ timezone }) => { - const t = useTranslation(); - - const [timeUnit, setTimeUnit] = useState('hours'); - const timeUnitOptions = useMemo(() => [ - ['hours', t('Hours')], - ['days', t('Days')], - ], [t]); - - const [displacement, setDisplacement] = useState(0); - - const handleTimeUnitChange = (timeUnit) => { - setTimeUnit(timeUnit); - setDisplacement(0); - }; - - const handlePreviousDateClick = () => setDisplacement((displacement) => displacement + 1); - const handleNextDateClick = () => setDisplacement((displacement) => displacement - 1); - - const Content = (timeUnit === 'hours' && ContentForHours) || (timeUnit === 'days' && ContentForDays); - - return
    } - > - -
    ; -}; - -export default BusiestChatTimesSection; diff --git a/ee/app/engagement-dashboard/client/components/UsersTab/NewUsersSection.js b/ee/app/engagement-dashboard/client/components/UsersTab/NewUsersSection.js deleted file mode 100644 index 76c76390fecb..000000000000 --- a/ee/app/engagement-dashboard/client/components/UsersTab/NewUsersSection.js +++ /dev/null @@ -1,234 +0,0 @@ -import { ResponsiveBar } from '@nivo/bar'; -import { Box, Flex, Select, Skeleton, ActionButton } from '@rocket.chat/fuselage'; -import { useResizeObserver } from '@rocket.chat/fuselage-hooks'; -import moment from 'moment'; -import React, { useMemo, useState } from 'react'; - -import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; -import { useEndpointData } from '../../../../../../client/hooks/useEndpointData'; -import { useFormatDate } from '../../../../../../client/hooks/useFormatDate'; -import CounterSet from '../../../../../../client/components/data/CounterSet'; -import { Section } from '../Section'; -import { downloadCsvAs } from '../../../../../../client/lib/download'; - -const TICK_WIDTH = 45; - -const NewUsersSection = ({ timezone }) => { - const t = useTranslation(); - const utc = timezone === 'utc'; - - const periodOptions = useMemo(() => [ - ['last 7 days', t('Last_7_days')], - ['last 30 days', t('Last_30_days')], - ['last 90 days', t('Last_90_days')], - ], [t]); - - const [periodId, setPeriodId] = useState('last 7 days'); - - const formatDate = useFormatDate(); - - const period = useMemo(() => { - switch (periodId) { - case 'last 7 days': - return { - start: utc - ? moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(7, 'days').utc() - : moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(7, 'days'), - end: utc - ? moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1, 'days').utc() - : moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1, 'days'), - }; - - case 'last 30 days': - return { - start: utc - ? moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(30, 'days').utc() - : moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(30, 'days'), - end: utc - ? moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1, 'days').utc() - : moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1, 'days'), - }; - - case 'last 90 days': - return { - start: utc - ? moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(90, 'days').utc() - : moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(90, 'days'), - end: utc - ? moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1, 'days').utc() - : moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1, 'days'), - }; - } - }, [periodId, utc]); - - const handlePeriodChange = (periodId) => setPeriodId(periodId); - - const params = useMemo(() => ({ - start: period.start.toISOString(), - end: period.end.toISOString(), - }), [period]); - - const { value: data } = useEndpointData('engagement-dashboard/users/new-users', useMemo(() => params, [params])); - - const { ref: sizeRef, contentBoxSize: { inlineSize = 600 } = {} } = useResizeObserver(); - - const maxTicks = Math.ceil(inlineSize / TICK_WIDTH); - - const tickValues = useMemo(() => { - const arrayLength = moment(period.end).diff(period.start, 'days') + 1; - if (arrayLength <= maxTicks || !maxTicks) { - return null; - } - - const values = Array.from({ length: arrayLength }, (_, i) => moment(period.start).add(i, 'days').format('YYYY-MM-DD')); - - const relation = Math.ceil(values.length / maxTicks); - - return values.reduce((acc, cur, i) => { - if ((i + 1) % relation === 0) { acc = [...acc, cur]; } - return acc; - }, []); - }, [period, maxTicks]); - - const [ - countFromPeriod, - variatonFromPeriod, - countFromYesterday, - variationFromYesterday, - values, - ] = useMemo(() => { - if (!data) { - return []; - } - - const values = Array.from({ length: moment(period.end).diff(period.start, 'days') + 1 }, (_, i) => ({ - date: moment(period.start).add(i, 'days').format('YYYY-MM-DD'), - newUsers: 0, - })); - for (const { day, users } of data.days) { - const i = utc - ? moment(day).utc().diff(period.start, 'days') - : moment(day).diff(period.start, 'days'); - if (i >= 0) { - values[i].newUsers += users; - } - } - - return [ - data.period.count, - data.period.variation, - data.yesterday.count, - data.yesterday.variation, - values, - ]; - }, [data, period, utc]); - - const downloadData = () => { - const data = [ - ['Date', 'New Users'], - ...values.map(({ date, newUsers }) => [date, newUsers]), - ]; - downloadCsvAs(data, `NewUsersSection_start_${ params.start }_end_${ params.end }`); - }; - - return
    {}} - > - {data - ? - - - - (dates.length === 7 ? moment(isoString).format('dddd') : ''), - }} - axisLeft={{ - // TODO: Get it from theme - tickSize: 0, - tickPadding: 4, - tickRotation: 0, - format: (hour) => moment().set({ hour: parseInt(hour, 10), minute: 0, second: 0 }).format('LT'), - }} - hoverTarget='cell' - animate={dates.length <= 7} - motionStiffness={90} - motionDamping={15} - theme={{ - // TODO: Get it from theme - axis: { - ticks: { - text: { - fill: '#9EA2A8', - fontFamily: 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', - fontSize: 10, - fontStyle: 'normal', - fontWeight: '600', - letterSpacing: '0.2px', - lineHeight: '12px', - }, - }, - }, - tooltip: { - container: { - backgroundColor: '#1F2329', - boxShadow: '0px 0px 12px rgba(47, 52, 61, 0.12), 0px 0px 2px rgba(47, 52, 61, 0.08)', - borderRadius: 2, - }, - }, - }} - tooltip={({ value }) => - {t('Value_users', { value })} - } - /> - - - - - : } -
    ; -}; - -export default UsersByTimeOfTheDaySection; diff --git a/ee/app/engagement-dashboard/client/components/UsersTab/UsersTab.Stories.js b/ee/app/engagement-dashboard/client/components/UsersTab/UsersTab.Stories.js deleted file mode 100644 index bf925ca34cf4..000000000000 --- a/ee/app/engagement-dashboard/client/components/UsersTab/UsersTab.Stories.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Margins } from '@rocket.chat/fuselage'; -import React from 'react'; - -import { UsersTab } from '.'; - -export default { - title: 'admin/enterprise/engagement/UsersTab', - component: UsersTab, - decorators: [ - (fn) => , - ], -}; - -export const _default = () => ; diff --git a/ee/app/engagement-dashboard/client/components/UsersTab/index.js b/ee/app/engagement-dashboard/client/components/UsersTab/index.js deleted file mode 100644 index 57be36ba1eee..000000000000 --- a/ee/app/engagement-dashboard/client/components/UsersTab/index.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { useBreakpoints } from '@rocket.chat/fuselage-hooks'; -import { Box, Divider, Flex, Margins } from '@rocket.chat/fuselage'; - -import NewUsersSection from './NewUsersSection'; -import ActiveUsersSection from './ActiveUsersSection'; -import UsersByTimeOfTheDaySection from './UsersByTimeOfTheDaySection'; -import BusiestChatTimesSection from './BusiestChatTimesSection'; - -const UsersTab = ({ timezone }) => { - const isXxlScreen = useBreakpoints().includes('xxl'); - - return <> - - - - - - - - - - - - - - - ; -}; - -export default UsersTab; diff --git a/ee/app/engagement-dashboard/client/components/data/Histogram.js b/ee/app/engagement-dashboard/client/components/data/Histogram.js deleted file mode 100644 index 30653cf05a1b..000000000000 --- a/ee/app/engagement-dashboard/client/components/data/Histogram.js +++ /dev/null @@ -1,85 +0,0 @@ -import { ResponsiveBar } from '@nivo/bar'; -import { Box, Flex } from '@rocket.chat/fuselage'; -import React from 'react'; - -import { polychromaticColors } from './colors'; - -export function Histogram() { - return - - - `${ users }%`, - }} - axisLeft={{ - tickSize: 0, - tickPadding: 5, - tickRotation: 0, - format: (utc) => `UTF ${ utc }`, - }} - animate={true} - motionStiffness={90} - motionDamping={15} - theme={{ - font: 'inherit', - fontStyle: 'normal', - fontWeight: 600, - fontSize: 10, - lineHeight: 12, - letterSpacing: 0.2, - color: '#9EA2A8', - grid: { - line: { - stroke: '#CBCED1', - strokeWidth: 1, - strokeDasharray: '4 1.5', - }, - }, - }} - /> - - - ; -} diff --git a/ee/app/engagement-dashboard/client/components/data/Histogram.stories.js b/ee/app/engagement-dashboard/client/components/data/Histogram.stories.js deleted file mode 100644 index e38e87275b70..000000000000 --- a/ee/app/engagement-dashboard/client/components/data/Histogram.stories.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Box, Flex, Margins } from '@rocket.chat/fuselage'; -import React from 'react'; - -import { Histogram } from './Histogram'; - -export default { - title: 'admin/enterprise/engagement/data/Histogram', - component: Histogram, - decorators: [(fn) => - - - - ], -}; - -export const _default = () => ; diff --git a/ee/app/engagement-dashboard/client/components/data/LegendSymbol.js b/ee/app/engagement-dashboard/client/components/data/LegendSymbol.js deleted file mode 100644 index 947631e97999..000000000000 --- a/ee/app/engagement-dashboard/client/components/data/LegendSymbol.js +++ /dev/null @@ -1,19 +0,0 @@ -import { Box, Margins } from '@rocket.chat/fuselage'; -import React from 'react'; - -export function LegendSymbol({ color = 'currentColor' }) { - return - ; -} diff --git a/ee/app/engagement-dashboard/client/components/data/LegendSymbol.stories.js b/ee/app/engagement-dashboard/client/components/data/LegendSymbol.stories.js deleted file mode 100644 index ca8a466d7b9d..000000000000 --- a/ee/app/engagement-dashboard/client/components/data/LegendSymbol.stories.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Box, Margins } from '@rocket.chat/fuselage'; -import React from 'react'; - -import { LegendSymbol } from './LegendSymbol'; -import { monochromaticColors, polychromaticColors } from './colors'; - -export default { - title: 'admin/enterprise/engagement/data/LegendSymbol', - component: LegendSymbol, - decorators: [(fn) => ], -}; - -export const _default = () => - - Legend text -; - -export const withColor = () => <> - {monochromaticColors.map((color) => - {color} - )} - {polychromaticColors.map((color) => - {color} - )} -; diff --git a/ee/app/engagement-dashboard/client/components/data/colors.js b/ee/app/engagement-dashboard/client/components/data/colors.js deleted file mode 100644 index 9373ef2f3c44..000000000000 --- a/ee/app/engagement-dashboard/client/components/data/colors.js +++ /dev/null @@ -1,2 +0,0 @@ -export const monochromaticColors = ['#E8F2FF', '#D1EBFE', '#A4D3FE', '#76B7FC', '#549DF9', '#1D74F5', '#10529E']; -export const polychromaticColors = ['#FFD031', '#2DE0A5', '#1D74F5']; diff --git a/ee/app/engagement-dashboard/client/index.js b/ee/app/engagement-dashboard/client/index.js deleted file mode 100644 index 86c73e4462ce..000000000000 --- a/ee/app/engagement-dashboard/client/index.js +++ /dev/null @@ -1 +0,0 @@ -import './routes'; diff --git a/ee/app/engagement-dashboard/client/routes.js b/ee/app/engagement-dashboard/client/routes.js deleted file mode 100644 index 87ea50489212..000000000000 --- a/ee/app/engagement-dashboard/client/routes.js +++ /dev/null @@ -1,33 +0,0 @@ -import { hasAllPermission } from '../../../../app/authorization'; -import { registerAdminRoute, registerAdminSidebarItem } from '../../../../client/views/admin'; -import { hasLicense } from '../../license/client'; -import { createTemplateForComponent } from '../../../../client/lib/portals/createTemplateForComponent'; -import { appLayout } from '../../../../client/lib/appLayout'; - -registerAdminRoute('/engagement-dashboard/:tab?', { - name: 'engagement-dashboard', - action: async () => { - const licensed = await hasLicense('engagement-dashboard'); - if (!licensed) { - return; - } - - const EngagementDashboardRoute = createTemplateForComponent('EngagementDashboardRoute', () => import('./components/EngagementDashboardRoute'), { attachment: 'at-parent' }); - appLayout.render('main', { center: EngagementDashboardRoute }); - }, -}); - -hasLicense('engagement-dashboard').then((enabled) => { - if (!enabled) { - return; - } - - registerAdminSidebarItem({ - href: 'engagement-dashboard', - i18nLabel: 'Engagement Dashboard', - icon: 'file-keynote', - permissionGranted: () => hasAllPermission('view-statistics'), - }); -}).catch((error) => { - console.error('Error checking license.', error); -}); diff --git a/ee/app/engagement-dashboard/server/api/channels.js b/ee/app/engagement-dashboard/server/api/channels.js deleted file mode 100644 index e97dd3995df0..000000000000 --- a/ee/app/engagement-dashboard/server/api/channels.js +++ /dev/null @@ -1,26 +0,0 @@ -import { check } from 'meteor/check'; - -import { API } from '../../../../../app/api/server'; -import { findAllChannelsWithNumberOfMessages } from '../lib/channels'; -import { transformDatesForAPI } from './helpers/date'; - -API.v1.addRoute('engagement-dashboard/channels/list', { authRequired: true }, { - get() { - const { start, end } = this.requestParams(); - const { offset, count } = this.getPaginationItems(); - - check(start, String); - check(end, String); - - const { channels, total } = Promise.await(findAllChannelsWithNumberOfMessages({ - ...transformDatesForAPI(start, end), - options: { offset, count }, - })); - return API.v1.success({ - channels, - count: channels.length, - offset, - total, - }); - }, -}); diff --git a/ee/app/engagement-dashboard/server/api/helpers/date.js b/ee/app/engagement-dashboard/server/api/helpers/date.js deleted file mode 100644 index 0aae7b2b0132..000000000000 --- a/ee/app/engagement-dashboard/server/api/helpers/date.js +++ /dev/null @@ -1,14 +0,0 @@ -export const transformDatesForAPI = (start, end) => { - if (isNaN(Date.parse(start))) { - throw new Error('The "start" query parameter must be a valid date.'); - } - if (end && isNaN(Date.parse(end))) { - throw new Error('The "end" query parameter must be a valid date.'); - } - start = new Date(start); - end = new Date(end); - return { - start, - end, - }; -}; diff --git a/ee/app/engagement-dashboard/server/api/messages.js b/ee/app/engagement-dashboard/server/api/messages.js deleted file mode 100644 index 94b6428c011e..000000000000 --- a/ee/app/engagement-dashboard/server/api/messages.js +++ /dev/null @@ -1,41 +0,0 @@ -import { check } from 'meteor/check'; - -import { API } from '../../../../../app/api/server'; -import { findWeeklyMessagesSentData, findMessagesSentOrigin, findTopFivePopularChannelsByMessageSentQuantity } from '../lib/messages'; -import { transformDatesForAPI } from './helpers/date'; - -API.v1.addRoute('engagement-dashboard/messages/messages-sent', { authRequired: true }, { - get() { - const { start, end } = this.requestParams(); - - check(start, String); - check(end, String); - - const data = Promise.await(findWeeklyMessagesSentData(transformDatesForAPI(start, end))); - return API.v1.success(data); - }, -}); - -API.v1.addRoute('engagement-dashboard/messages/origin', { authRequired: true }, { - get() { - const { start, end } = this.requestParams(); - - check(start, String); - check(end, String); - - const data = Promise.await(findMessagesSentOrigin(transformDatesForAPI(start, end))); - return API.v1.success(data); - }, -}); - -API.v1.addRoute('engagement-dashboard/messages/top-five-popular-channels', { authRequired: true }, { - get() { - const { start, end } = this.requestParams(); - - check(start, String); - check(end, String); - - const data = Promise.await(findTopFivePopularChannelsByMessageSentQuantity(transformDatesForAPI(start, end))); - return API.v1.success(data); - }, -}); diff --git a/ee/app/engagement-dashboard/server/api/users.js b/ee/app/engagement-dashboard/server/api/users.js deleted file mode 100644 index 8a675146237a..000000000000 --- a/ee/app/engagement-dashboard/server/api/users.js +++ /dev/null @@ -1,67 +0,0 @@ -import { check } from 'meteor/check'; - -import { API } from '../../../../../app/api/server'; -import { - findWeeklyUsersRegisteredData, - findActiveUsersMonthlyData, - findBusiestsChatsInADayByHours, - findBusiestsChatsWithinAWeek, - findUserSessionsByHourWithinAWeek, -} from '../lib/users'; -import { transformDatesForAPI } from './helpers/date'; - -API.v1.addRoute('engagement-dashboard/users/new-users', { authRequired: true }, { - get() { - const { start, end } = this.requestParams(); - - check(start, String); - check(end, String); - - const data = Promise.await(findWeeklyUsersRegisteredData(transformDatesForAPI(start, end))); - return API.v1.success(data); - }, -}); - -API.v1.addRoute('engagement-dashboard/users/active-users', { authRequired: true }, { - get() { - const { start, end } = this.requestParams(); - - check(start, String); - check(end, String); - - const data = Promise.await(findActiveUsersMonthlyData(transformDatesForAPI(start, end))); - return API.v1.success(data); - }, -}); - -API.v1.addRoute('engagement-dashboard/users/chat-busier/hourly-data', { authRequired: true }, { - get() { - const { start } = this.requestParams(); - - const data = Promise.await(findBusiestsChatsInADayByHours(transformDatesForAPI(start))); - return API.v1.success(data); - }, -}); - -API.v1.addRoute('engagement-dashboard/users/chat-busier/weekly-data', { authRequired: true }, { - get() { - const { start } = this.requestParams(); - - check(start, String); - - const data = Promise.await(findBusiestsChatsWithinAWeek(transformDatesForAPI(start))); - return API.v1.success(data); - }, -}); - -API.v1.addRoute('engagement-dashboard/users/users-by-time-of-the-day-in-a-week', { authRequired: true }, { - get() { - const { start, end } = this.requestParams(); - - check(start, String); - check(end, String); - - const data = Promise.await(findUserSessionsByHourWithinAWeek(transformDatesForAPI(start, end))); - return API.v1.success(data); - }, -}); diff --git a/ee/app/engagement-dashboard/server/index.js b/ee/app/engagement-dashboard/server/index.js deleted file mode 100644 index f87494c93e26..000000000000 --- a/ee/app/engagement-dashboard/server/index.js +++ /dev/null @@ -1,15 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { onLicense } from '../../license/server'; -import { fillFirstDaysOfMessagesIfNeeded } from './lib/messages'; -import { fillFirstDaysOfUsersIfNeeded } from './lib/users'; - -onLicense('engagement-dashboard', async () => { - await import('./listeners'); - await import('./api'); - Meteor.startup(async () => { - const date = new Date(); - fillFirstDaysOfUsersIfNeeded(date); - fillFirstDaysOfMessagesIfNeeded(date); - }); -}); diff --git a/ee/app/engagement-dashboard/server/lib/channels.js b/ee/app/engagement-dashboard/server/lib/channels.js deleted file mode 100644 index 923365e31085..000000000000 --- a/ee/app/engagement-dashboard/server/lib/channels.js +++ /dev/null @@ -1,27 +0,0 @@ -import moment from 'moment'; - -import { Rooms } from '../../../../../app/models/server/raw'; -import { convertDateToInt, diffBetweenDaysInclusive } from './date'; - -export const findAllChannelsWithNumberOfMessages = async ({ start, end, options = {} }) => { - const daysBetweenDates = diffBetweenDaysInclusive(end, start); - const endOfLastWeek = moment(start).clone().subtract(1, 'days').toDate(); - const startOfLastWeek = moment(endOfLastWeek).clone().subtract(daysBetweenDates, 'days').toDate(); - const total = await Rooms.findChannelsWithNumberOfMessagesBetweenDate({ - start: convertDateToInt(start), - end: convertDateToInt(end), - startOfLastWeek: convertDateToInt(startOfLastWeek), - endOfLastWeek: convertDateToInt(endOfLastWeek), - onlyCount: true, - }).toArray(); - return { - channels: await Rooms.findChannelsWithNumberOfMessagesBetweenDate({ - start: convertDateToInt(start), - end: convertDateToInt(end), - startOfLastWeek: convertDateToInt(startOfLastWeek), - endOfLastWeek: convertDateToInt(endOfLastWeek), - options, - }), - total: total.length ? total[0].total : 0, - }; -}; diff --git a/ee/app/engagement-dashboard/server/lib/date.js b/ee/app/engagement-dashboard/server/lib/date.js deleted file mode 100644 index 189c5bd0ff36..000000000000 --- a/ee/app/engagement-dashboard/server/lib/date.js +++ /dev/null @@ -1,11 +0,0 @@ -import moment from 'moment'; - -export const convertDateToInt = (date) => parseInt(moment(date).clone().format('YYYYMMDD')); -export const convertIntToDate = (intValue) => moment(intValue, 'YYYYMMDD').clone().toDate(); -export const diffBetweenDays = (start, end) => moment(new Date(start)).clone().diff(new Date(end), 'days'); -export const diffBetweenDaysInclusive = (start, end) => diffBetweenDays(start, end) + 1; - -export const getTotalOfWeekItems = (weekItems, property) => weekItems.reduce((acc, item) => { - acc += item[property]; - return acc; -}, 0); diff --git a/ee/app/engagement-dashboard/server/lib/users.js b/ee/app/engagement-dashboard/server/lib/users.js deleted file mode 100644 index 69c72c007e06..000000000000 --- a/ee/app/engagement-dashboard/server/lib/users.js +++ /dev/null @@ -1,134 +0,0 @@ -import moment from 'moment'; - -import AnalyticsRaw from '../../../../../app/models/server/raw/Analytics'; -import Sessions from '../../../../../app/models/server/raw/Sessions'; -import { Users } from '../../../../../app/models/server/raw'; -import { Analytics } from '../../../../../app/models/server'; -import { convertDateToInt, diffBetweenDaysInclusive, getTotalOfWeekItems, convertIntToDate } from './date'; - -export const handleUserCreated = (user) => { - if (user.roles?.includes('anonymous')) { - return; - } - - Promise.await(AnalyticsRaw.saveUserData({ - date: convertDateToInt(user.ts), - user, - })); - return user; -}; - -export const fillFirstDaysOfUsersIfNeeded = async (date) => { - const usersFromAnalytics = await AnalyticsRaw.findByTypeBeforeDate({ - type: 'users', - date: convertDateToInt(date), - }).toArray(); - if (!usersFromAnalytics.length) { - const startOfPeriod = moment(date).subtract(90, 'days').toDate(); - const users = await Users.getTotalOfRegisteredUsersByDate({ - start: startOfPeriod, - end: date, - }); - users.forEach((user) => Analytics.insert({ - ...user, - date: parseInt(user.date), - })); - } -}; - -export const findWeeklyUsersRegisteredData = async ({ start, end }) => { - const daysBetweenDates = diffBetweenDaysInclusive(end, start); - const endOfLastWeek = moment(start).clone().subtract(1, 'days').toDate(); - const startOfLastWeek = moment(endOfLastWeek).clone().subtract(daysBetweenDates, 'days').toDate(); - const today = convertDateToInt(end); - const yesterday = convertDateToInt(moment(end).clone().subtract(1, 'days').toDate()); - const currentPeriodUsers = await AnalyticsRaw.getTotalOfRegisteredUsersByDate({ - start: convertDateToInt(start), - end: convertDateToInt(end), - options: { count: daysBetweenDates, sort: { _id: -1 } }, - }); - const lastPeriodUsers = await AnalyticsRaw.getTotalOfRegisteredUsersByDate({ - start: convertDateToInt(startOfLastWeek), - end: convertDateToInt(endOfLastWeek), - options: { count: daysBetweenDates, sort: { _id: -1 } }, - }); - const yesterdayUsers = (currentPeriodUsers.find((item) => item._id === yesterday) || {}).users || 0; - const todayUsers = (currentPeriodUsers.find((item) => item._id === today) || {}).users || 0; - const currentPeriodTotalUsers = getTotalOfWeekItems(currentPeriodUsers, 'users'); - const lastPeriodTotalUsers = getTotalOfWeekItems(lastPeriodUsers, 'users'); - return { - days: currentPeriodUsers.map((day) => ({ day: convertIntToDate(day._id), users: day.users })), - period: { - count: currentPeriodTotalUsers, - variation: currentPeriodTotalUsers - lastPeriodTotalUsers, - }, - yesterday: { - count: yesterdayUsers, - variation: todayUsers - yesterdayUsers, - }, - }; -}; - -export const findActiveUsersMonthlyData = async ({ start, end }) => { - const startOfPeriod = moment(start); - const endOfPeriod = moment(end); - - return { - month: await Sessions.getActiveUsersOfPeriodByDayBetweenDates({ - start: { - year: startOfPeriod.year(), - month: startOfPeriod.month() + 1, - day: startOfPeriod.date(), - }, - end: { - year: endOfPeriod.year(), - month: endOfPeriod.month() + 1, - day: endOfPeriod.date(), - }, - }), - }; -}; - -export const findBusiestsChatsInADayByHours = async ({ start }) => { - const now = moment(start); - const yesterday = moment(now).clone().subtract(24, 'hours'); - return { - hours: await Sessions.getBusiestTimeWithinHoursPeriod({ - start: yesterday.toDate(), - end: now.toDate(), - groupSize: 2, - }), - }; -}; - -export const findBusiestsChatsWithinAWeek = async ({ start }) => { - const today = moment(start); - const startOfCurrentWeek = moment(today).clone().subtract(7, 'days'); - - return { - month: await Sessions.getTotalOfSessionsByDayBetweenDates({ - start: { - year: startOfCurrentWeek.year(), - month: startOfCurrentWeek.month() + 1, - day: startOfCurrentWeek.date(), - }, - end: { - year: today.year(), - month: today.month() + 1, - day: today.date(), - }, - }), - }; -}; - -export const findUserSessionsByHourWithinAWeek = async ({ start, end }) => { - const startOfPeriod = moment(start); - const endOfPeriod = moment(end); - - return { - week: await Sessions.getTotalOfSessionByHourAndDayBetweenDates({ - start: startOfPeriod.toDate(), - end: endOfPeriod.toDate(), - }), - }; -}; diff --git a/ee/app/engagement-dashboard/server/listeners/index.js b/ee/app/engagement-dashboard/server/listeners/index.js deleted file mode 100644 index 0ca660eabe68..000000000000 --- a/ee/app/engagement-dashboard/server/listeners/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import './messages'; -import './users'; diff --git a/ee/app/engagement-dashboard/server/listeners/messages.js b/ee/app/engagement-dashboard/server/listeners/messages.js deleted file mode 100644 index 56af5840abbe..000000000000 --- a/ee/app/engagement-dashboard/server/listeners/messages.js +++ /dev/null @@ -1,5 +0,0 @@ -import { callbacks } from '../../../../../app/callbacks/server'; -import { handleMessagesSent, handleMessagesDeleted } from '../lib/messages'; - -callbacks.add('afterSaveMessage', handleMessagesSent); -callbacks.add('afterDeleteMessage', handleMessagesDeleted); diff --git a/ee/app/engagement-dashboard/server/listeners/users.js b/ee/app/engagement-dashboard/server/listeners/users.js deleted file mode 100644 index b6b730d2d968..000000000000 --- a/ee/app/engagement-dashboard/server/listeners/users.js +++ /dev/null @@ -1,4 +0,0 @@ -import { callbacks } from '../../../../../app/callbacks/server'; -import { handleUserCreated } from '../lib/users'; - -callbacks.add('afterCreateUser', handleUserCreated); diff --git a/ee/app/license/client/index.ts b/ee/app/license/client/index.ts index cb6aff1426ba..744cdb774614 100644 --- a/ee/app/license/client/index.ts +++ b/ee/app/license/client/index.ts @@ -1,17 +1,15 @@ -import { CachedCollectionManager } from '../../../../app/ui-cached-collection'; -import { call } from '../../../../client/lib/utils/call'; +import { fetchFeatures } from '../../../client/lib/fetchFeatures'; +import { queryClient } from '../../../../client/lib/queryClient'; -const allModules = new Promise>((resolve, reject) => { - CachedCollectionManager.onLogin(async () => { - try { - const features: string[] = await call('license:getModules'); - resolve(new Set(features)); - } catch (e) { - console.error('Error getting modules', e); - reject(e); - } +const allModules = queryClient.fetchQuery({ + queryKey: ['ee.features'], + queryFn: fetchFeatures, +}) + .then((features) => new Set(features)) + .catch((e) => { + console.error('Error getting modules', e); + return Promise.reject(e); }); -}); export async function hasLicense(feature: string): Promise { try { diff --git a/ee/app/license/definitions/ILicense.ts b/ee/app/license/definitions/ILicense.ts new file mode 100644 index 000000000000..014912bef1ad --- /dev/null +++ b/ee/app/license/definitions/ILicense.ts @@ -0,0 +1,11 @@ +import { ILicenseTag } from './ILicenseTag'; + +export interface ILicense { + url: string; + expiry: string; + maxActiveUsers: number; + modules: string[]; + maxGuestUsers: number; + maxRoomsPerGuest: number; + tag?: ILicenseTag; +} diff --git a/ee/app/license/definitions/ILicenseTag.ts b/ee/app/license/definitions/ILicenseTag.ts new file mode 100644 index 000000000000..2f11fdebd5db --- /dev/null +++ b/ee/app/license/definitions/ILicenseTag.ts @@ -0,0 +1,4 @@ +export interface ILicenseTag { + name: string; + color: string; +} diff --git a/ee/app/license/server/bundles.ts b/ee/app/license/server/bundles.ts index f793d0888c95..f66ff10a5843 100644 --- a/ee/app/license/server/bundles.ts +++ b/ee/app/license/server/bundles.ts @@ -1,5 +1,18 @@ +export type BundleFeature = + | 'auditing' + | 'canned-responses' + | 'ldap-enterprise' + | 'livechat-enterprise' + | 'omnichannel-mobile-enterprise' + | 'engagement-dashboard' + | 'push-privacy' + | 'scalability' + | 'teams-mention' + | 'saml-enterprise' + | 'oauth-enterprise'; + interface IBundle { - [key: string]: string[]; + [key: string]: BundleFeature[]; } const bundles: IBundle = { diff --git a/ee/app/license/server/license.ts b/ee/app/license/server/license.ts index ad0b962759f9..888279c29c73 100644 --- a/ee/app/license/server/license.ts +++ b/ee/app/license/server/license.ts @@ -1,27 +1,14 @@ import { EventEmitter } from 'events'; import { Users } from '../../../../app/models/server'; -import { getBundleModules, isBundle, getBundleFromModule } from './bundles'; +import { getBundleModules, isBundle, getBundleFromModule, BundleFeature } from './bundles'; import decrypt from './decrypt'; import { getTagColor } from './getTagColor'; +import { ILicense } from '../definitions/ILicense'; +import { ILicenseTag } from '../definitions/ILicenseTag'; const EnterpriseLicenses = new EventEmitter(); -interface ILicenseTag { - name: string; - color: string; -} - -export interface ILicense { - url: string; - expiry: string; - maxActiveUsers: number; - modules: string[]; - maxGuestUsers: number; - maxRoomsPerGuest: number; - tag?: ILicenseTag; -} - export interface IValidLicense { valid?: boolean; license: ILicense; @@ -308,7 +295,7 @@ export function canAddNewUser(): boolean { return License.canAddNewUser(); } -export function onLicense(feature: string, cb: (...args: any[]) => void): void { +export function onLicense(feature: BundleFeature, cb: (...args: any[]) => void): void { if (hasLicense(feature)) { return cb(); } @@ -316,6 +303,63 @@ export function onLicense(feature: string, cb: (...args: any[]) => void): void { EnterpriseLicenses.once(`valid:${ feature }`, cb); } +export function onValidFeature(feature: BundleFeature, cb: () => void): (() => void) { + EnterpriseLicenses.on(`valid:${ feature }`, cb); + + if (hasLicense(feature)) { + cb(); + } + + return (): void => { + EnterpriseLicenses.off(`valid:${ feature }`, cb); + }; +} + +export function onInvalidFeature(feature: BundleFeature, cb: () => void): (() => void) { + EnterpriseLicenses.on(`invalid:${ feature }`, cb); + + if (!hasLicense(feature)) { + cb(); + } + + return (): void => { + EnterpriseLicenses.off(`invalid:${ feature }`, cb); + }; +} + +export function onToggledFeature(feature: BundleFeature, { + up, + down, +}: { + up?: () => void; + down?: () => void; +}): (() => void) { + let enabled = hasLicense(feature); + + const offValidFeature = onValidFeature(feature, () => { + if (!enabled) { + up?.(); + enabled = true; + } + }); + + const offInvalidFeature = onInvalidFeature(feature, () => { + if (enabled) { + down?.(); + enabled = false; + } + }); + + if (enabled) { + up?.(); + } + + return (): void => { + offValidFeature(); + offInvalidFeature(); + }; +} + export function onModule(cb: (...args: any[]) => void): void { EnterpriseLicenses.on('module', cb); } @@ -339,7 +383,7 @@ export interface IOverrideClassProperties { type Class = { new(...args: any[]): any }; -export function overwriteClassOnLicense(license: string, original: Class, overwrite: IOverrideClassProperties): void { +export function overwriteClassOnLicense(license: BundleFeature, original: Class, overwrite: IOverrideClassProperties): void { onLicense(license, () => { Object.entries(overwrite).forEach(([key, value]) => { const originalFn = original.prototype[key]; diff --git a/ee/app/livechat-enterprise/server/api/business-hours.ts b/ee/app/livechat-enterprise/server/api/business-hours.ts index 8044a1509a12..0138efc1d55a 100644 --- a/ee/app/livechat-enterprise/server/api/business-hours.ts +++ b/ee/app/livechat-enterprise/server/api/business-hours.ts @@ -1,23 +1,19 @@ -import { Promise } from 'meteor/promise'; - import { API } from '../../../../../app/api/server'; import { findBusinessHours } from '../business-hour/lib/business-hour'; -// @ts-ignore API.v1.addRoute('livechat/business-hours.list', { authRequired: true }, { - get() { + async get() { const { offset, count } = this.getPaginationItems(); const { sort } = this.parseJsonQuery(); const { name } = this.queryParams; - // @ts-ignore - return API.v1.success(Promise.await(findBusinessHours( + return API.v1.success(await findBusinessHours( this.userId, { offset, count, sort, }, - name))); + name)); }, }); diff --git a/ee/app/livechat-enterprise/server/api/departments.js b/ee/app/livechat-enterprise/server/api/departments.js index 49c9e840d3e9..07c7c5a6c688 100644 --- a/ee/app/livechat-enterprise/server/api/departments.js +++ b/ee/app/livechat-enterprise/server/api/departments.js @@ -344,15 +344,15 @@ API.v1.addRoute('livechat/departments.available-by-unit/:unitId', { authRequired }, }); -API.v1.addRoute('livechat/departments.by-unit/:unitId', { authRequired: true }, { - get() { +API.v1.addRoute('livechat/departments.by-unit/:id', { authRequired: true }, { + async get() { check(this.urlParams, { - unitId: String, + id: String, }); const { offset, count } = this.getPaginationItems(); - const { unitId } = this.urlParams; + const { id } = this.urlParams; - const { departments, total } = Promise.await(findAllDepartmentsByUnit(unitId, offset, count)); + const { departments, total } = await findAllDepartmentsByUnit(id, offset, count); return API.v1.success({ departments, diff --git a/ee/app/livechat-enterprise/server/api/lib/monitors.js b/ee/app/livechat-enterprise/server/api/lib/monitors.js index 6a9132c54508..04642b71bffd 100644 --- a/ee/app/livechat-enterprise/server/api/lib/monitors.js +++ b/ee/app/livechat-enterprise/server/api/lib/monitors.js @@ -17,7 +17,7 @@ export async function findMonitors({ userId, text, pagination: { offset, count, sort: sort || { name: 1 }, skip: offset, limit: count, - fields: { + projection: { username: 1, name: 1, status: 1, @@ -44,7 +44,7 @@ export async function findMonitorByUsername({ userId, username }) { throw new Error('error-not-authorized'); } const user = await Users.findOne({ username }, { - fields: { + projection: { username: 1, name: 1, status: 1, diff --git a/ee/app/livechat-enterprise/server/api/lib/units.js b/ee/app/livechat-enterprise/server/api/lib/units.ts similarity index 52% rename from ee/app/livechat-enterprise/server/api/lib/units.js rename to ee/app/livechat-enterprise/server/api/lib/units.ts index b2838a310b06..ba37e5200e49 100644 --- a/ee/app/livechat-enterprise/server/api/lib/units.js +++ b/ee/app/livechat-enterprise/server/api/lib/units.ts @@ -2,13 +2,28 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; import { hasPermissionAsync } from '../../../../../../app/authorization/server/functions/hasPermission'; import LivechatUnit from '../../../../models/server/models/LivechatUnit'; -import LivechatUnitMonitors from '../../../../models/server/models/LivechatUnitMonitors'; - -export async function findUnits({ userId, text, pagination: { offset, count, sort } }) { +import LivechatUnitMonitors from '../../../../models/server/raw/LivechatUnitMonitors'; +import { IOmnichannelBusinessUnit } from '../../../../../../definition/IOmnichannelBusinessUnit'; +import { ILivechatMonitor } from '../../../../../../definition/ILivechatMonitor'; + +export async function findUnits({ userId, text, pagination: { offset, count, sort } }: { + userId: string; + text?: string; + pagination: { + offset: number; + count: number; + sort: Record; + }; +}): Promise<{ + units: IOmnichannelBusinessUnit[]; + count: number; + offset: number; + total: number; + }> { if (!await hasPermissionAsync(userId, 'manage-livechat-units')) { throw new Error('error-not-authorized'); } - const filter = new RegExp(escapeRegExp(text), 'i'); + const filter = text && new RegExp(escapeRegExp(text), 'i'); const query = { ...text && { $or: [{ name: filter }] } }; @@ -30,18 +45,14 @@ export async function findUnits({ userId, text, pagination: { offset, count, sor }; } -export async function findUnitMonitors({ userId, unitId }) { +export async function findUnitMonitors({ userId, unitId }: { userId: string; unitId: string }): Promise { if (!await hasPermissionAsync(userId, 'manage-livechat-monitors')) { throw new Error('error-not-authorized'); } - const monitors = LivechatUnitMonitors.find({ unitId }).fetch(); - - return { - monitors, - }; + return LivechatUnitMonitors.find({ unitId }).toArray() as Promise; } -export async function findUnitById({ userId, unitId }) { +export async function findUnitById({ userId, unitId }: { userId: string; unitId: string }): Promise { if (!await hasPermissionAsync(userId, 'manage-livechat-units')) { throw new Error('error-not-authorized'); } diff --git a/ee/app/livechat-enterprise/server/api/rooms.ts b/ee/app/livechat-enterprise/server/api/rooms.ts index 317ae751b2c1..c41a2ae8cdfa 100644 --- a/ee/app/livechat-enterprise/server/api/rooms.ts +++ b/ee/app/livechat-enterprise/server/api/rooms.ts @@ -32,6 +32,10 @@ API.v1.addRoute('livechat/room.onHold', { authRequired: true }, { return API.v1.failure('Room is already On-Hold'); } + if (!room.open) { + return API.v1.failure('Room cannot be placed on hold after being closed'); + } + const user = Meteor.user(); if (!user) { return API.v1.failure('Invalid user'); diff --git a/ee/app/livechat-enterprise/server/api/units.js b/ee/app/livechat-enterprise/server/api/units.js deleted file mode 100644 index 616e700f8f75..000000000000 --- a/ee/app/livechat-enterprise/server/api/units.js +++ /dev/null @@ -1,42 +0,0 @@ -import { API } from '../../../../../app/api/server'; -import { findUnits, findUnitById, findUnitMonitors } from './lib/units'; - -API.v1.addRoute('livechat/units.list', { authRequired: true }, { - get() { - const { offset, count } = this.getPaginationItems(); - const { sort } = this.parseJsonQuery(); - const { text } = this.queryParams; - - return API.v1.success(Promise.await(findUnits({ - userId: this.userId, - text, - pagination: { - offset, - count, - sort, - }, - }))); - }, -}); - -API.v1.addRoute('livechat/units.getOne', { authRequired: true }, { - get() { - const { unitId } = this.queryParams; - - return API.v1.success(Promise.await(findUnitById({ - userId: this.userId, - unitId, - }))); - }, -}); - -API.v1.addRoute('livechat/unitMonitors.list', { authRequired: true }, { - get() { - const { unitId } = this.queryParams; - - return API.v1.success(Promise.await(findUnitMonitors({ - userId: this.userId, - unitId, - }))); - }, -}); diff --git a/ee/app/livechat-enterprise/server/api/units.ts b/ee/app/livechat-enterprise/server/api/units.ts new file mode 100644 index 000000000000..9132aaa7cd9e --- /dev/null +++ b/ee/app/livechat-enterprise/server/api/units.ts @@ -0,0 +1,102 @@ +import { API } from '../../../../../app/api/server'; +import { deprecationWarning } from '../../../../../app/api/server/helpers/deprecationWarning'; +import { findUnits, findUnitById, findUnitMonitors } from './lib/units'; +import { LivechatEnterprise } from '../lib/LivechatEnterprise'; + +API.v1.addRoute('livechat/units.list', { authRequired: true }, { + async get() { + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + const { text } = this.queryParams; + + const response = await findUnits({ + userId: this.userId, + text, + pagination: { + offset, + count, + sort, + }, + }); + + return API.v1.success(deprecationWarning({ response, endpoint: 'livechat/units.list' })); + }, +}); + +API.v1.addRoute('livechat/units.getOne', { authRequired: true }, { + async get() { + const { unitId } = this.queryParams; + + if (!unitId) { + return API.v1.failure('Missing "unitId" query parameter'); + } + + const unit = await findUnitById({ + userId: this.userId, + unitId, + }); + + return API.v1.success(deprecationWarning({ response: unit, endpoint: 'livechat/units.getOne' })); + }, +}); + +API.v1.addRoute('livechat/unitMonitors.list', { authRequired: true }, { + async get() { + const { unitId } = this.queryParams; + + if (!unitId) { + return API.v1.failure('The "unitId" parameter is required'); + } + return API.v1.success({ + monitors: await findUnitMonitors({ + userId: this.userId, + unitId, + }), + }); + }, +}); + +API.v1.addRoute('livechat/units', { authRequired: true, permissionsRequired: ['manage-livechat-units'] }, { + async get() { + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + const { text } = this.queryParams; + + return API.v1.success(await findUnits({ + userId: this.userId, + text, + pagination: { + offset, + count, + sort, + }, + })); + }, + async post() { + const { unitData, unitMonitors, unitDepartments } = this.bodyParams; + return LivechatEnterprise.saveUnit(null, unitData, unitMonitors, unitDepartments); + }, +}); + +API.v1.addRoute('livechat/units/:id', { authRequired: true, permissionsRequired: ['manage-livechat-units'] }, { + async get() { + const { id } = this.urlParams; + const unit = await findUnitById({ + userId: this.userId, + unitId: id, + }); + + return API.v1.success(unit); + }, + async post() { + const { unitData, unitMonitors, unitDepartments } = this.bodyParams; + const { id } = this.urlParams; + + return LivechatEnterprise.saveUnit(id, unitData, unitMonitors, unitDepartments); + }, + async delete() { + const { id } = this.urlParams; + + return LivechatEnterprise.removeUnit(id); + }, +}); diff --git a/ee/app/livechat-enterprise/server/business-hour/Custom.ts b/ee/app/livechat-enterprise/server/business-hour/Custom.ts index b5ae8b18a2cc..ebd121712cfe 100644 --- a/ee/app/livechat-enterprise/server/business-hour/Custom.ts +++ b/ee/app/livechat-enterprise/server/business-hour/Custom.ts @@ -48,8 +48,8 @@ class CustomBusinessHour extends AbstractBusinessHourType implements IBusinessHo const businessHourToReturn = { ...businessHourData, departmentsToApplyBusinessHour }; delete businessHourData.departments; const businessHourId = await this.baseSaveBusinessHour(businessHourData); - const currentDepartments = (await this.DepartmentsRepository.findByBusinessHourId(businessHourId, { fields: { _id: 1 } }).toArray()).map((dept: any) => dept._id); - const toRemove = [...currentDepartments.filter((dept: string) => !departments.includes(dept))]; + const currentDepartments = (await this.DepartmentsRepository.findByBusinessHourId(businessHourId, { projection: { _id: 1 } }).toArray()).map((dept) => dept._id); + const toRemove = [...currentDepartments.filter((dept) => !departments.includes(dept))]; const toAdd = [...departments.filter((dept: string) => !currentDepartments.includes(dept))]; await this.removeBusinessHourFromDepartmentsIfNeeded(businessHourId, toRemove); await this.addBusinessHourToDepartmentsIfNeeded(businessHourId, toAdd); @@ -69,8 +69,8 @@ class CustomBusinessHour extends AbstractBusinessHourType implements IBusinessHo } private async removeBusinessHourFromAgents(businessHourId: string): Promise { - const departmentIds = (await this.DepartmentsRepository.findByBusinessHourId(businessHourId, { fields: { _id: 1 } }).toArray()).map((dept: any) => dept._id); - const agentIds = (await this.DepartmentsAgentsRepository.findByDepartmentIds(departmentIds, { fields: { agentId: 1 } }).toArray()).map((dept: any) => dept.agentId); + const departmentIds = (await this.DepartmentsRepository.findByBusinessHourId(businessHourId, { projection: { _id: 1 } }).toArray()).map((dept) => dept._id); + const agentIds = (await this.DepartmentsAgentsRepository.findByDepartmentIds(departmentIds, { projection: { agentId: 1 } }).toArray()).map((dept) => dept.agentId); this.UsersRepository.removeBusinessHourByAgentIds(agentIds, businessHourId); } diff --git a/ee/app/livechat-enterprise/server/business-hour/Helper.ts b/ee/app/livechat-enterprise/server/business-hour/Helper.ts index ef729e8def20..dd8da6293688 100644 --- a/ee/app/livechat-enterprise/server/business-hour/Helper.ts +++ b/ee/app/livechat-enterprise/server/business-hour/Helper.ts @@ -10,7 +10,7 @@ import { import { ILivechatBusinessHour, LivechatBusinessHourTypes } from '../../../../../definition/ILivechatBusinessHour'; const getAllAgentIdsWithoutDepartment = async (): Promise => { - const agentIdsWithDepartment = (await LivechatDepartmentAgents.find({}, { fields: { agentId: 1 } }).toArray()).map((dept: any) => dept.agentId); + const agentIdsWithDepartment = (await LivechatDepartmentAgents.find({}, { projection: { agentId: 1 } }).toArray()).map((dept: any) => dept.agentId); const agentIdsWithoutDepartment = (await Users.findUsersInRolesWithQuery('livechat-agent', { _id: { $nin: agentIdsWithDepartment }, }, { projection: { _id: 1 } }).toArray()).map((user: any) => user._id); diff --git a/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts b/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts index 7c1f668e7648..69070c0d877d 100644 --- a/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts +++ b/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts @@ -42,7 +42,7 @@ export class OmnichannelQueueInactivityMonitorClass { }); this.createIndex(); this.user = Users.findOneById('rocket.cat'); - const language = settings.get('Language') || 'en'; + const language = settings.get('Language') || 'en'; this.message = TAPi18n.__('Closed_automatically_chat_queued_too_long', { lng: language }); this.bindedCloseRoom = Meteor.bindEnvironment(this.closeRoom.bind(this)); } diff --git a/ee/app/livechat-enterprise/server/methods/removeBusinessHour.ts b/ee/app/livechat-enterprise/server/methods/removeBusinessHour.ts index 9498d430e253..76894a1a000e 100644 --- a/ee/app/livechat-enterprise/server/methods/removeBusinessHour.ts +++ b/ee/app/livechat-enterprise/server/methods/removeBusinessHour.ts @@ -1,5 +1,4 @@ import { Meteor } from 'meteor/meteor'; -import { Promise } from 'meteor/promise'; import { hasPermission } from '../../../../../app/authorization/server'; import { businessHourManager } from '../../../../../app/livechat/server/business-hour'; diff --git a/ee/app/livechat-enterprise/server/permissions.js b/ee/app/livechat-enterprise/server/permissions.js deleted file mode 100644 index ef2b44ffd0b7..000000000000 --- a/ee/app/livechat-enterprise/server/permissions.js +++ /dev/null @@ -1,22 +0,0 @@ -import { Permissions, Roles } from '../../../../app/models/server'; - -export const createPermissions = () => { - if (!Permissions) { - return; - } - - const livechatMonitorRole = 'livechat-monitor'; - const livechatManagerRole = 'livechat-manager'; - const adminRole = 'admin'; - - const monitorRole = Roles.findOneById(livechatMonitorRole, { fields: { _id: 1 } }); - if (!monitorRole) { - Roles.createOrUpdate(livechatMonitorRole); - } - - Permissions.create('manage-livechat-units', [adminRole, livechatManagerRole]); - Permissions.create('manage-livechat-monitors', [adminRole, livechatManagerRole]); - Permissions.create('manage-livechat-tags', [adminRole, livechatManagerRole]); - Permissions.create('manage-livechat-priorities', [adminRole, livechatManagerRole]); - Permissions.create('manage-livechat-canned-responses', [adminRole, livechatManagerRole, livechatMonitorRole]); -}; diff --git a/ee/app/livechat-enterprise/server/permissions.ts b/ee/app/livechat-enterprise/server/permissions.ts new file mode 100644 index 000000000000..1b0ef93e0a68 --- /dev/null +++ b/ee/app/livechat-enterprise/server/permissions.ts @@ -0,0 +1,21 @@ + +import { Permissions, Roles } from '../../../../app/models/server/raw'; + +export const createPermissions = async (): Promise => { + const livechatMonitorRole = 'livechat-monitor'; + const livechatManagerRole = 'livechat-manager'; + const adminRole = 'admin'; + + const monitorRole = await Roles.findOneById(livechatMonitorRole, { fields: { _id: 1 } }); + if (!monitorRole) { + await Roles.createOrUpdate(livechatMonitorRole); + } + + await Promise.all([ + Permissions.create('manage-livechat-units', [adminRole, livechatManagerRole]), + Permissions.create('manage-livechat-monitors', [adminRole, livechatManagerRole]), + Permissions.create('manage-livechat-tags', [adminRole, livechatManagerRole]), + Permissions.create('manage-livechat-priorities', [adminRole, livechatManagerRole]), + Permissions.create('manage-livechat-canned-responses', [adminRole, livechatManagerRole, livechatMonitorRole]), + ]); +}; diff --git a/ee/app/teams-mention/server/index.ts b/ee/app/teams-mention/server/index.ts index 61ddf75230ac..362a0ee1b05b 100644 --- a/ee/app/teams-mention/server/index.ts +++ b/ee/app/teams-mention/server/index.ts @@ -1,5 +1,3 @@ -import { Promise } from 'meteor/promise'; - import { onLicense } from '../../license/server'; import { overwriteClassOnLicense } from '../../license/server/license'; import { SpotlightEnterprise } from './EESpotlight'; diff --git a/ee/client/.eslintrc.js b/ee/client/.eslintrc.js deleted file mode 100644 index 4d6bf74449b7..000000000000 --- a/ee/client/.eslintrc.js +++ /dev/null @@ -1,137 +0,0 @@ -module.exports = { - root: true, - extends: ['@rocket.chat/eslint-config', 'prettier'], - parser: 'babel-eslint', - plugins: ['react', 'react-hooks', 'prettier'], - rules: { - 'import/named': 'error', - 'import/order': [ - 'error', - { - 'newlines-between': 'always', - 'groups': ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']], - 'alphabetize': { - order: 'asc', - }, - }, - ], - 'jsx-quotes': ['error', 'prefer-single'], - 'new-cap': [ - 'error', - { capIsNewExceptions: ['HTML.Comment', 'HTML.Raw', 'HTML.DIV', 'SHA256'] }, - ], - 'prefer-arrow-callback': ['error', { allowNamedFunctions: true }], - 'prettier/prettier': 2, - 'react/display-name': 'error', - 'react/jsx-uses-react': 'error', - 'react/jsx-uses-vars': 'error', - 'react/jsx-no-undef': 'error', - 'react/jsx-fragments': ['error', 'syntax'], - 'react/no-multi-comp': 'error', - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': [ - 'warn', - { - additionalHooks: '(useComponentDidUpdate)', - }, - ], - }, - settings: { - 'import/resolver': { - node: { - extensions: ['.js', '.ts', '.tsx'], - }, - }, - 'react': { - version: 'detect', - }, - }, - env: { - browser: true, - es6: true, - }, - overrides: [ - { - files: ['**/*.ts', '**/*.tsx'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:@typescript-eslint/eslint-recommended', - '@rocket.chat/eslint-config', - 'prettier', - ], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'react', 'react-hooks', 'prettier'], - rules: { - '@typescript-eslint/ban-ts-ignore': 'off', - '@typescript-eslint/indent': 'off', - '@typescript-eslint/interface-name-prefix': ['error', 'always'], - '@typescript-eslint/no-extra-parens': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unused-vars': [ - 'error', - { - argsIgnorePattern: '^_', - }, - ], - 'func-call-spacing': 'off', - 'indent': 'off', - 'import/order': [ - 'error', - { - 'newlines-between': 'always', - 'groups': ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']], - 'alphabetize': { - order: 'asc', - }, - }, - ], - 'jsx-quotes': ['error', 'prefer-single'], - 'new-cap': [ - 'error', - { capIsNewExceptions: ['HTML.Comment', 'HTML.Raw', 'HTML.DIV', 'SHA256'] }, - ], - 'no-extra-parens': 'off', - 'no-spaced-func': 'off', - 'no-unused-vars': 'off', - 'no-useless-constructor': 'off', - 'no-use-before-define': 'off', - 'prefer-arrow-callback': ['error', { allowNamedFunctions: true }], - 'prettier/prettier': 2, - 'react/display-name': 'error', - 'react/jsx-uses-react': 'error', - 'react/jsx-uses-vars': 'error', - 'react/jsx-no-undef': 'error', - 'react/jsx-fragments': ['error', 'syntax'], - 'react/no-multi-comp': 'error', - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': [ - 'warn', - { - additionalHooks: '(useComponentDidUpdate)', - }, - ], - }, - env: { - browser: true, - es6: true, - }, - settings: { - 'import/resolver': { - node: { - extensions: ['.js', '.ts', '.tsx'], - }, - }, - 'react': { - version: 'detect', - }, - }, - }, - { - files: ['**/*.stories.js', '**/*.stories.jsx', '**/*.stories.ts', '**/*.stories.tsx'], - rules: { - 'react/display-name': 'off', - 'react/no-multi-comp': 'off', - }, - }, - ], -}; diff --git a/ee/client/.eslintrc.js b/ee/client/.eslintrc.js new file mode 120000 index 000000000000..4ce23c9428e7 --- /dev/null +++ b/ee/client/.eslintrc.js @@ -0,0 +1 @@ +../../client/.eslintrc.js \ No newline at end of file diff --git a/ee/client/.prettierrc b/ee/client/.prettierrc deleted file mode 100644 index 0244eac56844..000000000000 --- a/ee/client/.prettierrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "semi": true, - "bracketSpacing": true, - "arrowParens": "always", - "endOfLine": "lf", - "jsxSingleQuote": true, - "printWidth": 100, - "quoteProps": "consistent", - "singleQuote": true, - "trailingComma": "all", - "useTabs": true -} diff --git a/ee/client/.prettierrc b/ee/client/.prettierrc new file mode 120000 index 000000000000..9d5a1f5613c4 --- /dev/null +++ b/ee/client/.prettierrc @@ -0,0 +1 @@ +../../client/.prettierrc \ No newline at end of file diff --git a/ee/client/audit/AuditPage.stories.js b/ee/client/audit/AuditPage.stories.js index 67bddf5a601c..ce17ec477821 100644 --- a/ee/client/audit/AuditPage.stories.js +++ b/ee/client/audit/AuditPage.stories.js @@ -3,7 +3,7 @@ import React from 'react'; import AuditPage from './AuditPage'; export default { - title: 'ee/Audit', + title: 'auditing/Audit', component: AuditPage, }; diff --git a/ee/client/audit/AuditPageBase.js b/ee/client/audit/AuditPageBase.js index 1cb930867bac..56e81915a140 100644 --- a/ee/client/audit/AuditPageBase.js +++ b/ee/client/audit/AuditPageBase.js @@ -56,7 +56,7 @@ export const AuditPageBase = ({ - {t('Channels')} + {t('Rooms')} {t('Users')} diff --git a/ee/client/audit/RoomAutoComplete/RoomAutoComplete.js b/ee/client/audit/RoomAutoComplete/RoomAutoComplete.js index 1582d020f973..ec3e023da219 100644 --- a/ee/client/audit/RoomAutoComplete/RoomAutoComplete.js +++ b/ee/client/audit/RoomAutoComplete/RoomAutoComplete.js @@ -9,7 +9,7 @@ const query = (name = '') => ({ selector: JSON.stringify({ name }) }); const RoomAutoComplete = (props) => { const [filter, setFilter] = useState(''); const { value: data } = useEndpointData( - 'rooms.autocomplete.channelAndPrivate', + 'rooms.autocomplete.adminRooms', useMemo(() => query(filter), [filter]), ); const options = useMemo( diff --git a/ee/client/contexts/ServerContext/endpoints/v1/engagementDashboard.ts b/ee/client/contexts/ServerContext/endpoints/v1/engagementDashboard.ts deleted file mode 100644 index 1365da57e7f9..000000000000 --- a/ee/client/contexts/ServerContext/endpoints/v1/engagementDashboard.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { IDailyActiveUsers } from '../../../../../../definition/IUser'; -import { Serialized } from '../../../../../../definition/Serialized'; - -export type EngagementDashboardEndpoints = { - 'engagement-dashboard/users/active-users': { - GET: (params: { start: string; end: string }) => { - month: Serialized[]; - }; - }; -}; diff --git a/ee/client/index.ts b/ee/client/index.ts index 6cbeef1d20e1..079dc5ba6984 100644 --- a/ee/client/index.ts +++ b/ee/client/index.ts @@ -1,7 +1,7 @@ import '../app/auditing/client'; import '../app/authorization/client'; import '../app/canned-responses/client'; -import '../app/engagement-dashboard/client'; import '../app/license/client'; import '../app/livechat-enterprise/client'; import './omnichannel'; +import './startup'; diff --git a/ee/client/lib/fetchFeatures.ts b/ee/client/lib/fetchFeatures.ts new file mode 100644 index 000000000000..c03b13cb4d43 --- /dev/null +++ b/ee/client/lib/fetchFeatures.ts @@ -0,0 +1,9 @@ +import { CachedCollectionManager } from '../../../app/ui-cached-collection/client'; +import { call } from '../../../client/lib/utils/call'; + +export const fetchFeatures = (): Promise => + new Promise((resolve, reject) => { + CachedCollectionManager.onLogin(() => { + call('license:getModules').then(resolve, reject); + }); + }); diff --git a/ee/client/lib/getFromRestApi.ts b/ee/client/lib/getFromRestApi.ts new file mode 100644 index 000000000000..ec9f35154112 --- /dev/null +++ b/ee/client/lib/getFromRestApi.ts @@ -0,0 +1,24 @@ +import { APIClient } from '../../../app/utils/client/lib/RestApiClient'; +import { Serialized } from '../../../definition/Serialized'; +import { + MatchPathPattern, + OperationParams, + OperationResult, + PathFor, +} from '../../../definition/rest'; + +export const getFromRestApi = + >(endpoint: TPath) => + async ( + params: void extends OperationParams<'GET', MatchPathPattern> + ? void + : Serialized>>, + ): Promise>>> => { + const response = await APIClient.get(endpoint, params); + + if (typeof response === 'string') { + throw new Error('invalid response data type'); + } + + return response; + }; diff --git a/ee/client/lib/onToggledFeature.ts b/ee/client/lib/onToggledFeature.ts new file mode 100644 index 000000000000..723e1816f28f --- /dev/null +++ b/ee/client/lib/onToggledFeature.ts @@ -0,0 +1,42 @@ +import { QueryObserver } from 'react-query'; + +import { queryClient } from '../../../client/lib/queryClient'; +import type { BundleFeature } from '../../app/license/server/bundles'; +import { fetchFeatures } from './fetchFeatures'; + +export const onToggledFeature = ( + feature: BundleFeature, + { + up, + down, + }: { + up?: () => void; + down?: () => void; + }, +): (() => void) => { + const observer = new QueryObserver(queryClient, { + queryKey: ['ee.features'], + queryFn: fetchFeatures, + }); + + let enabled = false; + + return observer.subscribe((result) => { + if (!result.isSuccess) { + return; + } + + const features = result.data; + const hasFeature = features.includes(feature); + + if (!enabled && hasFeature) { + up?.(); + enabled = true; + } + + if (enabled && !hasFeature) { + down?.(); + enabled = false; + } + }); +}; diff --git a/ee/client/omnichannel/BusinessHoursTable.stories.js b/ee/client/omnichannel/BusinessHoursTable.stories.js index 0301ac69a085..8c9819b4c5e5 100644 --- a/ee/client/omnichannel/BusinessHoursTable.stories.js +++ b/ee/client/omnichannel/BusinessHoursTable.stories.js @@ -4,7 +4,7 @@ import React from 'react'; import BusinessHoursTable from './BusinessHoursTable'; export default { - title: 'omnichannel/businessHours/ee/BusinessHoursTable', + title: 'omnichannel/businessHours/BusinessHoursTable', component: BusinessHoursTable, }; diff --git a/ee/client/omnichannel/additionalForms/BusinessHoursMultiple.stories.js b/ee/client/omnichannel/additionalForms/BusinessHoursMultiple.stories.js index d3425b9b20bd..6325b324e84c 100644 --- a/ee/client/omnichannel/additionalForms/BusinessHoursMultiple.stories.js +++ b/ee/client/omnichannel/additionalForms/BusinessHoursMultiple.stories.js @@ -4,7 +4,7 @@ import React from 'react'; import BusinessHoursMultiple from './BusinessHoursMultiple'; export default { - title: 'omnichannel/businessHours/ee/BusinessHoursMultiple', + title: 'omnichannel/businessHours/BusinessHoursMultiple', component: BusinessHoursMultiple, }; diff --git a/ee/client/omnichannel/additionalForms/BusinessHoursTimeZone.stories.js b/ee/client/omnichannel/additionalForms/BusinessHoursTimeZone.stories.js index a9cd284f4dff..5db113b2fc20 100644 --- a/ee/client/omnichannel/additionalForms/BusinessHoursTimeZone.stories.js +++ b/ee/client/omnichannel/additionalForms/BusinessHoursTimeZone.stories.js @@ -4,7 +4,7 @@ import React from 'react'; import BusinessHoursTimeZone from './BusinessHoursTimeZone'; export default { - title: 'omnichannel/businessHours/ee/BusinessHoursTimeZone', + title: 'omnichannel/businessHours/BusinessHoursTimeZone', component: BusinessHoursTimeZone, }; diff --git a/ee/client/omnichannel/units/UnitEditWithData.js b/ee/client/omnichannel/units/UnitEditWithData.tsx similarity index 86% rename from ee/client/omnichannel/units/UnitEditWithData.js rename to ee/client/omnichannel/units/UnitEditWithData.tsx index 8057160888f2..4c685f4d57f5 100644 --- a/ee/client/omnichannel/units/UnitEditWithData.js +++ b/ee/client/omnichannel/units/UnitEditWithData.tsx @@ -1,5 +1,5 @@ import { Callout } from '@rocket.chat/fuselage'; -import React, { useMemo } from 'react'; +import React, { useMemo, FC } from 'react'; import { FormSkeleton } from '../../../../client/components/Skeleton'; import { useTranslation } from '../../../../client/contexts/TranslationContext'; @@ -7,9 +7,15 @@ import { AsyncStatePhase } from '../../../../client/hooks/useAsyncState'; import { useEndpointData } from '../../../../client/hooks/useEndpointData'; import UnitEdit from './UnitEdit'; -function UnitEditWithData({ unitId, reload, title }) { +const UnitEditWithData: FC<{ + unitId: string; + title: string; + reload: () => void; +}> = function UnitEditWithData({ unitId, reload, title }) { const query = useMemo(() => ({ unitId }), [unitId]); + const { value: data, phase: state, error } = useEndpointData('livechat/units.getOne', query); + const { value: unitMonitors, phase: unitMonitorsState, @@ -44,8 +50,9 @@ function UnitEditWithData({ unitId, reload, title }) { unitMonitors={unitMonitors} unitDepartments={unitDepartments} reload={reload} + isNew={false} /> ); -} +}; export default UnitEditWithData; diff --git a/ee/client/startup/engagementDashboard.ts b/ee/client/startup/engagementDashboard.ts new file mode 100644 index 000000000000..d21bd5abf652 --- /dev/null +++ b/ee/client/startup/engagementDashboard.ts @@ -0,0 +1,33 @@ +import { Meteor } from 'meteor/meteor'; + +import { hasAllPermission } from '../../../app/authorization/client'; +import { + registerAdminRoute, + registerAdminSidebarItem, + unregisterAdminSidebarItem, +} from '../../../client/views/admin'; +import { onToggledFeature } from '../lib/onToggledFeature'; + +const [registerRoute, unregisterRoute] = registerAdminRoute('/engagement-dashboard/:tab?', { + name: 'engagement-dashboard', + lazyRouteComponent: () => import('../views/admin/engagementDashboard/EngagementDashboardRoute'), + ready: false, +}); + +onToggledFeature('engagement-dashboard', { + up: () => + Meteor.startup(() => { + registerAdminSidebarItem({ + href: '/admin/engagement-dashboard', + i18nLabel: 'Engagement Dashboard', + icon: 'file-keynote', + permissionGranted: () => hasAllPermission('view-engagement-dashboard'), + }); + registerRoute(); + }), + down: () => + Meteor.startup(() => { + unregisterAdminSidebarItem('Engagement Dashboard'); + unregisterRoute(); + }), +}); diff --git a/ee/client/startup/index.ts b/ee/client/startup/index.ts new file mode 100644 index 000000000000..546da11f5f46 --- /dev/null +++ b/ee/client/startup/index.ts @@ -0,0 +1 @@ +import './engagementDashboard'; diff --git a/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.stories.tsx b/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.stories.tsx new file mode 100644 index 000000000000..02e5ca19e9e9 --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.stories.tsx @@ -0,0 +1,13 @@ +import { Meta, Story } from '@storybook/react'; +import React, { ReactElement } from 'react'; + +import EngagementDashboardPage from './EngagementDashboardPage'; + +export default { + title: 'admin/engagementDashboard/EngagementDashboardPage', + component: EngagementDashboardPage, + decorators: [(fn): ReactElement =>
    ], +} as Meta; + +export const Default: Story = () => ; +Default.storyName = 'EngagementDashboardPage'; diff --git a/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx b/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx new file mode 100644 index 000000000000..0d7f67af89ac --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx @@ -0,0 +1,66 @@ +import { Box, Select, Tabs } from '@rocket.chat/fuselage'; +import React, { ReactElement, useCallback, useMemo, useState } from 'react'; + +import Page from '../../../../../client/components/Page'; +import { useTranslation } from '../../../../../client/contexts/TranslationContext'; +import ChannelsTab from './channels/ChannelsTab'; +import MessagesTab from './messages/MessagesTab'; +import UsersTab from './users/UsersTab'; + +type EngagementDashboardPageProps = { + tab: 'users' | 'messages' | 'channels'; + onSelectTab?: (tab: 'users' | 'messages' | 'channels') => void; +}; + +const EngagementDashboardPage = ({ + tab = 'users', + onSelectTab, +}: EngagementDashboardPageProps): ReactElement => { + const t = useTranslation(); + + const timezoneOptions = useMemo( + () => [ + ['utc', t('UTC_Timezone')], + ['local', t('Local_Timezone')], + ], + [t], + ); + + const [timezoneId, setTimezoneId] = useState<'utc' | 'local'>('utc'); + const handleTimezoneChange = (timezoneId: string): void => + setTimezoneId(timezoneId as 'utc' | 'local'); + + const handleTabClick = useCallback( + (tab: 'users' | 'messages' | 'channels'): undefined | (() => void) => + onSelectTab ? (): void => onSelectTab(tab) : undefined, + [onSelectTab], + ); + + return ( + + + onChange(value as TPeriod)} + /> + ); +}; + +export default PeriodSelector; diff --git a/ee/client/views/admin/engagementDashboard/data/colors.ts b/ee/client/views/admin/engagementDashboard/data/colors.ts new file mode 100644 index 000000000000..1013778aeca7 --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/data/colors.ts @@ -0,0 +1,14 @@ +import colors from '@rocket.chat/fuselage-tokens/colors.json'; + +export const monochromaticColors = [ + colors.b100, + colors.b200, + colors.b300, + colors.b400, + colors.b500, + colors.b600, + colors.b700, + colors.b800, + colors.b900, +]; +export const polychromaticColors = [colors.y500, colors.g500, colors.b500]; diff --git a/ee/client/views/admin/engagementDashboard/data/periods.ts b/ee/client/views/admin/engagementDashboard/data/periods.ts new file mode 100644 index 000000000000..3b1113833cba --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/data/periods.ts @@ -0,0 +1,76 @@ +import moment from 'moment'; + +import { TranslationKey } from '../../../../../../client/contexts/TranslationContext'; + +const label = ( + translationKey: TranslationKey, + ...replacements: unknown[] +): readonly [translationKey: TranslationKey, ...replacements: unknown[]] => [ + translationKey, + ...replacements, +]; + +const lastNDays = + ( + n: number, + ): ((utc: boolean) => { + start: Date; + end: Date; + }) => + (utc): { start: Date; end: Date } => ({ + start: utc + ? moment.utc().startOf('day').subtract(n, 'days').toDate() + : moment() + .startOf('day') + .subtract(n + 1, 'days') + .toDate(), + end: utc + ? moment.utc().endOf('day').subtract(1, 'days').toDate() + : moment().endOf('day').toDate(), + }); + +export const periods = [ + { + key: 'last 7 days', + label: label('Last_7_days'), + range: lastNDays(7), + }, + { + key: 'last 30 days', + label: label('Last_30_days'), + range: lastNDays(30), + }, + { + key: 'last 90 days', + label: label('Last_90_days'), + range: lastNDays(90), + }, +] as const; + +export type Period = typeof periods[number]; + +export const getPeriod = (key: typeof periods[number]['key']): Period => { + const period = periods.find((period) => period.key === key); + + if (!period) { + throw new Error(`"${key}" is not a valid period key`); + } + + return period; +}; + +export const getPeriodRange = ( + key: typeof periods[number]['key'], + utc = false, +): { + start: Date; + end: Date; +} => { + const period = periods.find((period) => period.key === key); + + if (!period) { + throw new Error(`"${key}" is not a valid period key`); + } + + return period.range(utc); +}; diff --git a/ee/client/views/admin/engagementDashboard/data/usePeriodLabel.ts b/ee/client/views/admin/engagementDashboard/data/usePeriodLabel.ts new file mode 100644 index 000000000000..075b0578844e --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/data/usePeriodLabel.ts @@ -0,0 +1,10 @@ +import { useMemo } from 'react'; + +import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; +import { getPeriod, Period } from './periods'; + +export const usePeriodLabel = (period: Period['key']): string => { + const t = useTranslation(); + + return useMemo(() => t(...getPeriod(period).label), [period, t]); +}; diff --git a/ee/client/views/admin/engagementDashboard/data/usePeriodSelectorState.ts b/ee/client/views/admin/engagementDashboard/data/usePeriodSelectorState.ts new file mode 100644 index 000000000000..35ce960e5833 --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/data/usePeriodSelectorState.ts @@ -0,0 +1,25 @@ +import { useState } from 'react'; + +import { Period } from './periods'; + +export const usePeriodSelectorState = ( + ...periods: TPeriod[] +): [ + period: TPeriod, + periodSelectorProps: { + periods: TPeriod[]; + value: TPeriod; + onChange: (value: TPeriod) => void; + }, +] => { + const [period, setPeriod] = useState(periods[0]); + + return [ + period, + { + periods, + value: period, + onChange: (value): void => setPeriod(value), + }, + ]; +}; diff --git a/ee/client/views/admin/engagementDashboard/messages/MessagesPerChannelSection.tsx b/ee/client/views/admin/engagementDashboard/messages/MessagesPerChannelSection.tsx new file mode 100644 index 000000000000..789a4a5e6648 --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/messages/MessagesPerChannelSection.tsx @@ -0,0 +1,254 @@ +import { ResponsivePie } from '@nivo/pie'; +import { Box, Flex, Icon, Margins, Skeleton, Table, Tile } from '@rocket.chat/fuselage'; +import colors from '@rocket.chat/fuselage-tokens/colors'; +import React, { ReactElement, useMemo } from 'react'; + +import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; +import Section from '../Section'; +import DownloadDataButton from '../data/DownloadDataButton'; +import LegendSymbol from '../data/LegendSymbol'; +import PeriodSelector from '../data/PeriodSelector'; +import { usePeriodSelectorState } from '../data/usePeriodSelectorState'; +import { useMessageOrigins } from './useMessageOrigins'; +import { useTopFivePopularChannels } from './useTopFivePopularChannels'; + +const MessagesPerChannelSection = (): ReactElement => { + const [period, periodSelectorProps] = usePeriodSelectorState( + 'last 7 days', + 'last 30 days', + 'last 90 days', + ); + + const t = useTranslation(); + + const { data: messageOriginsData } = useMessageOrigins({ period }); + const { data: topFivePopularChannelsData } = useTopFivePopularChannels({ period }); + + const pie = useMemo( + () => + messageOriginsData?.origins?.reduce<{ [roomType: string]: number }>( + (obj, { messages, t }) => ({ ...obj, [t]: messages }), + {}, + ), + [messageOriginsData], + ); + + const table = useMemo( + () => + topFivePopularChannelsData?.channels?.reduce< + { + i: number; + t: string; + name?: string; + messages: number; + }[] + >( + (entries, { t, messages, name, usernames }, i) => [ + ...entries, + { i, t, name: name || usernames?.join(' × '), messages }, + ], + [], + ), + [topFivePopularChannelsData], + ); + + return ( +
    + + + messageOriginsData?.origins.map(({ t, messages }) => [t, messages]) + } + /> + + } + > + + + + + + + + {pie ? ( + + + + + + ( + + {t('Value_messages', { value })} + + )} + /> + + + + + + + + + + + {t('Private_Chats')} + + + + {t('Private_Channels')} + + + + {t('Public_Channels')} + + + + + + + ) : ( + + )} + + + + + + + {table ? ( + {t('Most_popular_channels_top_5')} + ) : ( + + )} + + {table && !table.length && ( + + {t('Not_enough_data')} + + )} + {(!table || !!table.length) && ( + + + + {'#'} + {t('Channel')} + {t('Number_of_messages')} + + + + {table && + table.map(({ i, t, name, messages }) => ( + + {i + 1}. + + + {(t === 'd' && ) || + (t === 'c' && ) || + (t === 'p' && )} + + {name} + + {messages} + + ))} + {!table && + Array.from({ length: 5 }, (_, i) => ( + + + + + + + + + + + + ))} + +
    + )} +
    +
    +
    +
    +
    +
    +
    + ); +}; + +export default MessagesPerChannelSection; diff --git a/ee/client/views/admin/engagementDashboard/messages/MessagesSentSection.tsx b/ee/client/views/admin/engagementDashboard/messages/MessagesSentSection.tsx new file mode 100644 index 000000000000..a753a40327d2 --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/messages/MessagesSentSection.tsx @@ -0,0 +1,177 @@ +import { ResponsiveBar } from '@nivo/bar'; +import { Box, Flex, Skeleton } from '@rocket.chat/fuselage'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; +import moment from 'moment'; +import React, { ReactElement, useMemo } from 'react'; + +import CounterSet from '../../../../../../client/components/data/CounterSet'; +import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; +import Section from '../Section'; +import DownloadDataButton from '../data/DownloadDataButton'; +import PeriodSelector from '../data/PeriodSelector'; +import { usePeriodLabel } from '../data/usePeriodLabel'; +import { usePeriodSelectorState } from '../data/usePeriodSelectorState'; +import { useMessagesSent } from './useMessagesSent'; + +const MessagesSentSection = (): ReactElement => { + const [period, periodSelectorProps] = usePeriodSelectorState( + 'last 7 days', + 'last 30 days', + 'last 90 days', + ); + const periodLabel = usePeriodLabel(period); + + const t = useTranslation(); + + const { data } = useMessagesSent({ period }); + + const [countFromPeriod, variatonFromPeriod, countFromYesterday, variationFromYesterday, values] = + useMemo(() => { + if (!data) { + return []; + } + + const values = Array.from( + { length: moment(data.end).diff(data.start, 'days') + 1 }, + (_, i) => ({ + date: moment(data.start).add(i, 'days').toISOString(), + newMessages: 0, + }), + ); + + for (const { day, messages } of data.days ?? []) { + const i = moment(day).diff(data.start, 'days'); + if (i >= 0) { + values[i].newMessages += messages; + } + } + + return [ + data.period?.count, + data.period?.variation, + data.yesterday?.count, + data.yesterday?.variation, + values, + ]; + }, [data]); + + return ( +
    + + + values?.map(({ date, newMessages }) => [date, newMessages]) + } + /> + + } + > + , + variation: variatonFromPeriod ?? 0, + description: periodLabel, + }, + { + count: countFromYesterday ?? , + variation: variationFromYesterday ?? 0, + description: t('Yesterday'), + }, + ]} + /> + + {values ? ( + + + + + moment(date).format('dddd'), + }) || + null + } + axisLeft={null} + animate={true} + // @ts-ignore + motionStiffness={90} + motionDamping={15} + theme={{ + // TODO: Get it from theme + axis: { + ticks: { + text: { + fill: colors.n600, + fontFamily: + 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', + fontSize: '10px', + fontStyle: 'normal', + fontWeight: 600, + letterSpacing: '0.2px', + lineHeight: '12px', + }, + }, + }, + tooltip: { + container: { + backgroundColor: colors.n900, + boxShadow: + '0px 0px 12px rgba(47, 52, 61, 0.12), 0px 0px 2px rgba(47, 52, 61, 0.08)', + borderRadius: 2, + }, + }, + }} + tooltip={({ value }): ReactElement => ( + + {t('Value_messages', { value })} + + )} + /> + + + + + ) : ( + + )} + +
    + ); +}; + +export default MessagesSentSection; diff --git a/ee/client/views/admin/engagementDashboard/messages/MessagesTab.stories.tsx b/ee/client/views/admin/engagementDashboard/messages/MessagesTab.stories.tsx new file mode 100644 index 000000000000..1ab04dcd6d4b --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/messages/MessagesTab.stories.tsx @@ -0,0 +1,14 @@ +import { Margins } from '@rocket.chat/fuselage'; +import { Meta, Story } from '@storybook/react'; +import React from 'react'; + +import MessagesTab from './MessagesTab'; + +export default { + title: 'admin/engagementDashboard/MessagesTab', + component: MessagesTab, + decorators: [(fn) => ], +} as Meta; + +export const Default: Story = () => ; +Default.storyName = 'MessagesTab'; diff --git a/ee/app/engagement-dashboard/client/components/MessagesTab/index.js b/ee/client/views/admin/engagementDashboard/messages/MessagesTab.tsx similarity index 54% rename from ee/app/engagement-dashboard/client/components/MessagesTab/index.js rename to ee/client/views/admin/engagementDashboard/messages/MessagesTab.tsx index 8c111a61d41d..438ebd5938ba 100644 --- a/ee/app/engagement-dashboard/client/components/MessagesTab/index.js +++ b/ee/client/views/admin/engagementDashboard/messages/MessagesTab.tsx @@ -1,13 +1,15 @@ -import React from 'react'; import { Divider } from '@rocket.chat/fuselage'; +import React, { ReactElement } from 'react'; -import MessagesSentSection from './MessagesSentSection'; import MessagesPerChannelSection from './MessagesPerChannelSection'; +import MessagesSentSection from './MessagesSentSection'; -const MessagesTab = () => <> - - - -; +const MessagesTab = (): ReactElement => ( + <> + + + + +); export default MessagesTab; diff --git a/ee/client/views/admin/engagementDashboard/messages/useMessageOrigins.ts b/ee/client/views/admin/engagementDashboard/messages/useMessageOrigins.ts new file mode 100644 index 000000000000..edb5e9d0eb59 --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/messages/useMessageOrigins.ts @@ -0,0 +1,31 @@ +import { useQuery } from 'react-query'; + +import { getFromRestApi } from '../../../../lib/getFromRestApi'; +import { getPeriodRange, Period } from '../data/periods'; + +type UseMessageOriginsOptions = { period: Period['key'] }; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const useMessageOrigins = ({ period }: UseMessageOriginsOptions) => + useQuery( + ['admin/engagement-dashboard/messages/origins', { period }], + async () => { + const { start, end } = getPeriodRange(period); + + const response = await getFromRestApi('/v1/engagement-dashboard/messages/origin')({ + start: start.toISOString(), + end: end.toISOString(), + }); + + return response + ? { + ...response, + start, + end, + } + : undefined; + }, + { + refetchInterval: 5 * 60 * 1000, + }, + ); diff --git a/ee/client/views/admin/engagementDashboard/messages/useMessagesSent.ts b/ee/client/views/admin/engagementDashboard/messages/useMessagesSent.ts new file mode 100644 index 000000000000..6612109bcd43 --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/messages/useMessagesSent.ts @@ -0,0 +1,31 @@ +import { useQuery } from 'react-query'; + +import { getFromRestApi } from '../../../../lib/getFromRestApi'; +import { getPeriodRange, Period } from '../data/periods'; + +type UseMessagesSentOptions = { period: Period['key'] }; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const useMessagesSent = ({ period }: UseMessagesSentOptions) => + useQuery( + ['admin/engagement-dashboard/messages/messages-sent', { period }], + async () => { + const { start, end } = getPeriodRange(period); + + const response = await getFromRestApi('/v1/engagement-dashboard/messages/messages-sent')({ + start: start.toISOString(), + end: end.toISOString(), + }); + + return response + ? { + ...response, + start, + end, + } + : undefined; + }, + { + refetchInterval: 5 * 60 * 1000, + }, + ); diff --git a/ee/client/views/admin/engagementDashboard/messages/useTopFivePopularChannels.ts b/ee/client/views/admin/engagementDashboard/messages/useTopFivePopularChannels.ts new file mode 100644 index 000000000000..7080b438618a --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/messages/useTopFivePopularChannels.ts @@ -0,0 +1,33 @@ +import { useQuery } from 'react-query'; + +import { getFromRestApi } from '../../../../lib/getFromRestApi'; +import { getPeriodRange, Period } from '../data/periods'; + +type UseTopFivePopularChannelsOptions = { period: Period['key'] }; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const useTopFivePopularChannels = ({ period }: UseTopFivePopularChannelsOptions) => + useQuery( + ['admin/engagement-dashboard/messages/top-five-popular-channels', { period }], + async () => { + const { start, end } = getPeriodRange(period); + + const response = await getFromRestApi( + '/v1/engagement-dashboard/messages/top-five-popular-channels', + )({ + start: start.toISOString(), + end: end.toISOString(), + }); + + return response + ? { + ...response, + start, + end, + } + : undefined; + }, + { + refetchInterval: 5 * 60 * 1000, + }, + ); diff --git a/ee/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx b/ee/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx new file mode 100644 index 000000000000..0723acc792e5 --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx @@ -0,0 +1,291 @@ +import { ResponsiveLine } from '@nivo/line'; +import { Box, Flex, Skeleton, Tile } from '@rocket.chat/fuselage'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; +import moment from 'moment'; +import React, { ReactElement, useMemo } from 'react'; + +import CounterSet from '../../../../../../client/components/data/CounterSet'; +import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; +import { useFormatDate } from '../../../../../../client/hooks/useFormatDate'; +import Section from '../Section'; +import DownloadDataButton from '../data/DownloadDataButton'; +import LegendSymbol from '../data/LegendSymbol'; +import { useActiveUsers } from './useActiveUsers'; + +type ActiveUsersSectionProps = { + timezone: 'utc' | 'local'; +}; + +const ActiveUsersSection = ({ timezone }: ActiveUsersSectionProps): ReactElement => { + const utc = timezone === 'utc'; + const { data } = useActiveUsers({ utc }); + + const [ + countDailyActiveUsers, + diffDailyActiveUsers, + countWeeklyActiveUsers, + diffWeeklyActiveUsers, + countMonthlyActiveUsers, + diffMonthlyActiveUsers, + dauValues = [], + wauValues = [], + mauValues = [], + ] = useMemo(() => { + if (!data) { + return []; + } + + const createPoint = (i: number): { x: Date; y: number } => ({ + x: moment(data.start).add(i, 'days').toDate(), + y: 0, + }); + + const createPoints = (): { x: Date; y: number }[] => + Array.from({ length: moment(data.end).diff(data.start, 'days') + 1 }, (_, i) => + createPoint(i), + ); + + const dauValues = createPoints(); + const prevDauValue = createPoint(-1); + const wauValues = createPoints(); + const prevWauValue = createPoint(-1); + const mauValues = createPoints(); + const prevMauValue = createPoint(-1); + + const usersListsMap = data.month.reduce<{ [x: number]: string[] }>((map, dayData) => { + const date = utc + ? moment + .utc({ year: dayData.year, month: dayData.month - 1, day: dayData.day }) + .endOf('day') + : moment({ year: dayData.year, month: dayData.month - 1, day: dayData.day }).endOf('day'); + const dateOffset = date.diff(data.start, 'days'); + if (dateOffset >= 0) { + map[dateOffset] = dayData.usersList; + dauValues[dateOffset].y = dayData.users; + } + return map; + }, {}); + + const distributeValueOverPoints = ( + usersListsMap: { [x: number]: string[] }, + dateOffset: number, + T: number, + array: { x: Date; y: number }[], + ): void => { + const usersSet = new Set(); + for (let k = dateOffset; T > 0; k--, T--) { + if (usersListsMap[k]) { + usersListsMap[k].forEach((userId) => usersSet.add(userId)); + } + } + array[dateOffset].y = usersSet.size; + }; + + for (let i = 0; i < 30; i++) { + distributeValueOverPoints(usersListsMap, i, 7, wauValues); + distributeValueOverPoints(usersListsMap, i, 30, mauValues); + } + prevWauValue.y = wauValues[28].y; + prevMauValue.y = mauValues[28].y; + prevDauValue.y = dauValues[28].y; + + return [ + dauValues[dauValues.length - 1].y, + dauValues[dauValues.length - 1].y - prevDauValue.y, + wauValues[wauValues.length - 1].y, + wauValues[wauValues.length - 1].y - prevWauValue.y, + mauValues[mauValues.length - 1].y, + mauValues[mauValues.length - 1].y - prevMauValue.y, + dauValues, + wauValues, + mauValues, + ]; + }, [data, utc]); + + const formatDate = useFormatDate(); + const t = useTranslation(); + + return ( +
    { + const values = []; + + for (let i = 0; i < 30; i++) { + values.push([ + dauValues[i].x.toISOString(), + dauValues[i].y, + wauValues[i].y, + mauValues[i].y, + ]); + } + + return values; + }} + /> + } + > + , + variation: diffDailyActiveUsers ?? 0, + description: ( + <> + {t('Daily_Active_Users')} + + ), + }, + { + count: countWeeklyActiveUsers ?? , + variation: diffWeeklyActiveUsers ?? 0, + description: ( + <> + {t('Weekly_Active_Users')} + + ), + }, + { + count: countMonthlyActiveUsers ?? , + variation: diffMonthlyActiveUsers ?? 0, + description: ( + <> + {t('Monthly_Active_Users')} + + ), + }, + ]} + /> + + {data ? ( + + + + + + moment(date).format(dauValues.length === 7 ? 'dddd' : 'L'), + }} + animate={true} + motionStiffness={90} + motionDamping={15} + theme={{ + // TODO: Get it from theme + axis: { + ticks: { + text: { + fill: '#9EA2A8', + fontFamily: + 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', + fontSize: '10px', + fontStyle: 'normal', + fontWeight: 600, + letterSpacing: '0.2px', + lineHeight: '12px', + }, + }, + }, + tooltip: { + container: { + backgroundColor: '#1F2329', + boxShadow: + '0px 0px 12px rgba(47, 52, 61, 0.12), 0px 0px 2px rgba(47, 52, 61, 0.08)', + borderRadius: 2, + }, + }, + }} + enableSlices='x' + sliceTooltip={({ slice: { points } }): ReactElement => ( + + + {formatDate(points[0].data.x)} + {points.map(({ serieId, data: { y: activeUsers } }) => ( + + + {(serieId === 'dau' && t('DAU_value', { value: activeUsers })) || + (serieId === 'wau' && t('WAU_value', { value: activeUsers })) || + (serieId === 'mau' && t('MAU_value', { value: activeUsers }))} + + + ))} + + + )} + /> + + + + + ) : ( + + )} + +
    + ); +}; + +export default ActiveUsersSection; diff --git a/ee/client/views/admin/engagementDashboard/users/BusiestChatTimesSection.tsx b/ee/client/views/admin/engagementDashboard/users/BusiestChatTimesSection.tsx new file mode 100644 index 000000000000..839f0cfd6e97 --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/users/BusiestChatTimesSection.tsx @@ -0,0 +1,59 @@ +import { Select } from '@rocket.chat/fuselage'; +import React, { ReactElement, useMemo, useState } from 'react'; + +import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; +import Section from '../Section'; +import ContentForDays from './ContentForDays'; +import ContentForHours from './ContentForHours'; + +type TimeUnit = 'hours' | 'days'; + +type BusiestChatTimesSectionProps = { + timezone: 'utc' | 'local'; +}; + +const BusiestChatTimesSection = ({ timezone }: BusiestChatTimesSectionProps): ReactElement => { + const t = useTranslation(); + + const [timeUnit, setTimeUnit] = useState('hours'); + const timeUnitOptions = useMemo( + () => [ + ['hours', t('Hours')], + ['days', t('Days')], + ], + [t], + ); + + const [displacement, setDisplacement] = useState(0); + + const handleTimeUnitChange = (timeUnit: string): void => { + setTimeUnit(timeUnit as TimeUnit); + setDisplacement(0); + }; + + const handlePreviousDateClick = (): void => setDisplacement((displacement) => displacement + 1); + const handleNextDateClick = (): void => setDisplacement((displacement) => displacement - 1); + + const Content = ( + { + hours: ContentForHours, + days: ContentForDays, + } as const + )[timeUnit]; + + return ( +
    } + > + +
    + ); +}; + +export default BusiestChatTimesSection; diff --git a/ee/client/views/admin/engagementDashboard/users/ContentForDays.tsx b/ee/client/views/admin/engagementDashboard/users/ContentForDays.tsx new file mode 100644 index 000000000000..296d5661de01 --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/users/ContentForDays.tsx @@ -0,0 +1,139 @@ +import { ResponsiveBar } from '@nivo/bar'; +import { Box, Button, Chevron, Flex, Margins, Skeleton } from '@rocket.chat/fuselage'; +import colors from '@rocket.chat/fuselage-tokens/colors'; +import moment from 'moment'; +import React, { ReactElement, useMemo } from 'react'; + +import { useWeeklyChatActivity } from './useWeeklyChatActivity'; + +type ContentForDaysProps = { + displacement: number; + onPreviousDateClick: () => void; + onNextDateClick: () => void; + timezone: 'utc' | 'local'; +}; + +const ContentForDays = ({ + displacement, + onPreviousDateClick, + onNextDateClick, + timezone, +}: ContentForDaysProps): ReactElement => { + const utc = timezone === 'utc'; + const { data } = useWeeklyChatActivity({ displacement, utc }); + + const formattedCurrentDate = useMemo(() => { + if (!data) { + return null; + } + + const endOfWeek = moment(data.day); + const startOfWeek = moment(data.day).subtract(6, 'days'); + return `${startOfWeek.format('L')} - ${endOfWeek.format('L')}`; + }, [data]); + + const values = useMemo( + () => + data?.month + ?.map(({ users, day, month, year }) => ({ + users, + day: moment({ year, month: month - 1, day }), + })) + ?.sort(({ day: a }, { day: b }) => a.diff(b)) + ?.map(({ users, day }) => ({ users, day: String(day.valueOf()) })) ?? [], + [data], + ); + + return ( + <> + + + + + + + {formattedCurrentDate} + + + + + + + + {data ? ( + + + + + moment(parseInt(timestamp, 10)).format('L'), + }} + axisLeft={null} + animate={true} + // @ts-ignore + motionStiffness={90} + motionDamping={15} + theme={{ + // TODO: Get it from theme + axis: { + ticks: { + text: { + fill: colors.n600, + fontFamily: + 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', + fontSize: '10px', + fontStyle: 'normal', + fontWeight: 600, + letterSpacing: '0.2px', + lineHeight: '12px', + }, + }, + }, + }} + /> + + + + + ) : ( + + )} + + + ); +}; + +export default ContentForDays; diff --git a/ee/client/views/admin/engagementDashboard/users/ContentForHours.tsx b/ee/client/views/admin/engagementDashboard/users/ContentForHours.tsx new file mode 100644 index 000000000000..28b259947655 --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/users/ContentForHours.tsx @@ -0,0 +1,140 @@ +import { ResponsiveBar } from '@nivo/bar'; +import { Box, Button, Chevron, Skeleton } from '@rocket.chat/fuselage'; +import { useBreakpoints } from '@rocket.chat/fuselage-hooks'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; +import moment from 'moment'; +import React, { ReactElement, useMemo } from 'react'; + +import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; +import { useHourlyChatActivity } from './useHourlyChatActivity'; + +type ContentForHoursProps = { + displacement: number; + onPreviousDateClick: () => void; + onNextDateClick: () => void; + timezone: 'utc' | 'local'; +}; + +const ContentForHours = ({ + displacement, + onPreviousDateClick, + onNextDateClick, + timezone, +}: ContentForHoursProps): ReactElement => { + const utc = timezone === 'utc'; + const { data } = useHourlyChatActivity({ displacement, utc }); + + const t = useTranslation(); + const isLgScreen = useBreakpoints().includes('lg'); + + const values = useMemo(() => { + if (!data) { + return []; + } + + const divider = 2; + const values = Array.from({ length: 24 / divider }, (_, i) => ({ + hour: String(divider * i), + users: 0, + })); + + for (const { hour, users } of data?.hours ?? []) { + const i = Math.floor(hour / divider); + values[i] = values[i] || { hour: String(divider * i), users: 0 }; + values[i].users += users; + } + + return values; + }, [data]); + + return ( + <> + + + + {data ? moment(data.day).format(displacement < 7 ? 'dddd' : 'L') : null} + + + + {data ? ( + + + + + moment().set({ hour, minute: 0, second: 0 }).format('LT'), + }} + axisLeft={null} + animate={true} + motionStiffness={90} + motionDamping={15} + theme={{ + // TODO: Get it from theme + axis: { + ticks: { + text: { + fill: colors.n600, + fontFamily: + 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', + fontSize: '10px', + fontStyle: 'normal', + fontWeight: 600, + letterSpacing: '0.2px', + lineHeight: '12px', + }, + }, + }, + tooltip: { + // @ts-ignore + backgroundColor: colors.n900, + boxShadow: + '0px 0px 12px rgba(47, 52, 61, 0.12), 0px 0px 2px rgba(47, 52, 61, 0.08)', + borderRadius: 2, + padding: 4, + }, + }} + tooltip={({ value }): ReactElement => ( + + {t('Value_users', { value })} + + )} + /> + + + + ) : ( + + )} + + ); +}; + +export default ContentForHours; diff --git a/ee/client/views/admin/engagementDashboard/users/NewUsersSection.tsx b/ee/client/views/admin/engagementDashboard/users/NewUsersSection.tsx new file mode 100644 index 000000000000..c1b4b8d62e13 --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/users/NewUsersSection.tsx @@ -0,0 +1,224 @@ +import { ResponsiveBar } from '@nivo/bar'; +import { Box, Flex, Skeleton } from '@rocket.chat/fuselage'; +import { useResizeObserver } from '@rocket.chat/fuselage-hooks'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; +import moment from 'moment'; +import React, { ReactElement, useMemo } from 'react'; + +import CounterSet from '../../../../../../client/components/data/CounterSet'; +import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; +import { useFormatDate } from '../../../../../../client/hooks/useFormatDate'; +import Section from '../Section'; +import DownloadDataButton from '../data/DownloadDataButton'; +import PeriodSelector from '../data/PeriodSelector'; +import { usePeriodLabel } from '../data/usePeriodLabel'; +import { usePeriodSelectorState } from '../data/usePeriodSelectorState'; +import { useNewUsers } from './useNewUsers'; + +const TICK_WIDTH = 45; + +type NewUsersSectionProps = { + timezone: 'utc' | 'local'; +}; + +const NewUsersSection = ({ timezone }: NewUsersSectionProps): ReactElement => { + const [period, periodSelectorProps] = usePeriodSelectorState( + 'last 7 days', + 'last 30 days', + 'last 90 days', + ); + const periodLabel = usePeriodLabel(period); + + const utc = timezone === 'utc'; + const { data } = useNewUsers({ period, utc }); + + const t = useTranslation(); + + const formatDate = useFormatDate(); + + const { ref: sizeRef, contentBoxSize: { inlineSize = 600 } = {} } = useResizeObserver(); + + const maxTicks = Math.ceil(inlineSize / TICK_WIDTH); + + const tickValues = useMemo(() => { + if (!data) { + return undefined; + } + + const arrayLength = moment(data.end).diff(data.start, 'days') + 1; + if (arrayLength <= maxTicks || !maxTicks) { + return undefined; + } + + const values = Array.from({ length: arrayLength }, (_, i) => + moment(data.start).add(i, 'days').format('YYYY-MM-DD'), + ); + + const relation = Math.ceil(values.length / maxTicks); + + return values.reduce((acc, cur, i) => { + if ((i + 1) % relation === 0) { + acc = [...acc, cur]; + } + return acc; + }, [] as string[]); + }, [data, maxTicks]); + + const [countFromPeriod, variatonFromPeriod, countFromYesterday, variationFromYesterday, values] = + useMemo(() => { + if (!data) { + return []; + } + + const values = Array.from( + { length: moment(data.end).diff(data.start, 'days') + 1 }, + (_, i) => ({ + date: moment(data.start).add(i, 'days').format('YYYY-MM-DD'), + newUsers: 0, + }), + ); + for (const { day, users } of data.days) { + const i = utc + ? moment(day).utc().diff(data.start, 'days') + : moment(day).diff(data.start, 'days'); + if (i >= 0) { + values[i].newUsers += users; + } + } + + return [ + data.period.count, + data.period.variation, + data.yesterday.count, + data.yesterday.variation, + values, + ]; + }, [data, utc]); + + return ( +
    + + + values?.map(({ date, newUsers }) => [date, newUsers]) + } + /> + + } + > + , + variation: variatonFromPeriod ?? 0, + description: periodLabel, + }, + { + count: countFromYesterday ?? , + variation: variationFromYesterday ?? 0, + description: t('Yesterday'), + }, + ]} + /> + + {values ? ( + + + + + + moment(date).format(values?.length === 7 ? 'dddd' : 'DD/MM'), + }} + axisLeft={{ + tickSize: 0, + // TODO: Get it from theme + tickPadding: 4, + tickRotation: 0, + }} + animate={true} + motionStiffness={90} + motionDamping={15} + theme={{ + // TODO: Get it from theme + axis: { + ticks: { + text: { + fill: colors.n600, + fontFamily: + 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', + fontSize: '10px', + fontStyle: 'normal', + fontWeight: 600, + letterSpacing: '0.2px', + lineHeight: '12px', + }, + }, + }, + tooltip: { + // @ts-ignore + backgroundColor: colors.n900, + boxShadow: + '0px 0px 12px rgba(47, 52, 61, 0.12), 0px 0px 2px rgba(47, 52, 61, 0.08)', + borderRadius: 2, + padding: 4, + }, + }} + tooltip={({ value, indexValue }): ReactElement => ( + + {t('Value_users', { value })}, {formatDate(indexValue)} + + )} + /> + + + + + ) : ( + + + + )} + +
    + ); +}; + +export default NewUsersSection; diff --git a/ee/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx b/ee/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx new file mode 100644 index 000000000000..68baf0619b8b --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx @@ -0,0 +1,200 @@ +import { ResponsiveHeatMap } from '@nivo/heatmap'; +import { Box, Flex, Skeleton } from '@rocket.chat/fuselage'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; +import moment from 'moment'; +import React, { ReactElement, useMemo } from 'react'; + +import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; +import Section from '../Section'; +import DownloadDataButton from '../data/DownloadDataButton'; +import PeriodSelector from '../data/PeriodSelector'; +import { usePeriodSelectorState } from '../data/usePeriodSelectorState'; +import { useUsersByTimeOfTheDay } from './useUsersByTimeOfTheDay'; + +type UsersByTimeOfTheDaySectionProps = { + timezone: 'utc' | 'local'; +}; + +const UsersByTimeOfTheDaySection = ({ + timezone, +}: UsersByTimeOfTheDaySectionProps): ReactElement => { + const [period, periodSelectorProps] = usePeriodSelectorState( + 'last 7 days', + 'last 30 days', + 'last 90 days', + ); + + const utc = timezone === 'utc'; + + const { data } = useUsersByTimeOfTheDay({ period, utc }); + + const t = useTranslation(); + + const [dates, values] = useMemo(() => { + if (!data) { + return []; + } + + const dates = Array.from( + { + length: utc + ? moment(data.end).diff(data.start, 'days') + 1 + : moment(data.end).diff(data.start, 'days') - 1, + }, + (_, i) => + utc + ? moment.utc(data.start).endOf('day').add(i, 'days') + : moment(data.start) + .endOf('day') + .add(i + 1, 'days'), + ); + + const values = Array.from( + { length: 24 }, + (_, hour) => + ({ + hour: String(hour), + ...dates + .map((date) => ({ [date.toISOString()]: 0 })) + .reduce((obj, elem) => ({ ...obj, ...elem }), {}), + } as { [date: string]: number } & { hour: string }), + ); + + const timezoneOffset = moment().utcOffset() / 60; + + for (const { users, hour, day, month, year } of data.week) { + const date = utc + ? moment.utc([year, month - 1, day, hour]) + : moment([year, month - 1, day, hour]).add(timezoneOffset, 'hours'); + + if (utc || (!date.isSame(data.end) && !date.clone().startOf('day').isSame(data.start))) { + values[date.hour()][date.endOf('day').toISOString()] += users; + } + } + + return [dates.map((date) => date.toISOString()), values]; + }, [data, utc]); + + return ( +
    + + + data?.week + ?.map(({ users, hour, day, month, year }) => ({ + date: moment([year, month - 1, day, hour, 0, 0, 0]), + users, + })) + ?.sort((a, b) => a.date.diff(b.date)) + ?.map(({ date, users }) => [date.toISOString(), users]) + } + /> + + } + > + {values ? ( + + + + + + dates?.length === 7 ? moment(isoString).format('dddd') : '', + }} + axisLeft={{ + // TODO: Get it from theme + tickSize: 0, + tickPadding: 4, + tickRotation: 0, + format: (hour): string => + moment() + .set({ hour: parseInt(hour, 10), minute: 0, second: 0 }) + .format('LT'), + }} + hoverTarget='cell' + animate={dates && dates.length <= 7} + motionStiffness={90} + motionDamping={15} + theme={{ + // TODO: Get it from theme + axis: { + ticks: { + text: { + fill: colors.n600, + fontFamily: + 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', + fontSize: 10, + fontStyle: 'normal', + fontWeight: 600, + letterSpacing: '0.2px', + lineHeight: '12px', + }, + }, + }, + tooltip: { + container: { + backgroundColor: colors.n900, + boxShadow: + '0px 0px 12px rgba(47, 52, 61, 0.12), 0px 0px 2px rgba(47, 52, 61, 0.08)', + borderRadius: 2, + }, + }, + }} + tooltip={({ value }): ReactElement => ( + + {t('Value_users', { value })} + + )} + /> + + + + + ) : ( + + )} +
    + ); +}; + +export default UsersByTimeOfTheDaySection; diff --git a/ee/client/views/admin/engagementDashboard/users/UsersTab.stories.tsx b/ee/client/views/admin/engagementDashboard/users/UsersTab.stories.tsx new file mode 100644 index 000000000000..45ec3cd06fc5 --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/users/UsersTab.stories.tsx @@ -0,0 +1,14 @@ +import { Margins } from '@rocket.chat/fuselage'; +import { Meta, Story } from '@storybook/react'; +import React, { ReactElement } from 'react'; + +import UsersTab from './UsersTab'; + +export default { + title: 'admin/engagementDashboard/UsersTab', + component: UsersTab, + decorators: [(fn): ReactElement => ], +} as Meta; + +export const Default: Story = () => ; +Default.storyName = 'UsersTab'; diff --git a/ee/client/views/admin/engagementDashboard/users/UsersTab.tsx b/ee/client/views/admin/engagementDashboard/users/UsersTab.tsx new file mode 100644 index 000000000000..f870c4fa3593 --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/users/UsersTab.tsx @@ -0,0 +1,37 @@ +import { Box, Divider, Flex, Margins } from '@rocket.chat/fuselage'; +import { useBreakpoints } from '@rocket.chat/fuselage-hooks'; +import React, { ReactElement } from 'react'; + +import ActiveUsersSection from './ActiveUsersSection'; +import BusiestChatTimesSection from './BusiestChatTimesSection'; +import NewUsersSection from './NewUsersSection'; +import UsersByTimeOfTheDaySection from './UsersByTimeOfTheDaySection'; + +type UsersTabProps = { + timezone: 'utc' | 'local'; +}; + +const UsersTab = ({ timezone }: UsersTabProps): ReactElement => { + const isXxlScreen = useBreakpoints().includes('xxl'); + + return ( + <> + + + + + + + + + + + + + + + + ); +}; + +export default UsersTab; diff --git a/ee/client/views/admin/engagementDashboard/users/useActiveUsers.ts b/ee/client/views/admin/engagementDashboard/users/useActiveUsers.ts new file mode 100644 index 000000000000..17bc28f4d8f0 --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/users/useActiveUsers.ts @@ -0,0 +1,32 @@ +import moment from 'moment'; +import { useQuery } from 'react-query'; + +import { getFromRestApi } from '../../../../lib/getFromRestApi'; +import { getPeriodRange } from '../data/periods'; + +type UseActiveUsersOptions = { utc: boolean }; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const useActiveUsers = ({ utc }: UseActiveUsersOptions) => + useQuery( + ['admin/engagement-dashboard/users/active', { utc }], + async () => { + const { start, end } = getPeriodRange('last 30 days', utc); + + const response = await getFromRestApi('/v1/engagement-dashboard/users/active-users')({ + start: (utc ? moment.utc(start) : moment(start)).subtract(29, 'days').toISOString(), + end: end.toISOString(), + }); + + return response + ? { + ...response, + start, + end, + } + : undefined; + }, + { + refetchInterval: 5 * 60 * 1000, + }, + ); diff --git a/ee/client/views/admin/engagementDashboard/users/useHourlyChatActivity.ts b/ee/client/views/admin/engagementDashboard/users/useHourlyChatActivity.ts new file mode 100644 index 000000000000..ab21f3d2f38d --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/users/useHourlyChatActivity.ts @@ -0,0 +1,36 @@ +import moment from 'moment'; +import { useQuery } from 'react-query'; + +import { getFromRestApi } from '../../../../lib/getFromRestApi'; + +type UseHourlyChatActivityOptions = { + displacement: number; + utc: boolean; +}; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const useHourlyChatActivity = ({ displacement, utc }: UseHourlyChatActivityOptions) => + useQuery( + ['admin/engagement-dashboard/users/hourly-chat-activity', { displacement, utc }], + async () => { + const day = (utc ? moment.utc().endOf('day') : moment().endOf('day')) + .subtract(displacement, 'days') + .toDate(); + + const response = await getFromRestApi( + '/v1/engagement-dashboard/users/chat-busier/hourly-data', + )({ + start: day.toISOString(), + }); + + return response + ? { + ...response, + day, + } + : undefined; + }, + { + refetchInterval: 5 * 60 * 1000, + }, + ); diff --git a/ee/client/views/admin/engagementDashboard/users/useNewUsers.ts b/ee/client/views/admin/engagementDashboard/users/useNewUsers.ts new file mode 100644 index 000000000000..4fcbe5afb250 --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/users/useNewUsers.ts @@ -0,0 +1,31 @@ +import { useQuery } from 'react-query'; + +import { getFromRestApi } from '../../../../lib/getFromRestApi'; +import { getPeriodRange, Period } from '../data/periods'; + +type UseNewUsersOptions = { period: Period['key']; utc: boolean }; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const useNewUsers = ({ period, utc }: UseNewUsersOptions) => + useQuery( + ['admin/engagement-dashboard/users/new', { period, utc }], + async () => { + const { start, end } = getPeriodRange(period, utc); + + const response = await getFromRestApi('/v1/engagement-dashboard/users/new-users')({ + start: start.toISOString(), + end: end.toISOString(), + }); + + return response + ? { + ...response, + start, + end, + } + : undefined; + }, + { + refetchInterval: 5 * 60 * 1000, + }, + ); diff --git a/ee/client/views/admin/engagementDashboard/users/useUsersByTimeOfTheDay.ts b/ee/client/views/admin/engagementDashboard/users/useUsersByTimeOfTheDay.ts new file mode 100644 index 000000000000..91da8883a49e --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/users/useUsersByTimeOfTheDay.ts @@ -0,0 +1,33 @@ +import { useQuery } from 'react-query'; + +import { getFromRestApi } from '../../../../lib/getFromRestApi'; +import { getPeriodRange, Period } from '../data/periods'; + +type UseUsersByTimeOfTheDayOptions = { period: Period['key']; utc: boolean }; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const useUsersByTimeOfTheDay = ({ period, utc }: UseUsersByTimeOfTheDayOptions) => + useQuery( + ['admin/engagement-dashboard/users/users-by-time-of-the-day', { period, utc }], + async () => { + const { start, end } = getPeriodRange(period, utc); + + const response = await getFromRestApi( + '/v1/engagement-dashboard/users/users-by-time-of-the-day-in-a-week', + )({ + start: start.toISOString(), + end: end.toISOString(), + }); + + return response + ? { + ...response, + start, + end, + } + : undefined; + }, + { + refetchInterval: 5 * 60 * 1000, + }, + ); diff --git a/ee/client/views/admin/engagementDashboard/users/useWeeklyChatActivity.ts b/ee/client/views/admin/engagementDashboard/users/useWeeklyChatActivity.ts new file mode 100644 index 000000000000..b6327e61253c --- /dev/null +++ b/ee/client/views/admin/engagementDashboard/users/useWeeklyChatActivity.ts @@ -0,0 +1,36 @@ +import moment from 'moment'; +import { useQuery } from 'react-query'; + +import { getFromRestApi } from '../../../../lib/getFromRestApi'; + +type UseWeeklyChatActivityOptions = { + displacement: number; + utc: boolean; +}; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const useWeeklyChatActivity = ({ displacement, utc }: UseWeeklyChatActivityOptions) => + useQuery( + ['admin/engagement-dashboard/users/weekly-chat-activity', { displacement, utc }], + async () => { + const day = (utc ? moment.utc().endOf('day') : moment().endOf('day')) + .subtract(displacement, 'weeks') + .toDate(); + + const response = await getFromRestApi( + '/v1/engagement-dashboard/users/chat-busier/weekly-data', + )({ + start: day.toISOString(), + }); + + return response + ? { + ...response, + day, + } + : undefined; + }, + { + refetchInterval: 5 * 60 * 1000, + }, + ); diff --git a/ee/client/views/admin/users/SeatsCapUsage/SeatsCapUsage.stories.tsx b/ee/client/views/admin/users/SeatsCapUsage/SeatsCapUsage.stories.tsx index f229dd1bc3b0..3eb3405eb01f 100644 --- a/ee/client/views/admin/users/SeatsCapUsage/SeatsCapUsage.stories.tsx +++ b/ee/client/views/admin/users/SeatsCapUsage/SeatsCapUsage.stories.tsx @@ -3,7 +3,7 @@ import React, { ReactElement } from 'react'; import SeatsCapUsage from './SeatsCapUsage'; export default { - title: 'ee/admin/users/SeatsCapUsage', + title: 'admin/users/SeatsCapUsage', component: SeatsCapUsage, }; diff --git a/ee/definition/.eslintrc.js b/ee/definition/.eslintrc.js new file mode 120000 index 000000000000..4ce23c9428e7 --- /dev/null +++ b/ee/definition/.eslintrc.js @@ -0,0 +1 @@ +../../client/.eslintrc.js \ No newline at end of file diff --git a/ee/definition/.prettierrc b/ee/definition/.prettierrc new file mode 120000 index 000000000000..9d5a1f5613c4 --- /dev/null +++ b/ee/definition/.prettierrc @@ -0,0 +1 @@ +../../client/.prettierrc \ No newline at end of file diff --git a/ee/definition/rest/index.ts b/ee/definition/rest/index.ts new file mode 100644 index 000000000000..052364327e57 --- /dev/null +++ b/ee/definition/rest/index.ts @@ -0,0 +1,4 @@ +import type { EngagementDashboardEndpoints } from './v1/engagementDashboard'; +import type { OmnichannelEndpoints } from './v1/omnichannel'; + +export type EnterpriseEndpoints = EngagementDashboardEndpoints & OmnichannelEndpoints; diff --git a/ee/definition/rest/v1/engagementDashboard.ts b/ee/definition/rest/v1/engagementDashboard.ts new file mode 100644 index 000000000000..c45dd1f603b4 --- /dev/null +++ b/ee/definition/rest/v1/engagementDashboard.ts @@ -0,0 +1,109 @@ +import type { IDirectMessageRoom, IRoom } from '../../../../definition/IRoom'; +import type { IUser } from '../../../../definition/IUser'; + +export type EngagementDashboardEndpoints = { + '/v1/engagement-dashboard/channels/list': { + GET: (params: { start: Date; end: Date; offset: number; count: number }) => { + channels: { + room: { + _id: IRoom['_id']; + name: IRoom['name'] | IRoom['fname']; + ts: IRoom['ts']; + t: IRoom['t']; + _updatedAt: IRoom['_updatedAt']; + usernames?: IDirectMessageRoom['usernames']; + }; + messages: number; + lastWeekMessages: number; + diffFromLastWeek: number; + }[]; + count: number; + offset: number; + total: number; + }; + }; + '/v1/engagement-dashboard/messages/origin': { + GET: (params: { start: Date; end: Date }) => { + origins: { + t: IRoom['t']; + messages: number; + }[]; + }; + }; + '/v1/engagement-dashboard/messages/top-five-popular-channels': { + GET: (params: { start: Date; end: Date }) => { + channels: { + t: IRoom['t']; + messages: number; + name: IRoom['name'] | IRoom['fname']; + usernames?: IDirectMessageRoom['usernames']; + }[]; + }; + }; + '/v1/engagement-dashboard/messages/messages-sent': { + GET: (params: { start: Date; end: Date }) => { + days: { day: Date; messages: number }[]; + period: { + count: number; + variation: number; + }; + yesterday: { + count: number; + variation: number; + }; + }; + }; + '/v1/engagement-dashboard/users/active-users': { + GET: (params: { start: Date; end: Date }) => { + month: { + day: number; + month: number; + year: number; + usersList: IUser['_id'][]; + users: number; + }[]; + }; + }; + '/v1/engagement-dashboard/users/chat-busier/weekly-data': { + GET: (params: { start: Date }) => { + month: { + users: number; + day: number; + month: number; + year: number; + }[]; + }; + }; + '/v1/engagement-dashboard/users/chat-busier/hourly-data': { + GET: (params: { start: Date }) => { + hours: { + users: number; + hour: number; + }[]; + }; + }; + '/v1/engagement-dashboard/users/users-by-time-of-the-day-in-a-week': { + GET: (params: { start: Date; end: Date }) => { + week: { + users: number; + hour: number; + day: number; + month: number; + year: number; + }[]; + }; + }; + '/v1/engagement-dashboard/users/new-users': { + GET: (params: { start: Date; end: Date }) => { + days: { day: Date; users: number }[]; + period: { + count: number; + variation: number; + }; + yesterday: { + count: number; + variation: number; + }; + }; + }; +}; diff --git a/ee/definition/rest/v1/omnichannel/businessHours.ts b/ee/definition/rest/v1/omnichannel/businessHours.ts new file mode 100644 index 000000000000..fa42bedf8746 --- /dev/null +++ b/ee/definition/rest/v1/omnichannel/businessHours.ts @@ -0,0 +1,17 @@ +import { ILivechatBusinessHour } from '../../../../../definition/ILivechatBusinessHour'; + +export type OmnichannelBusinessHoursEndpoints = { + 'livechat/business-hours.list': { + GET: (params: { + name?: string; + offset: number; + count: number; + sort: Record; + }) => { + businessHours: ILivechatBusinessHour[]; + count: number; + offset: number; + total: number; + }; + }; +}; diff --git a/ee/definition/rest/v1/omnichannel/businessUnits.ts b/ee/definition/rest/v1/omnichannel/businessUnits.ts new file mode 100644 index 000000000000..d8bb5ceb84e8 --- /dev/null +++ b/ee/definition/rest/v1/omnichannel/businessUnits.ts @@ -0,0 +1,34 @@ +import { ILivechatMonitor } from '../../../../../definition/ILivechatMonitor'; +import { IOmnichannelBusinessUnit } from '../../../../../definition/IOmnichannelBusinessUnit'; +import { PaginatedResult } from '../../../../../definition/rest/helpers/PaginatedResult'; + +export type OmnichannelBusinessUnitsEndpoints = { + 'livechat/units.list': { + GET: (params: { text: string }) => PaginatedResult & { + units: IOmnichannelBusinessUnit[]; + }; + }; + 'livechat/units.getOne': { + GET: (params: { unitId: string }) => IOmnichannelBusinessUnit; + }; + 'livechat/unitMonitors.list': { + GET: (params: { unitId: string }) => { monitors: ILivechatMonitor[] }; + }; + 'livechat/units': { + GET: (params: { text: string }) => PaginatedResult & { units: IOmnichannelBusinessUnit[] }; + POST: (params: { + unitData: string; + unitMonitors: string; + unitDepartments: string; + }) => IOmnichannelBusinessUnit; + }; + 'livechat/units/:id': { + GET: () => IOmnichannelBusinessUnit; + POST: (params: { + unitData: string; + unitMonitors: string; + unitDepartments: string; + }) => IOmnichannelBusinessUnit; + DELETE: () => number; + }; +}; diff --git a/ee/definition/rest/v1/omnichannel/cannedResponses.ts b/ee/definition/rest/v1/omnichannel/cannedResponses.ts new file mode 100644 index 000000000000..f536aeb5163d --- /dev/null +++ b/ee/definition/rest/v1/omnichannel/cannedResponses.ts @@ -0,0 +1,37 @@ +import { ILivechatDepartment } from '../../../../../definition/ILivechatDepartment'; +import { IOmnichannelCannedResponse } from '../../../../../definition/IOmnichannelCannedResponse'; +import { IUser } from '../../../../../definition/IUser'; + +export type OmnichannelCannedResponsesEndpoints = { + 'canned-responses': { + GET: (params: { + shortcut?: string; + text?: string; + scope?: string; + createdBy?: IUser['username']; + tags?: any; + departmentId?: ILivechatDepartment['_id']; + offset?: number; + count?: number; + }) => { + cannedResponses: IOmnichannelCannedResponse[]; + count?: number; + offset?: number; + total: number; + }; + POST: (params: { + _id?: IOmnichannelCannedResponse['_id']; + shortcut: string; + text: string; + scope: string; + tags?: any; + departmentId?: ILivechatDepartment['_id']; + }) => void; + DELETE: (params: { _id: IOmnichannelCannedResponse['_id'] }) => void; + }; + 'canned-responses/:_id': { + GET: () => { + cannedResponse: IOmnichannelCannedResponse; + }; + }; +}; diff --git a/ee/definition/rest/v1/omnichannel/index.ts b/ee/definition/rest/v1/omnichannel/index.ts new file mode 100644 index 000000000000..29cc5999c08a --- /dev/null +++ b/ee/definition/rest/v1/omnichannel/index.ts @@ -0,0 +1,7 @@ +import type { OmnichannelBusinessHoursEndpoints } from './businessHours'; +import type { OmnichannelBusinessUnitsEndpoints } from './businessUnits'; +import { OmnichannelCannedResponsesEndpoints } from './cannedResponses'; + +export type OmnichannelEndpoints = OmnichannelBusinessHoursEndpoints & + OmnichannelBusinessUnitsEndpoints & + OmnichannelCannedResponsesEndpoints; diff --git a/ee/server/api/api.ts b/ee/server/api/api.ts new file mode 100644 index 000000000000..59ce2ae7db31 --- /dev/null +++ b/ee/server/api/api.ts @@ -0,0 +1,34 @@ +import { API, Options, NonEnterpriseTwoFactorOptions } from '../../../app/api/server/api'; +import { use } from '../../../app/settings/server/Middleware'; +import { isEnterprise } from '../../app/license/server/license'; + +// Overwrites two factor method to enforce 2FA check for enterprise APIs when +// no license was provided to prevent abuse on enterprise APIs. + +export const isNonEnterpriseTwoFactorOptions = (options?: Options): + options is NonEnterpriseTwoFactorOptions => !!options + && 'forceTwoFactorAuthenticationForNonEnterprise' in options + && Boolean(options.forceTwoFactorAuthenticationForNonEnterprise); + +API.v1.processTwoFactor = use(API.v1.processTwoFactor, function([params, ...context], next) { + if (isNonEnterpriseTwoFactorOptions(params.options) && !isEnterprise()) { + const options: NonEnterpriseTwoFactorOptions = { + + ...params.options, + twoFactorOptions: { + disableRememberMe: true, + requireSecondFactor: true, + disablePasswordFallback: false, + }, + twoFactorRequired: true, + authRequired: true, + }; + + return next({ + ...params, + options, + }, ...context); + } + + return next(params, ...context); +}); diff --git a/ee/server/api/engagementDashboard/channels.ts b/ee/server/api/engagementDashboard/channels.ts new file mode 100644 index 000000000000..dee036076517 --- /dev/null +++ b/ee/server/api/engagementDashboard/channels.ts @@ -0,0 +1,33 @@ +import { check, Match } from 'meteor/check'; + +import { API } from '../../../../app/api/server'; +import { findAllChannelsWithNumberOfMessages } from '../../lib/engagementDashboard/channels'; +import { isDateISOString, mapDateForAPI } from '../../lib/engagementDashboard/date'; + +API.v1.addRoute('engagement-dashboard/channels/list', { + authRequired: true, + permissionsRequired: ['view-engagement-dashboard'], +}, { + async get() { + check(this.queryParams, Match.ObjectIncluding({ + start: Match.Where(isDateISOString), + end: Match.Where(isDateISOString), + })); + + const { start, end } = this.queryParams; + const { offset, count } = this.getPaginationItems(); + + const { channels, total } = await findAllChannelsWithNumberOfMessages({ + start: mapDateForAPI(start), + end: mapDateForAPI(end), + options: { offset, count }, + }); + + return API.v1.success({ + channels, + total, + offset, + count: channels.length, + }); + }, +}); diff --git a/ee/app/engagement-dashboard/server/api/index.js b/ee/server/api/engagementDashboard/index.ts similarity index 100% rename from ee/app/engagement-dashboard/server/api/index.js rename to ee/server/api/engagementDashboard/index.ts diff --git a/ee/server/api/engagementDashboard/messages.ts b/ee/server/api/engagementDashboard/messages.ts new file mode 100644 index 000000000000..23457f87d2b3 --- /dev/null +++ b/ee/server/api/engagementDashboard/messages.ts @@ -0,0 +1,56 @@ +import { check, Match } from 'meteor/check'; + +import { API } from '../../../../app/api/server'; +import { findWeeklyMessagesSentData, findMessagesSentOrigin, findTopFivePopularChannelsByMessageSentQuantity } from '../../lib/engagementDashboard/messages'; +import { isDateISOString, transformDatesForAPI } from '../../lib/engagementDashboard/date'; + +API.v1.addRoute('engagement-dashboard/messages/messages-sent', { + authRequired: true, + permissionsRequired: ['view-engagement-dashboard'], +}, { + async get() { + check(this.queryParams, Match.ObjectIncluding({ + start: Match.Where(isDateISOString), + end: Match.Where(isDateISOString), + })); + + const { start, end } = this.queryParams; + + const data = await findWeeklyMessagesSentData(transformDatesForAPI(start, end)); + return API.v1.success(data); + }, +}); + +API.v1.addRoute('engagement-dashboard/messages/origin', { + authRequired: true, + permissionsRequired: ['view-engagement-dashboard'], +}, { + async get() { + check(this.queryParams, Match.ObjectIncluding({ + start: Match.Where(isDateISOString), + end: Match.Where(isDateISOString), + })); + + const { start, end } = this.queryParams; + + const data = await findMessagesSentOrigin(transformDatesForAPI(start, end)); + return API.v1.success(data); + }, +}); + +API.v1.addRoute('engagement-dashboard/messages/top-five-popular-channels', { + authRequired: true, + permissionsRequired: ['view-engagement-dashboard'], +}, { + async get() { + check(this.queryParams, Match.ObjectIncluding({ + start: Match.Where(isDateISOString), + end: Match.Where(isDateISOString), + })); + + const { start, end } = this.queryParams; + + const data = await findTopFivePopularChannelsByMessageSentQuantity(transformDatesForAPI(start, end)); + return API.v1.success(data); + }, +}); diff --git a/ee/server/api/engagementDashboard/users.ts b/ee/server/api/engagementDashboard/users.ts new file mode 100644 index 000000000000..bd1c12f22f02 --- /dev/null +++ b/ee/server/api/engagementDashboard/users.ts @@ -0,0 +1,94 @@ +import { check, Match } from 'meteor/check'; + +import { API } from '../../../../app/api/server'; +import { + findWeeklyUsersRegisteredData, + findActiveUsersMonthlyData, + findBusiestsChatsInADayByHours, + findBusiestsChatsWithinAWeek, + findUserSessionsByHourWithinAWeek, +} from '../../lib/engagementDashboard/users'; +import { isDateISOString, transformDatesForAPI } from '../../lib/engagementDashboard/date'; + +API.v1.addRoute('engagement-dashboard/users/new-users', { + authRequired: true, + permissionsRequired: ['view-engagement-dashboard'], +}, { + async get() { + check(this.queryParams, Match.ObjectIncluding({ + start: Match.Where(isDateISOString), + end: Match.Where(isDateISOString), + })); + + const { start, end } = this.queryParams; + + const data = await findWeeklyUsersRegisteredData(transformDatesForAPI(start, end)); + return API.v1.success(data); + }, +}); + +API.v1.addRoute('engagement-dashboard/users/active-users', { + authRequired: true, + permissionsRequired: ['view-engagement-dashboard'], +}, { + async get() { + check(this.queryParams, Match.ObjectIncluding({ + start: Match.Where(isDateISOString), + end: Match.Where(isDateISOString), + })); + + const { start, end } = this.queryParams; + + const data = await findActiveUsersMonthlyData(transformDatesForAPI(start, end)); + return API.v1.success(data); + }, +}); + +API.v1.addRoute('engagement-dashboard/users/chat-busier/hourly-data', { + authRequired: true, + permissionsRequired: ['view-engagement-dashboard'], +}, { + async get() { + check(this.queryParams, Match.ObjectIncluding({ + start: Match.Where(isDateISOString), + })); + + const { start } = this.queryParams; + + const data = await findBusiestsChatsInADayByHours(transformDatesForAPI(start)); + return API.v1.success(data); + }, +}); + +API.v1.addRoute('engagement-dashboard/users/chat-busier/weekly-data', { + authRequired: true, + permissionsRequired: ['view-engagement-dashboard'], +}, { + async get() { + check(this.queryParams, Match.ObjectIncluding({ + start: Match.Where(isDateISOString), + })); + + const { start } = this.queryParams; + + const data = await findBusiestsChatsWithinAWeek(transformDatesForAPI(start)); + return API.v1.success(data); + }, +}); + +API.v1.addRoute('engagement-dashboard/users/users-by-time-of-the-day-in-a-week', { + authRequired: true, + permissionsRequired: ['view-engagement-dashboard'], +}, { + async get() { + check(this.queryParams, Match.ObjectIncluding({ + start: Match.Where(isDateISOString), + end: Match.Where(isDateISOString), + })); + + const { start, end } = this.queryParams; + + const data = await findUserSessionsByHourWithinAWeek(transformDatesForAPI(start, end)); + return API.v1.success(data); + }, +}); diff --git a/ee/server/api/index.ts b/ee/server/api/index.ts index a0007bcae7c6..e0ccd58c5f4f 100644 --- a/ee/server/api/index.ts +++ b/ee/server/api/index.ts @@ -1,2 +1,3 @@ +import './api'; import './ldap'; import './licenses'; diff --git a/ee/server/api/ldap.ts b/ee/server/api/ldap.ts index 1c4627e585d0..0dd3d652857f 100644 --- a/ee/server/api/ldap.ts +++ b/ee/server/api/ldap.ts @@ -2,10 +2,13 @@ import { hasRole } from '../../../app/authorization/server'; import { settings } from '../../../app/settings/server'; import { API } from '../../../app/api/server/api'; import { LDAPEE } from '../sdk'; -import { hasLicense } from '../../app/license/server/license'; -API.v1.addRoute('ldap.syncNow', { authRequired: true }, { - post() { +API.v1.addRoute('ldap.syncNow', { + authRequired: true, + forceTwoFactorAuthenticationForNonEnterprise: true, + twoFactorRequired: true, +}, { + async post() { if (!this.userId) { throw new Error('error-invalid-user'); } @@ -14,18 +17,14 @@ API.v1.addRoute('ldap.syncNow', { authRequired: true }, { throw new Error('error-not-authorized'); } - if (!hasLicense('ldap-enterprise')) { - throw new Error('error-not-authorized'); - } - if (settings.get('LDAP_Enable') !== true) { throw new Error('LDAP_disabled'); } - LDAPEE.sync(); + await LDAPEE.sync(); return API.v1.success({ - message: 'Sync_in_progress', + message: 'Sync_in_progress' as const, }); }, }); diff --git a/ee/server/api/licenses.ts b/ee/server/api/licenses.ts index 0972584c3983..c59ab4e40606 100644 --- a/ee/server/api/licenses.ts +++ b/ee/server/api/licenses.ts @@ -1,9 +1,10 @@ import { check } from 'meteor/check'; -import { ILicense, getLicenses, validateFormat, flatModules, getMaxActiveUsers } from '../../app/license/server/license'; +import { getLicenses, validateFormat, flatModules, getMaxActiveUsers } from '../../app/license/server/license'; import { Settings, Users } from '../../../app/models/server'; import { API } from '../../../app/api/server/api'; import { hasPermission } from '../../../app/authorization/server'; +import { ILicense } from '../../app/license/definitions/ILicense'; function licenseTransform(license: ILicense): ILicense { return { diff --git a/ee/server/configuration/oauth.ts b/ee/server/configuration/oauth.ts index 438393280891..92c15da8aa55 100644 --- a/ee/server/configuration/oauth.ts +++ b/ee/server/configuration/oauth.ts @@ -21,6 +21,7 @@ interface IOAuthUserIdentity { interface IOAuthSettings { mapChannels: string; mergeRoles: string; + rolesToSync: string; rolesClaim: string; groupsClaim: string; channelsAdmin: string; @@ -33,6 +34,7 @@ function getOAuthSettings(serviceName: string): IOAuthSettings { return { mapChannels: settings.get(`Accounts_OAuth_Custom-${ serviceName }-map_channels`) as string, mergeRoles: settings.get(`Accounts_OAuth_Custom-${ serviceName }-merge_roles`) as string, + rolesToSync: settings.get(`Accounts_OAuth_Custom-${ serviceName }-roles_to_sync`) as string, rolesClaim: settings.get(`Accounts_OAuth_Custom-${ serviceName }-roles_claim`) as string, groupsClaim: settings.get(`Accounts_OAuth_Custom-${ serviceName }-groups_claim`) as string, channelsAdmin: settings.get(`Accounts_OAuth_Custom-${ serviceName }-channels_admin`) as string, @@ -61,7 +63,7 @@ onLicense('oauth-enterprise', () => { } if (settings.mergeRoles) { - OAuthEEManager.updateRolesFromSSO(auth.user, auth.serviceData, settings.rolesClaim); + OAuthEEManager.updateRolesFromSSO(auth.user, auth.serviceData, settings.rolesClaim, settings.rolesToSync.split(',').map((role) => role.trim())); } }); diff --git a/ee/server/index.js b/ee/server/index.ts similarity index 91% rename from ee/server/index.js rename to ee/server/index.ts index b25b60748d88..963e58fe6d97 100644 --- a/ee/server/index.js +++ b/ee/server/index.ts @@ -7,7 +7,6 @@ import '../app/api-enterprise/server/index'; import '../app/auditing/server/index'; import '../app/authorization/server/index'; import '../app/canned-responses/server/index'; -import '../app/engagement-dashboard/server/index'; import '../app/livechat-enterprise/server/index'; import '../app/settings/server/index'; import '../app/teams-mention/server/index'; diff --git a/ee/server/lib/engagementDashboard/channels.ts b/ee/server/lib/engagementDashboard/channels.ts new file mode 100644 index 000000000000..d723bceb4b34 --- /dev/null +++ b/ee/server/lib/engagementDashboard/channels.ts @@ -0,0 +1,54 @@ +import moment from 'moment'; + +import { Rooms } from '../../../../app/models/server/raw'; +import { convertDateToInt, diffBetweenDaysInclusive } from './date'; +import { IDirectMessageRoom, IRoom } from '../../../../definition/IRoom'; + +export const findAllChannelsWithNumberOfMessages = async ({ start, end, options = {} }: { + start: Date; + end: Date; + options: { + offset?: number; + count?: number; + }; +}): Promise<{ + channels: { + room: { + _id: IRoom['_id']; + name: IRoom['name'] | IRoom['fname']; + ts: IRoom['ts']; + t: IRoom['t']; + _updatedAt: IRoom['_updatedAt']; + usernames?: IDirectMessageRoom['usernames']; + }; + messages: number; + lastWeekMessages: number; + diffFromLastWeek: number; + }[]; + total: number; +}> => { + const daysBetweenDates = diffBetweenDaysInclusive(end, start); + const endOfLastWeek = moment(start).subtract(1, 'days').toDate(); + const startOfLastWeek = moment(endOfLastWeek).subtract(daysBetweenDates, 'days').toDate(); + + const channels = await Rooms.findChannelsWithNumberOfMessagesBetweenDate({ + start: convertDateToInt(start), + end: convertDateToInt(end), + startOfLastWeek: convertDateToInt(startOfLastWeek), + endOfLastWeek: convertDateToInt(endOfLastWeek), + options, + }).toArray(); + + const total = (await Rooms.findChannelsWithNumberOfMessagesBetweenDate({ + start: convertDateToInt(start), + end: convertDateToInt(end), + startOfLastWeek: convertDateToInt(startOfLastWeek), + endOfLastWeek: convertDateToInt(endOfLastWeek), + onlyCount: true, + }).toArray())[0]?.total ?? 0; + + return { + channels, + total, + }; +}; diff --git a/ee/server/lib/engagementDashboard/date.ts b/ee/server/lib/engagementDashboard/date.ts new file mode 100644 index 000000000000..9af9911ec0b7 --- /dev/null +++ b/ee/server/lib/engagementDashboard/date.ts @@ -0,0 +1,34 @@ +import mem from 'mem'; +import moment from 'moment'; + +export const isDateISOString = mem((input: string): input is string => { + const timestamp = Date.parse(input); + return !Number.isNaN(timestamp) && new Date(timestamp).toISOString() === input; +}, { maxAge: 10000 }); + +export const mapDateForAPI = (input: string): Date => { + if (!isDateISOString(input)) { + throw new Error('invalid ISO 8601 date'); + } + + return new Date(Date.parse(input)); +}; + +export const convertDateToInt = (date: Date): number => parseInt(moment(date).clone().format('YYYYMMDD'), 10); +export const convertIntToDate = (intValue: number): Date => moment(intValue, 'YYYYMMDD').clone().toDate(); +export const diffBetweenDays = (start: string | number | Date, end: string | number | Date): number => moment(new Date(start)).clone().diff(new Date(end), 'days'); +export const diffBetweenDaysInclusive = (start: string | number | Date, end: string | number | Date): number => diffBetweenDays(start, end) + 1; + +export const getTotalOfWeekItems = >(weekItems: T[], property: keyof T): number => weekItems.reduce((acc, item) => { + acc += item[property]; + return acc; +}, 0); + +export function transformDatesForAPI(start: string): { start: Date; end: undefined }; +export function transformDatesForAPI(start: string, end: string): { start: Date; end: Date }; +export function transformDatesForAPI(start: string, end?: string): { start: Date; end: Date | undefined } { + return { + start: mapDateForAPI(start), + end: end ? mapDateForAPI(end) : undefined, + }; +} diff --git a/ee/app/engagement-dashboard/server/lib/messages.js b/ee/server/lib/engagementDashboard/messages.ts similarity index 56% rename from ee/app/engagement-dashboard/server/lib/messages.js rename to ee/server/lib/engagementDashboard/messages.ts index e49443dc5829..645011a83ae8 100644 --- a/ee/app/engagement-dashboard/server/lib/messages.js +++ b/ee/server/lib/engagementDashboard/messages.ts @@ -1,69 +1,79 @@ import moment from 'moment'; -import AnalyticsRaw from '../../../../../app/models/server/raw/Analytics'; -import { roomTypes } from '../../../../../app/utils'; -import { Messages } from '../../../../../app/models/server/raw'; -import { Analytics } from '../../../../../app/models/server'; +import { roomTypes } from '../../../../app/utils/server'; +import { Messages, Analytics } from '../../../../app/models/server/raw'; import { convertDateToInt, diffBetweenDaysInclusive, convertIntToDate, getTotalOfWeekItems } from './date'; +import { IDirectMessageRoom, IRoom } from '../../../../definition/IRoom'; +import { IMessage } from '../../../../definition/IMessage'; -export const handleMessagesSent = (message, room) => { +export const handleMessagesSent = (message: IMessage, room: IRoom): IMessage => { const roomTypesToShow = roomTypes.getTypesToShowOnDashboard(); if (!roomTypesToShow.includes(room.t)) { return message; } - Promise.await(AnalyticsRaw.saveMessageSent({ + Promise.await(Analytics.saveMessageSent({ date: convertDateToInt(message.ts), room, })); return message; }; -export const handleMessagesDeleted = (message, room) => { +export const handleMessagesDeleted = (message: IMessage, room: IRoom): IMessage => { const roomTypesToShow = roomTypes.getTypesToShowOnDashboard(); if (!roomTypesToShow.includes(room.t)) { - return; + return message; } - Promise.await(AnalyticsRaw.saveMessageDeleted({ + Promise.await(Analytics.saveMessageDeleted({ date: convertDateToInt(message.ts), room, })); return message; }; -export const fillFirstDaysOfMessagesIfNeeded = async (date) => { - const messagesFromAnalytics = await AnalyticsRaw.findByTypeBeforeDate({ +export const fillFirstDaysOfMessagesIfNeeded = async (date: Date): Promise => { + const messagesFromAnalytics = await Analytics.findByTypeBeforeDate({ type: 'messages', date: convertDateToInt(date), }).toArray(); if (!messagesFromAnalytics.length) { - const startOfPeriod = moment(convertIntToDate(date)).subtract(90, 'days').toDate(); + const startOfPeriod = moment(date).subtract(90, 'days').toDate(); const messages = await Messages.getTotalOfMessagesSentByDate({ start: startOfPeriod, end: date, }); - messages.forEach((message) => Analytics.insert({ + await Promise.all(messages.map((message) => Analytics.insertOne({ ...message, date: parseInt(message.date), - })); + }))); } }; -export const findWeeklyMessagesSentData = async ({ start, end }) => { +export const findWeeklyMessagesSentData = async ({ start, end }: { start: Date; end: Date }): Promise<{ + days: { day: Date; messages: number }[]; + period: { + count: number; + variation: number; + }; + yesterday: { + count: number; + variation: number; + }; +}> => { const daysBetweenDates = diffBetweenDaysInclusive(end, start); const endOfLastWeek = moment(start).clone().subtract(1, 'days').toDate(); const startOfLastWeek = moment(endOfLastWeek).clone().subtract(daysBetweenDates, 'days').toDate(); const today = convertDateToInt(end); const yesterday = convertDateToInt(moment(end).clone().subtract(1, 'days').toDate()); - const currentPeriodMessages = await AnalyticsRaw.getMessagesSentTotalByDate({ + const currentPeriodMessages = await Analytics.getMessagesSentTotalByDate({ start: convertDateToInt(start), end: convertDateToInt(end), options: { count: daysBetweenDates, sort: { _id: -1 } }, - }); - const lastPeriodMessages = await AnalyticsRaw.getMessagesSentTotalByDate({ + }).toArray(); + const lastPeriodMessages = await Analytics.getMessagesSentTotalByDate({ start: convertDateToInt(startOfLastWeek), end: convertDateToInt(endOfLastWeek), options: { count: daysBetweenDates, sort: { _id: -1 } }, - }); + }).toArray(); const yesterdayMessages = (currentPeriodMessages.find((item) => item._id === yesterday) || {}).messages || 0; const todayMessages = (currentPeriodMessages.find((item) => item._id === today) || {}).messages || 0; const currentPeriodTotalOfMessages = getTotalOfWeekItems(currentPeriodMessages, 'messages'); @@ -81,25 +91,38 @@ export const findWeeklyMessagesSentData = async ({ start, end }) => { }; }; -export const findMessagesSentOrigin = async ({ start, end }) => { - const origins = await AnalyticsRaw.getMessagesOrigin({ +export const findMessagesSentOrigin = async ({ start, end }: { start: Date; end: Date }): Promise<{ + origins: { + t: IRoom['t']; + messages: number; + }[]; +}> => { + const origins = await Analytics.getMessagesOrigin({ start: convertDateToInt(start), end: convertDateToInt(end), - }); - const roomTypesToShow = roomTypes.getTypesToShowOnDashboard(); + }).toArray(); + const roomTypesToShow: IRoom['t'][] = roomTypes.getTypesToShowOnDashboard() as IRoom['t'][]; const responseTypes = origins.map((origin) => origin.t); - const missingTypes = roomTypesToShow.filter((type) => !responseTypes.includes(type)); + const missingTypes = roomTypesToShow.filter((type): type is IRoom['t'] => !responseTypes.includes(type)); if (missingTypes.length) { missingTypes.forEach((type) => origins.push({ messages: 0, t: type })); } + return { origins }; }; -export const findTopFivePopularChannelsByMessageSentQuantity = async ({ start, end }) => { - const channels = await AnalyticsRaw.getMostPopularChannelsByMessagesSentQuantity({ +export const findTopFivePopularChannelsByMessageSentQuantity = async ({ start, end }: { start: Date; end: Date }): Promise<{ + channels: { + t: IRoom['t']; + messages: number; + name: IRoom['name'] | IRoom['fname']; + usernames?: IDirectMessageRoom['usernames']; + }[]; +}> => { + const channels = await Analytics.getMostPopularChannelsByMessagesSentQuantity({ start: convertDateToInt(start), end: convertDateToInt(end), options: { count: 5, sort: { messages: -1 } }, - }); + }).toArray(); return { channels }; }; diff --git a/ee/server/lib/engagementDashboard/startup.ts b/ee/server/lib/engagementDashboard/startup.ts new file mode 100644 index 000000000000..8aa81c16c4ac --- /dev/null +++ b/ee/server/lib/engagementDashboard/startup.ts @@ -0,0 +1,28 @@ +import { fillFirstDaysOfMessagesIfNeeded, handleMessagesDeleted, handleMessagesSent } from './messages'; +import { fillFirstDaysOfUsersIfNeeded, handleUserCreated } from './users'; +import { callbacks } from '../../../../app/callbacks/lib/callbacks'; +import { Permissions } from '../../../../app/models/server/raw'; + +export const attachCallbacks = (): void => { + callbacks.add('afterSaveMessage', handleMessagesSent, callbacks.priority.MEDIUM, 'engagementDashboard.afterSaveMessage'); + callbacks.add('afterDeleteMessage', handleMessagesDeleted, callbacks.priority.MEDIUM, 'engagementDashboard.afterDeleteMessage'); + callbacks.add('afterCreateUser', handleUserCreated, callbacks.priority.MEDIUM, 'engagementDashboard.afterCreateUser'); +}; + +export const detachCallbacks = (): void => { + callbacks.remove('afterSaveMessage', 'engagementDashboard.afterSaveMessage'); + callbacks.remove('afterDeleteMessage', 'engagementDashboard.afterDeleteMessage'); + callbacks.remove('afterCreateUser', 'engagementDashboard.afterCreateUser'); +}; + +export const prepareAnalytics = async (): Promise => { + const now = new Date(); + await Promise.all([ + fillFirstDaysOfUsersIfNeeded(now), + fillFirstDaysOfMessagesIfNeeded(now), + ]); +}; + +export const prepareAuthorization = async (): Promise => { + Permissions.create('view-engagement-dashboard', ['admin']); +}; diff --git a/ee/server/lib/engagementDashboard/users.ts b/ee/server/lib/engagementDashboard/users.ts new file mode 100644 index 000000000000..8e3faa3ff74e --- /dev/null +++ b/ee/server/lib/engagementDashboard/users.ts @@ -0,0 +1,149 @@ +import moment from 'moment'; + +import { Users, Analytics, Sessions } from '../../../../app/models/server/raw'; +import { convertDateToInt, diffBetweenDaysInclusive, getTotalOfWeekItems, convertIntToDate } from './date'; +import { IUser } from '../../../../definition/IUser'; + +export const handleUserCreated = (user: IUser): IUser => { + if (user.roles?.includes('anonymous')) { + return user; + } + + Promise.await(Analytics.saveUserData({ + date: convertDateToInt(user.createdAt), + })); + + return user; +}; + +export const fillFirstDaysOfUsersIfNeeded = async (date: Date): Promise => { + const usersFromAnalytics = await Analytics.findByTypeBeforeDate({ + type: 'users', + date: convertDateToInt(date), + }).toArray(); + if (!usersFromAnalytics.length) { + const startOfPeriod = moment(date).subtract(90, 'days').toDate(); + const users = await Users.getTotalOfRegisteredUsersByDate({ + start: startOfPeriod, + end: date, + }); + users.forEach((user) => Analytics.insertOne({ + ...user, + date: parseInt(user.date), + })); + } +}; + +export const findWeeklyUsersRegisteredData = async ({ start, end }: { start: Date; end: Date }): Promise<{ + days: { day: Date; users: number }[]; + period: { + count: number; + variation: number; + }; + yesterday: { + count: number; + variation: number; + }; +}> => { + const daysBetweenDates = diffBetweenDaysInclusive(end, start); + const endOfLastWeek = moment(start).clone().subtract(1, 'days').toDate(); + const startOfLastWeek = moment(endOfLastWeek).clone().subtract(daysBetweenDates, 'days').toDate(); + const today = convertDateToInt(end); + const yesterday = convertDateToInt(moment(end).clone().subtract(1, 'days').toDate()); + const currentPeriodUsers = await Analytics.getTotalOfRegisteredUsersByDate({ + start: convertDateToInt(start), + end: convertDateToInt(end), + options: { count: daysBetweenDates, sort: { _id: -1 } }, + }).toArray(); + const lastPeriodUsers = await Analytics.getTotalOfRegisteredUsersByDate({ + start: convertDateToInt(startOfLastWeek), + end: convertDateToInt(endOfLastWeek), + options: { count: daysBetweenDates, sort: { _id: -1 } }, + }).toArray(); + const yesterdayUsers = (currentPeriodUsers.find((item) => item._id === yesterday) || {}).users || 0; + const todayUsers = (currentPeriodUsers.find((item) => item._id === today) || {}).users || 0; + const currentPeriodTotalUsers = getTotalOfWeekItems(currentPeriodUsers, 'users'); + const lastPeriodTotalUsers = getTotalOfWeekItems(lastPeriodUsers, 'users'); + return { + days: currentPeriodUsers.map((day) => ({ day: convertIntToDate(day._id), users: day.users })), + period: { + count: currentPeriodTotalUsers, + variation: currentPeriodTotalUsers - lastPeriodTotalUsers, + }, + yesterday: { + count: yesterdayUsers, + variation: todayUsers - yesterdayUsers, + }, + }; +}; + +const createDestructuredDate = (input: moment.MomentInput): { + year: number; + month: number; + day: number; +} => { + const date = moment(input); + + return { + year: date.year(), + month: date.month() + 1, + day: date.date(), + }; +}; + +export const findActiveUsersMonthlyData = async ({ start, end }: { start: Date; end: Date }): Promise<{ + month: { + day: number; + month: number; + year: number; + usersList: IUser['_id'][]; + users: number; + }[]; +}> => ({ + month: await Sessions.getActiveUsersOfPeriodByDayBetweenDates({ + start: createDestructuredDate(start), + end: createDestructuredDate(end), + }), +}); + +export const findBusiestsChatsInADayByHours = async ({ start }: { start: Date }): Promise<{ + hours: { + hour: number; + users: number; + }[]; +}> => ({ + hours: await Sessions.getBusiestTimeWithinHoursPeriod({ + start: moment(start).subtract(24, 'hours').toDate(), + end: start, + groupSize: 2, + }), +}); + +export const findBusiestsChatsWithinAWeek = async ({ start }: { start: Date }): Promise<{ + month: { + day: number; + month: number; + year: number; + users: number; + }[]; +}> => ({ + month: await Sessions.getTotalOfSessionsByDayBetweenDates({ + start: createDestructuredDate(moment(start).subtract(7, 'days')), + end: createDestructuredDate(start), + }), +}); + +export const findUserSessionsByHourWithinAWeek = async ({ start, end }: { start: Date; end: Date }): Promise<{ + week: { + hour: number; + day: number; + month: number; + year: number; + users: number; + }[]; +}> => ({ + week: await Sessions.getTotalOfSessionByHourAndDayBetweenDates({ + start, + end, + }), +}); diff --git a/ee/server/lib/ldap/Manager.ts b/ee/server/lib/ldap/Manager.ts index 7230dbdb6c6d..07d4821ebd46 100644 --- a/ee/server/lib/ldap/Manager.ts +++ b/ee/server/lib/ldap/Manager.ts @@ -8,16 +8,16 @@ import type { IRole } from '../../../../definition/IRole'; import { IImportUser } from '../../../../definition/IImportUser'; import { ImporterAfterImportCallback } from '../../../../app/importer/server/definitions/IConversionCallbacks'; import { settings } from '../../../../app/settings/server'; -import { Roles, Rooms } from '../../../../app/models/server'; +import { Rooms } from '../../../../app/models/server'; import { Users as UsersRaw, - Roles as RolesRaw, + Roles, Subscriptions as SubscriptionsRaw, } from '../../../../app/models/server/raw'; import { LDAPDataConverter } from '../../../../server/lib/ldap/DataConverter'; import { LDAPConnection } from '../../../../server/lib/ldap/Connection'; import { LDAPManager } from '../../../../server/lib/ldap/Manager'; -import { logger, searchLogger } from '../../../../server/lib/ldap/Logger'; +import { logger, searchLogger, mapLogger } from '../../../../server/lib/ldap/Logger'; import { templateVarHandler } from '../../../../app/utils/lib/templateVarHandler'; import { api } from '../../../../server/sdk/api'; import { addUserToRoom, removeUserFromRoom, createRoom } from '../../../../app/lib/server/functions'; @@ -191,8 +191,8 @@ export class LDAPEEManager extends LDAPManager { return; } - const roles = await RolesRaw.find({}, { - fields: { + const roles = await Roles.find({}, { + projection: { _updatedAt: 0, }, }).toArray() as Array; @@ -224,7 +224,7 @@ export class LDAPEEManager extends LDAPManager { logger.debug(`User role exists for mapping ${ ldapField } -> ${ roleName }`); if (await this.isUserInGroup(ldap, syncUserRolesBaseDN, syncUserRolesFilter, { dn, username }, ldapField)) { - if (Roles.addUserRoles(user._id, roleName)) { + if (await Roles.addUserRoles(user._id, roleName)) { this.broadcastRoleChange('added', roleName, user._id, username); } logger.debug(`Synced user group ${ roleName } from LDAP for ${ user.username }`); @@ -235,7 +235,7 @@ export class LDAPEEManager extends LDAPManager { continue; } - if (Roles.removeUserRoles(user._id, roleName)) { + if (await Roles.removeUserRoles(user._id, roleName)) { this.broadcastRoleChange('removed', roleName, user._id, username); } } @@ -409,36 +409,45 @@ export class LDAPEEManager extends LDAPManager { private static isUserDeactivated(ldapUser: ILDAPEntry): boolean { // Account locked by "Draft-behera-ldap-password-policy" if (ldapUser.pwdAccountLockedTime) { + mapLogger.debug('User account is locked by password policy (attribute pwdAccountLockedTime)'); return true; } // EDirectory: Account manually disabled by an admin if (ldapUser.loginDisabled) { + mapLogger.debug('User account was manually disabled by an admin (attribute loginDisabled)'); return true; } // Oracle: Account must not be allowed to authenticate if (ldapUser.orclIsEnabled && ldapUser.orclIsEnabled !== 'ENABLED') { + mapLogger.debug('User must not be allowed to authenticate (attribute orclIsEnabled)'); return true; } // Active Directory - Account locked automatically by security policies - if (ldapUser.lockoutTime) { - // Automatic unlock is disabled - if (!ldapUser.lockoutDuration) { - return true; - } + if (ldapUser.lockoutTime && ldapUser.lockoutTime !== '0') { + const lockoutTimeValue = Number(ldapUser.lockoutTime); + if (lockoutTimeValue && !isNaN(lockoutTimeValue)) { + // Automatic unlock is disabled + if (!ldapUser.lockoutDuration) { + mapLogger.debug('User account locked indefinitely by security policy (attribute lockoutTime)'); + return true; + } - const lockoutTime = new Date(Number(ldapUser.lockoutTime)); - lockoutTime.setMinutes(lockoutTime.getMinutes() + Number(ldapUser.lockoutDuration)); - // Account has not unlocked itself yet - if (lockoutTime.valueOf() > Date.now()) { - return true; + const lockoutTime = new Date(lockoutTimeValue); + lockoutTime.setMinutes(lockoutTime.getMinutes() + Number(ldapUser.lockoutDuration)); + // Account has not unlocked itself yet + if (lockoutTime.valueOf() > Date.now()) { + mapLogger.debug('User account locked temporarily by security policy (attribute lockoutTime)'); + return true; + } } } // Active Directory - Account disabled by an Admin if (ldapUser.userAccountControl && (ldapUser.userAccountControl & 2) === 2) { + mapLogger.debug('User account disabled by an admin (attribute userAccountControl)'); return true; } @@ -465,7 +474,7 @@ export class LDAPEEManager extends LDAPManager { } userData.deleted = deleted; - logger.debug(`${ deleted ? 'Deactivating' : 'Activating' } user ${ userData.name } (${ userData.username })`); + logger.info(`${ deleted ? 'Deactivating' : 'Activating' } user ${ userData.name } (${ userData.username })`); } public static copyCustomFields(ldapUser: ILDAPEntry, userData: IImportUser): void { diff --git a/ee/server/lib/oauth/Manager.ts b/ee/server/lib/oauth/Manager.ts index 9d8222c1ca68..b53b1de90db5 100644 --- a/ee/server/lib/oauth/Manager.ts +++ b/ee/server/lib/oauth/Manager.ts @@ -1,7 +1,8 @@ import { addUserRoles, removeUserFromRoles } from '../../../../app/authorization/server'; -import { Roles, Rooms } from '../../../../app/models/server'; +import { Rooms } from '../../../../app/models/server'; import { addUserToRoom, createRoom } from '../../../../app/lib/server/functions'; import { Logger } from '../../../../app/logger/server'; +import { Roles } from '../../../../app/models/server/raw'; export const logger = new Logger('OAuth'); @@ -34,7 +35,7 @@ export class OAuthEEManager { } } - static updateRolesFromSSO(user: Record, identity: Record, roleClaimName: string): void { + static updateRolesFromSSO(user: Record, identity: Record, roleClaimName: string, rolesToSync: string[]): void { if (user && identity && roleClaimName) { const rolesFromSSO = this.mapRolesFromSSO(identity, roleClaimName); @@ -42,19 +43,15 @@ export class OAuthEEManager { user.roles = []; } - const toRemove = user.roles.filter((val: any) => !rolesFromSSO.includes(val)); + const toRemove = user.roles.filter((val: any) => !rolesFromSSO.includes(val) && rolesToSync.includes(val)); - // loop through roles that user has that sso doesnt have and remove each one - toRemove.forEach(function(role: any) { - removeUserFromRoles(user._id, role); - }); + // remove all roles that the user has, but sso doesnt + removeUserFromRoles(user._id, toRemove); - const toAdd = rolesFromSSO.filter((val: any) => !user.roles.includes(val)); + const toAdd = rolesFromSSO.filter((val: any) => !user.roles.includes(val) && (!rolesToSync.length || rolesToSync.includes(val))); - // loop through sso roles and add the new ones - toAdd.forEach(function(role: any) { - addUserRoles(user._id, role); - }); + // add all roles that sso has, but the user doesnt + addUserRoles(user._id, toAdd); } } @@ -64,7 +61,7 @@ export class OAuthEEManager { if (identity && roleClaimName) { // Adding roles if (identity[roleClaimName] && Array.isArray(identity[roleClaimName])) { - roles = identity[roleClaimName].filter((val: string) => val !== 'offline_access' && val !== 'uma_authorization' && Roles.findOneByIdOrName(val)); + roles = identity[roleClaimName].filter((val: string) => val !== 'offline_access' && val !== 'uma_authorization' && Promise.await(Roles.findOneByIdOrName(val))); } } diff --git a/ee/server/services/ddp-streamer/Publication.ts b/ee/server/services/ddp-streamer/Publication.ts index 74e152417b4c..94eedc9769d3 100644 --- a/ee/server/services/ddp-streamer/Publication.ts +++ b/ee/server/services/ddp-streamer/Publication.ts @@ -1,9 +1,10 @@ import { EventEmitter } from 'events'; +import type { IPublication } from 'meteor/rocketchat:streamer'; + import { Server } from './Server'; import { Client } from './Client'; import { IPacket } from './types/IPacket'; -import { IPublication } from '../../../../server/modules/streamer/streamer.module'; export class Publication extends EventEmitter implements IPublication { _session: IPublication['_session']; diff --git a/ee/server/services/ddp-streamer/Streamer.ts b/ee/server/services/ddp-streamer/Streamer.ts index 2932b52a0917..387ff123bcf7 100644 --- a/ee/server/services/ddp-streamer/Streamer.ts +++ b/ee/server/services/ddp-streamer/Streamer.ts @@ -1,9 +1,10 @@ import WebSocket from 'ws'; +import type { DDPSubscription, Connection, TransformMessage } from 'meteor/rocketchat:streamer'; import { server } from './configureServer'; import { DDP_EVENTS } from './constants'; import { isEmpty } from './lib/utils'; -import { Streamer, DDPSubscription, Connection, StreamerCentral, TransformMessage } from '../../../../server/modules/streamer/streamer.module'; +import { Streamer, StreamerCentral } from '../../../../server/modules/streamer/streamer.module'; import { api } from '../../../../server/sdk/api'; StreamerCentral.on('broadcast', (name, eventName, args) => { diff --git a/ee/server/services/ddp-streamer/streams/index.ts b/ee/server/services/ddp-streamer/streams/index.ts index a34fd2757eec..8d058b1a87c2 100644 --- a/ee/server/services/ddp-streamer/streams/index.ts +++ b/ee/server/services/ddp-streamer/streams/index.ts @@ -14,10 +14,11 @@ const notifications = new NotificationsModule(Stream); getConnection() .then((db) => { + const Users = new UsersRaw(db.collection(Collections.User)); notifications.configure({ Rooms: new RoomsRaw(db.collection(Collections.Rooms)), - Subscriptions: new SubscriptionsRaw(db.collection(Collections.Subscriptions)), - Users: new UsersRaw(db.collection(Collections.User)), + Subscriptions: new SubscriptionsRaw(db.collection(Collections.Subscriptions), { Users }), + Users, Settings: new SettingsRaw(db.collection(Collections.Settings)), }); }); diff --git a/ee/server/services/ddp-streamer/types/ws.d.ts b/ee/server/services/ddp-streamer/types/ws.d.ts deleted file mode 100644 index b71218b72140..000000000000 --- a/ee/server/services/ddp-streamer/types/ws.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* eslint-disable no-redeclare */ -/* eslint-disable no-unused-vars */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import 'ws'; - -type FrameOptions = { - opcode: number; // The opcode - readOnly: boolean; // Specifies whether `data` can be modified - fin: boolean; // Specifies whether or not to set the FIN bit - mask: boolean; // Specifies whether or not to mask `data` - rsv1: boolean; // Specifies whether or not to set the RSV1 bit -} - -declare module 'ws' { - class Sender { - static frame(data: Buffer, options: FrameOptions): Buffer[]; - } -} diff --git a/ee/server/services/definition/externals/ws.d.ts b/ee/server/services/definition/externals/ws.d.ts new file mode 100644 index 000000000000..e6afc68181cb --- /dev/null +++ b/ee/server/services/definition/externals/ws.d.ts @@ -0,0 +1,13 @@ +import 'ws'; + +declare module 'ws' { + namespace Sender { + function frame(data: Buffer, options: { + opcode: number; // The opcode + readOnly: boolean; // Specifies whether `data` can be modified + fin: boolean; // Specifies whether or not to set the FIN bit + mask: boolean; // Specifies whether or not to mask `data` + rsv1: boolean; // Specifies whether or not to set the RSV1 bit + }): Buffer[]; + } +} diff --git a/ee/server/services/package-lock.json b/ee/server/services/package-lock.json index b88d36579b81..49fddd2ef26e 100644 --- a/ee/server/services/package-lock.json +++ b/ee/server/services/package-lock.json @@ -216,6 +216,12 @@ "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.4.tgz", "integrity": "sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw==", "dev": true + }, + "ws": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", + "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "dev": true } } }, @@ -228,6 +234,11 @@ "debug": "^4.3.1" } }, + "@rocket.chat/emitter": { + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/@rocket.chat/emitter/-/emitter-0.30.1.tgz", + "integrity": "sha512-BH1wMBo5AZwgWXIRm4k2M5rz9W7uDR+2xbfo/HP2bIjyTTq9mlU/20w0JBXAR7PaMf8gWgyfAWWGPi4ZBHA+ag==" + }, "@rocket.chat/string-helpers": { "version": "0.29.0", "resolved": "https://registry.npmjs.org/@rocket.chat/string-helpers/-/string-helpers-0.29.0.tgz", @@ -413,6 +424,17 @@ "debug": "4" } }, + "ajv": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.7.1.tgz", + "integrity": "sha512-gPpOObTO1QjbnN1sVMjJcp1TF9nggMfO4MBR5uQl6ZVTOaEPq5i4oq/6R9q2alMMPB3eg53wFv1RuJBLuxf3Hw==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, "amp": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", @@ -1219,6 +1241,11 @@ } } }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "fast-json-patch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.0.tgz", @@ -1670,6 +1697,11 @@ "pako": "^0.2.5" } }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -2484,6 +2516,11 @@ "once": "^1.3.1" } }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -2558,6 +2595,11 @@ "ttl": "^1.3.0" } }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, "require-in-the-middle": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.1.0.tgz", @@ -3064,6 +3106,14 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -3127,9 +3177,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.1.tgz", - "integrity": "sha512-2c6faOUH/nhoQN6abwMloF7Iyl0ZS2E9HGtsiLrWn0zOOMWlhtDmdf/uihDt6jnuCxgtwGBNy6Onsoy2s2O2Ow==" + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", + "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==" }, "xorshift": { "version": "0.2.1", diff --git a/ee/server/services/package.json b/ee/server/services/package.json index 242fc2c9bfb0..802ae72e2ead 100644 --- a/ee/server/services/package.json +++ b/ee/server/services/package.json @@ -6,7 +6,11 @@ "scripts": { "dev": "pm2 start ecosystem.config.js", "pm2": "pm2", - "start": "ts-node index.ts", + "start:account": "ts-node --files ./account/service.ts", + "start:authorization": "ts-node --files ./authorization/service.ts", + "start:ddp-streamer": "ts-node --files ./ddp-streamer/service.ts", + "start:presence": "ts-node --files ./presence/service.ts", + "start:stream-hub": "ts-node --files ./stream-hub/service.ts", "typecheck": "tsc --noEmit --skipLibCheck", "build": "tsc", "build-containers": "npm run build && docker-compose build && rm -rf ./dist", @@ -18,7 +22,9 @@ "author": "Rocket.Chat", "license": "MIT", "dependencies": { - "@rocket.chat/string-helpers": "^0.29.0", + "@rocket.chat/emitter": "^0.30.1", + "@rocket.chat/string-helpers": "^0.30.1", + "ajv": "^8.7.1", "bcrypt": "^5.0.1", "body-parser": "^1.19.0", "colorette": "^1.3.0", @@ -37,7 +43,7 @@ "sodium-plus": "^0.9.0", "underscore.string": "^3.3.5", "uuid": "^7.0.3", - "ws": "^7.5.1" + "ws": "^7.5.5" }, "devDependencies": { "@types/cookie": "^0.4.1", @@ -51,5 +57,9 @@ "pm2": "^5.1.1", "ts-node": "^10.0.0", "typescript": "^4.3.5" + }, + "volta": { + "node": "12.22.1", + "npm": "6.14.12" } } diff --git a/ee/server/services/stream-hub/StreamHub.ts b/ee/server/services/stream-hub/StreamHub.ts index b559e43a7f75..ebb4830ead12 100755 --- a/ee/server/services/stream-hub/StreamHub.ts +++ b/ee/server/services/stream-hub/StreamHub.ts @@ -31,13 +31,13 @@ export class StreamHub extends ServiceClass implements IServiceClass { const Rooms = new RoomsRaw(db.collection('rocketchat_room'), Trash); const Settings = new SettingsRaw(db.collection('rocketchat_settings'), Trash); const Users = new UsersRaw(UsersCol, Trash); - const UsersSessions = new UsersSessionsRaw(db.collection('usersSessions'), Trash); - const Subscriptions = new SubscriptionsRaw(db.collection('rocketchat_subscription'), Trash); + const UsersSessions = new UsersSessionsRaw(db.collection('usersSessions'), Trash, { preventSetUpdatedAt: true }); + const Subscriptions = new SubscriptionsRaw(db.collection('rocketchat_subscription'), { Users }, Trash); const LivechatInquiry = new LivechatInquiryRaw(db.collection('rocketchat_livechat_inquiry'), Trash); const LivechatDepartmentAgents = new LivechatDepartmentAgentsRaw(db.collection('rocketchat_livechat_department_agents'), Trash); const Messages = new MessagesRaw(db.collection('rocketchat_message'), Trash); const Permissions = new PermissionsRaw(db.collection('rocketchat_permissions'), Trash); - const Roles = new RolesRaw(db.collection('rocketchat_roles'), Trash, { Users, Subscriptions }); + const Roles = new RolesRaw(db.collection('rocketchat_roles'), { Users, Subscriptions }, Trash); const LoginServiceConfiguration = new LoginServiceConfigurationRaw(db.collection('meteor_accounts_loginServiceConfiguration'), Trash); const InstanceStatus = new InstanceStatusRaw(db.collection('instances'), Trash); const IntegrationHistory = new IntegrationHistoryRaw(db.collection('rocketchat_integration_history'), Trash); diff --git a/ee/server/services/tsconfig.json b/ee/server/services/tsconfig.json index c9da08939693..f80de2d7ff4c 100644 --- a/ee/server/services/tsconfig.json +++ b/ee/server/services/tsconfig.json @@ -3,7 +3,6 @@ "module": "CommonJS", "target": "es2018", "lib": ["esnext", "dom"], - "types" : ["node"], "allowJs": true, "checkJs": false, @@ -42,6 +41,10 @@ // "emitDecoratorMetadata": true, // "experimentalDecorators": true, }, + "include": [ + "./**/*", + "../../../definition" + ], "exclude": [ "./dist", "./ecosystem.config.js" diff --git a/ee/server/startup/engagementDashboard.ts b/ee/server/startup/engagementDashboard.ts new file mode 100644 index 000000000000..13822f136e8d --- /dev/null +++ b/ee/server/startup/engagementDashboard.ts @@ -0,0 +1,21 @@ +import { Meteor } from 'meteor/meteor'; + +import { onToggledFeature } from '../../app/license/server/license'; + +onToggledFeature('engagement-dashboard', { + up: () => Meteor.startup(async () => { + const { + prepareAnalytics, + prepareAuthorization, + attachCallbacks, + } = await import('../lib/engagementDashboard/startup'); + await prepareAuthorization(); + await prepareAnalytics(); + attachCallbacks(); + await import('../api/engagementDashboard'); + }), + down: () => Meteor.startup(async () => { + const { detachCallbacks } = await import('../lib/engagementDashboard/startup'); + detachCallbacks(); + }), +}); diff --git a/ee/server/startup/index.ts b/ee/server/startup/index.ts index 2a58ffbd4b4b..91cfb4839c7c 100644 --- a/ee/server/startup/index.ts +++ b/ee/server/startup/index.ts @@ -1 +1,2 @@ +import './engagementDashboard'; import './seatsCap'; diff --git a/imports/message-read-receipt/server/api/methods/getReadReceipts.js b/imports/message-read-receipt/server/api/methods/getReadReceipts.js index e13647249217..810c7db70d65 100644 --- a/imports/message-read-receipt/server/api/methods/getReadReceipts.js +++ b/imports/message-read-receipt/server/api/methods/getReadReceipts.js @@ -5,7 +5,7 @@ import { canAccessRoom } from '../../../../../app/authorization/server'; import { ReadReceipt } from '../../lib/ReadReceipt'; Meteor.methods({ - getReadReceipts({ messageId }) { + async getReadReceipts({ messageId }) { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getReadReceipts' }); } diff --git a/imports/message-read-receipt/server/lib/ReadReceipt.js b/imports/message-read-receipt/server/lib/ReadReceipt.js index ab30a835763b..82310014d5e5 100644 --- a/imports/message-read-receipt/server/lib/ReadReceipt.js +++ b/imports/message-read-receipt/server/lib/ReadReceipt.js @@ -1,13 +1,12 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; -import { ReadReceipts, Subscriptions, Messages, Rooms, Users, LivechatVisitors } from '../../../../app/models'; -import { settings } from '../../../../app/settings'; -import { roomTypes } from '../../../../app/utils'; +import { Subscriptions, Messages, Rooms, Users, LivechatVisitors } from '../../../../app/models/server'; +import { ReadReceipts } from '../../../../app/models/server/raw'; +import { settings } from '../../../../app/settings/server'; +import { roomTypes } from '../../../../app/utils/server'; import { SystemLogger } from '../../../../server/lib/logger/system'; -const rawReadReceipts = ReadReceipts.model.rawCollection(); - // debounced function by roomId, so multiple calls within 2 seconds to same roomId runs only once const list = {}; const debounceByRoomId = function(fn) { @@ -85,17 +84,21 @@ export const ReadReceipt = { } try { - await rawReadReceipts.insertMany(receipts); + await ReadReceipts.insertMany(receipts); } catch (e) { SystemLogger.error('Error inserting read receipts per user'); } } }, - getReceipts(message) { - return ReadReceipts.findByMessageId(message._id).map((receipt) => ({ + async getReceipts(message) { + const receipts = await ReadReceipts.findByMessageId(message._id).toArray(); + + return receipts.map((receipt) => ({ ...receipt, - user: receipt.token ? LivechatVisitors.getVisitorByToken(receipt.token, { fields: { username: 1, name: 1 } }) : Users.findOneById(receipt.userId, { fields: { username: 1, name: 1 } }), + user: receipt.token + ? LivechatVisitors.getVisitorByToken(receipt.token, { fields: { username: 1, name: 1 } }) + : Users.findOneById(receipt.userId, { fields: { username: 1, name: 1 } }), })); }, }; diff --git a/mocha_end_to_end.opts.js b/mocha_end_to_end.opts.js deleted file mode 100644 index 36850cb81209..000000000000 --- a/mocha_end_to_end.opts.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -module.exports = { - require: [ - 'babel-mocha-es6-compiler', - 'babel-polyfill', - ], - reporter: 'spec', - ui: 'bdd', - extension: 'js,ts', - timeout: 10000, - bail: true, - file: 'tests/end-to-end/teardown.js', - spec: [ - 'tests/end-to-end/api/*.js', - 'tests/end-to-end/apps/*.js', - ], -}; diff --git a/package-lock.json b/package-lock.json index e83cf8126066..ae0af47f0571 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "Rocket.Chat", - "version": "4.1.2", + "version": "4.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -8,7 +8,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, "requires": { "@babel/highlight": "^7.0.0" } @@ -223,12 +222,12 @@ } }, "@babel/generator": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.4.tgz", - "integrity": "sha512-d3itta0tu+UayjEORPNz6e1T3FtvWlP5N4V5M+lhp/CxT4oAA7/NcScnpRyspUMLK6tu9MNHmQHxRykuN2R7hw==", + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz", + "integrity": "sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==", "dev": true, "requires": { - "@babel/types": "^7.15.4", + "@babel/types": "^7.15.6", "jsesc": "^2.5.1", "source-map": "^0.5.0" } @@ -1701,7 +1700,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", - "dev": true, "requires": { "chalk": "^2.0.0", "esutils": "^2.0.2", @@ -1712,7 +1710,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -1721,7 +1718,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -1732,7 +1728,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -1740,14 +1735,12 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -1755,9 +1748,9 @@ } }, "@babel/parser": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.7.tgz", - "integrity": "sha512-rycZXvQ+xS9QyIcJ9HXeDWf1uxqlbVFAUq0Rq0dbc50Zb/+wUe/ehyfzGfm9KZZF0kBejYgxltBXocP+gKdL2g==", + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", + "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", "dev": true }, "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { @@ -1858,9 +1851,9 @@ } }, "@babel/plugin-proposal-decorators": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.15.4.tgz", - "integrity": "sha512-WNER+YLs7avvRukEddhu5PSfSaMMimX2xBFgLQS7Bw16yrUxJGWidO9nQp+yLy9MVybg5Ba3BlhAw+BkdhpDmg==", + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.15.8.tgz", + "integrity": "sha512-5n8+xGK7YDrXF+WAORg3P7LlCCdiaAyKLZi22eP2BwTy4kJ0kFUMMDCj4nQ8YrKyNZgjhU/9eRVqONnjB3us8g==", "dev": true, "requires": { "@babel/helper-create-class-features-plugin": "^7.15.4", @@ -2920,9 +2913,9 @@ } }, "@babel/plugin-transform-typescript": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.15.4.tgz", - "integrity": "sha512-sM1/FEjwYjXvMwu1PJStH11kJ154zd/lpY56NQJ5qH2D0mabMv1CAy/kdvS9RP4Xgfj9fBBA3JiSLdDHgXdzOA==", + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.15.8.tgz", + "integrity": "sha512-ZXIkJpbaf6/EsmjeTbiJN/yMxWPFWvlr7sEG1P95Xb4S4IBcrf2n7s/fItIhsAmOf8oSh3VJPDppO6ExfAfKRQ==", "dev": true, "requires": { "@babel/helper-create-class-features-plugin": "^7.15.4", @@ -3268,9 +3261,9 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", "dev": true, "requires": { "@babel/highlight": "^7.14.5" @@ -3336,9 +3329,9 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", "dev": true, "requires": { "@babel/highlight": "^7.14.5" @@ -3429,9 +3422,9 @@ } }, "@base2/pretty-print-object": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@base2/pretty-print-object/-/pretty-print-object-1.0.0.tgz", - "integrity": "sha512-4Th98KlMHr5+JkxfcoDT//6vY8vM+iSPrLNpHhRyLx2CFYi8e2RfqPLdpbnpo0Q5lQC5hNB79yes07zb02fvCw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz", + "integrity": "sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==", "dev": true }, "@bcoe/v8-coverage": { @@ -4439,9 +4432,9 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", "dev": true, "requires": { "@babel/highlight": "^7.14.5" @@ -5086,7 +5079,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "requires": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -5095,14 +5087,12 @@ "@nodelib/fs.stat": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" }, "@nodelib/fs.walk": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz", "integrity": "sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA==", - "dev": true, "requires": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -5433,13 +5423,13 @@ "integrity": "sha512-XZi0UitfOnzQOUnk0flqyhqWtRqpoPToZzBvhzIla1r+7bLH7dBxjHlbGjT1EOQ1Xso0hGrbZKLxGnRmakJx8g==" }, "@rocket.chat/livechat": { - "version": "1.9.6", - "resolved": "https://registry.npmjs.org/@rocket.chat/livechat/-/livechat-1.9.6.tgz", - "integrity": "sha512-JKS07hypHjEwzcdvCwxPwq6mRkYL6Citotsra9iAPf0YXauZVTYTU6L4q4BWMssMv+jodCmQpM2bmiJKckSwWQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rocket.chat/livechat/-/livechat-1.10.0.tgz", + "integrity": "sha512-pN3p20GoBeISigjNT+F3lmzA1XRhsADGNmgmOQavzzUWZl6borbcRQLoaqGKToGGnZNJRRRa/ArxCmwI9ylJMQ==", "dev": true, "requires": { "@kossnocorp/desvg": "^0.2.0", - "@rocket.chat/sdk": "^1.0.0-alpha.41", + "@rocket.chat/sdk": "^1.0.0-alpha.42", "@rocket.chat/ui-kit": "^0.14.1", "css-vars-ponyfill": "^2.3.2", "date-fns": "^2.15.0", @@ -5702,17 +5692,17 @@ } }, "@storybook/addon-actions": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.3.9.tgz", - "integrity": "sha512-r02sSxkd2csKb69PVi8541e6hYGJ+FXOEBUnvLPyf5IULs7tRWPJekNd6cHxP1hoUBnmmj2f8CXmmbsXzr6rrQ==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.3.12.tgz", + "integrity": "sha512-mzuN4Ano4eyicwycM2PueGzzUCAEzt9/6vyptWEIVJu0sjK0J9KtBRlqFi1xGQxmCfimDR/n/vWBBkc7fp2uJA==", "dev": true, "requires": { - "@storybook/addons": "6.3.9", - "@storybook/api": "6.3.9", - "@storybook/client-api": "6.3.9", - "@storybook/components": "6.3.9", - "@storybook/core-events": "6.3.9", - "@storybook/theming": "6.3.9", + "@storybook/addons": "6.3.12", + "@storybook/api": "6.3.12", + "@storybook/client-api": "6.3.12", + "@storybook/components": "6.3.12", + "@storybook/core-events": "6.3.12", + "@storybook/theming": "6.3.12", "core-js": "^3.8.2", "fast-deep-equal": "^3.1.3", "global": "^4.4.0", @@ -5741,17 +5731,17 @@ } }, "@storybook/addon-backgrounds": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-6.3.9.tgz", - "integrity": "sha512-gqKLRZy/CdeZHcZz6ktz8SjzbV9WYOkhcgHb2kncLOp3OQ6St2VWHmBVitVpiN8V/ClL/lvLKPGKy7szKUwQRA==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-6.3.12.tgz", + "integrity": "sha512-51cHBx0HV7K/oRofJ/1pE05qti6sciIo8m4iPred1OezXIrJ/ckzP+gApdaUdzgcLAr6/MXQWLk0sJuImClQ6w==", "dev": true, "requires": { - "@storybook/addons": "6.3.9", - "@storybook/api": "6.3.9", - "@storybook/client-logger": "6.3.9", - "@storybook/components": "6.3.9", - "@storybook/core-events": "6.3.9", - "@storybook/theming": "6.3.9", + "@storybook/addons": "6.3.12", + "@storybook/api": "6.3.12", + "@storybook/client-logger": "6.3.12", + "@storybook/components": "6.3.12", + "@storybook/core-events": "6.3.12", + "@storybook/theming": "6.3.12", "core-js": "^3.8.2", "global": "^4.4.0", "memoizerific": "^1.11.3", @@ -5769,25 +5759,25 @@ } }, "@storybook/addon-controls": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-6.3.9.tgz", - "integrity": "sha512-B++fteuGzv1DUD3hKW3wTXtN4CBWpv4HzpLqng6L/xLfTZeOd39F73ooi/NmsoVO4vUl4ywrq0/QiHiXnp1kIQ==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-6.3.12.tgz", + "integrity": "sha512-WO/PbygE4sDg3BbstJ49q0uM3Xu5Nw4lnHR5N4hXSvRAulZt1d1nhphRTHjfX+CW+uBcfzkq9bksm6nKuwmOyw==", "dev": true, "requires": { - "@storybook/addons": "6.3.9", - "@storybook/api": "6.3.9", - "@storybook/client-api": "6.3.9", - "@storybook/components": "6.3.9", - "@storybook/node-logger": "6.3.9", - "@storybook/theming": "6.3.9", + "@storybook/addons": "6.3.12", + "@storybook/api": "6.3.12", + "@storybook/client-api": "6.3.12", + "@storybook/components": "6.3.12", + "@storybook/node-logger": "6.3.12", + "@storybook/theming": "6.3.12", "core-js": "^3.8.2", "ts-dedent": "^2.0.0" }, "dependencies": { "@storybook/node-logger": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.9.tgz", - "integrity": "sha512-tAo7sLNDGkL20NNGjwFgsywrl5rf/ImJaD6DnhSJDncaMcDy5xOA5Fn1IbkQuWUoKexE+nCkTiiynoP4Nzcp4w==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.12.tgz", + "integrity": "sha512-iktOem/Ls2+dsZY9PhPeC6T1QhX/y7OInP88neLsqEPEbB2UXca3Ydv7OZBhBVbvN25W45b05MRzbtNUxYLNRw==", "dev": true, "requires": { "@types/npmlog": "^4.1.2", @@ -5843,9 +5833,9 @@ } }, "@storybook/addon-docs": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-6.3.9.tgz", - "integrity": "sha512-UgujxJei5xejFpAfW40TdoDJnnfeKUPrp6QWMjMRcMG3+uW69VWviw4p3qzS9phO5vQJsINpRT8Bu0aGz2TpJw==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-6.3.12.tgz", + "integrity": "sha512-iUrqJBMTOn2PgN8AWNQkfxfIPkh8pEg27t8UndMgfOpeGK/VWGw2UEifnA82flvntcilT4McxmVbRHkeBY9K5A==", "dev": true, "requires": { "@babel/core": "^7.12.10", @@ -5857,20 +5847,20 @@ "@mdx-js/loader": "^1.6.22", "@mdx-js/mdx": "^1.6.22", "@mdx-js/react": "^1.6.22", - "@storybook/addons": "6.3.9", - "@storybook/api": "6.3.9", - "@storybook/builder-webpack4": "6.3.9", - "@storybook/client-api": "6.3.9", - "@storybook/client-logger": "6.3.9", - "@storybook/components": "6.3.9", - "@storybook/core": "6.3.9", - "@storybook/core-events": "6.3.9", + "@storybook/addons": "6.3.12", + "@storybook/api": "6.3.12", + "@storybook/builder-webpack4": "6.3.12", + "@storybook/client-api": "6.3.12", + "@storybook/client-logger": "6.3.12", + "@storybook/components": "6.3.12", + "@storybook/core": "6.3.12", + "@storybook/core-events": "6.3.12", "@storybook/csf": "0.0.1", - "@storybook/csf-tools": "6.3.9", - "@storybook/node-logger": "6.3.9", - "@storybook/postinstall": "6.3.9", - "@storybook/source-loader": "6.3.9", - "@storybook/theming": "6.3.9", + "@storybook/csf-tools": "6.3.12", + "@storybook/node-logger": "6.3.12", + "@storybook/postinstall": "6.3.12", + "@storybook/source-loader": "6.3.12", + "@storybook/theming": "6.3.12", "acorn": "^7.4.1", "acorn-jsx": "^5.3.1", "acorn-walk": "^7.2.0", @@ -5895,9 +5885,9 @@ }, "dependencies": { "@storybook/node-logger": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.9.tgz", - "integrity": "sha512-tAo7sLNDGkL20NNGjwFgsywrl5rf/ImJaD6DnhSJDncaMcDy5xOA5Fn1IbkQuWUoKexE+nCkTiiynoP4Nzcp4w==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.12.tgz", + "integrity": "sha512-iktOem/Ls2+dsZY9PhPeC6T1QhX/y7OInP88neLsqEPEbB2UXca3Ydv7OZBhBVbvN25W45b05MRzbtNUxYLNRw==", "dev": true, "requires": { "@types/npmlog": "^4.1.2", @@ -6027,21 +6017,21 @@ } }, "@storybook/addon-essentials": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-6.3.9.tgz", - "integrity": "sha512-F5FP9eqSSX++Dh7n/jAxVbfKGSh58EDIj7YdDVlCvf0qc9wLscV3dMS0kEcx/pP7z293JA4ADnztITOVSzf+qQ==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-6.3.12.tgz", + "integrity": "sha512-PK0pPE0xkq00kcbBcFwu/5JGHQTu4GvLIHfwwlEGx6GWNQ05l6Q+1Z4nE7xJGv2PSseSx3CKcjn8qykNLe6O6g==", "dev": true, "requires": { - "@storybook/addon-actions": "6.3.9", - "@storybook/addon-backgrounds": "6.3.9", - "@storybook/addon-controls": "6.3.9", - "@storybook/addon-docs": "6.3.9", + "@storybook/addon-actions": "6.3.12", + "@storybook/addon-backgrounds": "6.3.12", + "@storybook/addon-controls": "6.3.12", + "@storybook/addon-docs": "6.3.12", "@storybook/addon-measure": "^2.0.0", - "@storybook/addon-toolbars": "6.3.9", - "@storybook/addon-viewport": "6.3.9", - "@storybook/addons": "6.3.9", - "@storybook/api": "6.3.9", - "@storybook/node-logger": "6.3.9", + "@storybook/addon-toolbars": "6.3.12", + "@storybook/addon-viewport": "6.3.12", + "@storybook/addons": "6.3.12", + "@storybook/api": "6.3.12", + "@storybook/node-logger": "6.3.12", "core-js": "^3.8.2", "regenerator-runtime": "^0.13.7", "storybook-addon-outline": "^1.4.1", @@ -6049,9 +6039,9 @@ }, "dependencies": { "@storybook/node-logger": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.9.tgz", - "integrity": "sha512-tAo7sLNDGkL20NNGjwFgsywrl5rf/ImJaD6DnhSJDncaMcDy5xOA5Fn1IbkQuWUoKexE+nCkTiiynoP4Nzcp4w==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.12.tgz", + "integrity": "sha512-iktOem/Ls2+dsZY9PhPeC6T1QhX/y7OInP88neLsqEPEbB2UXca3Ydv7OZBhBVbvN25W45b05MRzbtNUxYLNRw==", "dev": true, "requires": { "@types/npmlog": "^4.1.2", @@ -6191,16 +6181,16 @@ } }, "@storybook/addon-toolbars": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-6.3.9.tgz", - "integrity": "sha512-9LgjmHtmOwv5++nKEruVKvHfqfqz23uHr7aht9b3tLf/6oNuRoCakddyIunoxhxw6SIW8rUObgjt8bnGxBSu1g==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-6.3.12.tgz", + "integrity": "sha512-8GvP6zmAfLPRnYRARSaIwLkQClLIRbflRh4HZoFk6IMjQLXZb4NL3JS5OLFKG+HRMMU2UQzfoSDqjI7k7ptyRw==", "dev": true, "requires": { - "@storybook/addons": "6.3.9", - "@storybook/api": "6.3.9", - "@storybook/client-api": "6.3.9", - "@storybook/components": "6.3.9", - "@storybook/theming": "6.3.9", + "@storybook/addons": "6.3.12", + "@storybook/api": "6.3.12", + "@storybook/client-api": "6.3.12", + "@storybook/components": "6.3.12", + "@storybook/theming": "6.3.12", "core-js": "^3.8.2", "regenerator-runtime": "^0.13.7" }, @@ -6214,17 +6204,17 @@ } }, "@storybook/addon-viewport": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-6.3.9.tgz", - "integrity": "sha512-zQe6i4LU1elSB/+ey7UfSTU64HEjQL8Lx+eYZx0xe80An+lMEhcDF4EGSTCDXFUwIX2LqEf3lc/vAfhA881Mrw==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-6.3.12.tgz", + "integrity": "sha512-TRjyfm85xouOPmXxeLdEIzXLfJZZ1ePQ7p/5yphDGBHdxMU4m4qiZr8wYpUaxHsRu/UB3dKfaOyGT+ivogbnbw==", "dev": true, "requires": { - "@storybook/addons": "6.3.9", - "@storybook/api": "6.3.9", - "@storybook/client-logger": "6.3.9", - "@storybook/components": "6.3.9", - "@storybook/core-events": "6.3.9", - "@storybook/theming": "6.3.9", + "@storybook/addons": "6.3.12", + "@storybook/api": "6.3.12", + "@storybook/client-logger": "6.3.12", + "@storybook/components": "6.3.12", + "@storybook/core-events": "6.3.12", + "@storybook/theming": "6.3.12", "core-js": "^3.8.2", "global": "^4.4.0", "memoizerific": "^1.11.3", @@ -6241,17 +6231,17 @@ } }, "@storybook/addons": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.3.9.tgz", - "integrity": "sha512-5tRkeHgdb/I/rp3GBkxonDLVsA45Vpgh/vFrsecrS/98wkSYfPEhqrDGLOosJHFrN3J2pznAuNFaA05158uBsw==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.3.12.tgz", + "integrity": "sha512-UgoMyr7Qr0FS3ezt8u6hMEcHgyynQS9ucr5mAwZky3wpXRPFyUTmMto9r4BBUdqyUvTUj/LRKIcmLBfj+/l0Fg==", "dev": true, "requires": { - "@storybook/api": "6.3.9", - "@storybook/channels": "6.3.9", - "@storybook/client-logger": "6.3.9", - "@storybook/core-events": "6.3.9", - "@storybook/router": "6.3.9", - "@storybook/theming": "6.3.9", + "@storybook/api": "6.3.12", + "@storybook/channels": "6.3.12", + "@storybook/client-logger": "6.3.12", + "@storybook/core-events": "6.3.12", + "@storybook/router": "6.3.12", + "@storybook/theming": "6.3.12", "core-js": "^3.8.2", "global": "^4.4.0", "regenerator-runtime": "^0.13.7" @@ -6266,19 +6256,19 @@ } }, "@storybook/api": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/api/-/api-6.3.9.tgz", - "integrity": "sha512-lwen3jcY4YbnD8spAZrmXcToed/pwad9QpxkG0GNf6ctcOumN6HIK93fKeJ0vvPYc3v/uq1qKeLyTZ3NrgHQRg==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/api/-/api-6.3.12.tgz", + "integrity": "sha512-LScRXUeCWEW/OP+jiooNMQICVdusv7azTmULxtm72fhkXFRiQs2CdRNTiqNg46JLLC9z95f1W+pGK66X6HiiQA==", "dev": true, "requires": { "@reach/router": "^1.3.4", - "@storybook/channels": "6.3.9", - "@storybook/client-logger": "6.3.9", - "@storybook/core-events": "6.3.9", + "@storybook/channels": "6.3.12", + "@storybook/client-logger": "6.3.12", + "@storybook/core-events": "6.3.12", "@storybook/csf": "0.0.1", - "@storybook/router": "6.3.9", + "@storybook/router": "6.3.12", "@storybook/semver": "^7.3.2", - "@storybook/theming": "6.3.9", + "@storybook/theming": "6.3.12", "@types/reach__router": "^1.3.7", "core-js": "^3.8.2", "fast-deep-equal": "^3.1.3", @@ -6370,9 +6360,9 @@ } }, "@storybook/builder-webpack4": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/builder-webpack4/-/builder-webpack4-6.3.9.tgz", - "integrity": "sha512-/Ff0f3vmoCCB62jDvTSKO1BVaZIYNfbDxrHOT0o2NKFmuHwH96qZDwqPhntoAxDYcjJZ6r4+tVo2ktyE+QAGVg==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/builder-webpack4/-/builder-webpack4-6.3.12.tgz", + "integrity": "sha512-Dlm5Fc1svqpFDnVPZdAaEBiM/IDZHMV3RfEGbUTY/ZC0q8b/Ug1czzp/w0aTIjOFRuBDcG6IcplikaqHL8CJLg==", "dev": true, "requires": { "@babel/core": "^7.12.10", @@ -6396,20 +6386,20 @@ "@babel/preset-env": "^7.12.11", "@babel/preset-react": "^7.12.10", "@babel/preset-typescript": "^7.12.7", - "@storybook/addons": "6.3.9", - "@storybook/api": "6.3.9", - "@storybook/channel-postmessage": "6.3.9", - "@storybook/channels": "6.3.9", - "@storybook/client-api": "6.3.9", - "@storybook/client-logger": "6.3.9", - "@storybook/components": "6.3.9", - "@storybook/core-common": "6.3.9", - "@storybook/core-events": "6.3.9", - "@storybook/node-logger": "6.3.9", - "@storybook/router": "6.3.9", + "@storybook/addons": "6.3.12", + "@storybook/api": "6.3.12", + "@storybook/channel-postmessage": "6.3.12", + "@storybook/channels": "6.3.12", + "@storybook/client-api": "6.3.12", + "@storybook/client-logger": "6.3.12", + "@storybook/components": "6.3.12", + "@storybook/core-common": "6.3.12", + "@storybook/core-events": "6.3.12", + "@storybook/node-logger": "6.3.12", + "@storybook/router": "6.3.12", "@storybook/semver": "^7.3.2", - "@storybook/theming": "6.3.9", - "@storybook/ui": "6.3.9", + "@storybook/theming": "6.3.12", + "@storybook/ui": "6.3.12", "@types/node": "^14.0.10", "@types/webpack": "^4.41.26", "autoprefixer": "^9.8.6", @@ -6472,9 +6462,9 @@ } }, "@storybook/node-logger": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.9.tgz", - "integrity": "sha512-tAo7sLNDGkL20NNGjwFgsywrl5rf/ImJaD6DnhSJDncaMcDy5xOA5Fn1IbkQuWUoKexE+nCkTiiynoP4Nzcp4w==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.12.tgz", + "integrity": "sha512-iktOem/Ls2+dsZY9PhPeC6T1QhX/y7OInP88neLsqEPEbB2UXca3Ydv7OZBhBVbvN25W45b05MRzbtNUxYLNRw==", "dev": true, "requires": { "@types/npmlog": "^4.1.2", @@ -6513,9 +6503,9 @@ "dev": true }, "@types/node": { - "version": "14.17.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.20.tgz", - "integrity": "sha512-gI5Sl30tmhXsqkNvopFydP7ASc4c2cLfGNQrVKN3X90ADFWFsPEsotm/8JHSUJQKTHbwowAHtcJPeyVhtKv0TQ==", + "version": "14.17.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.29.tgz", + "integrity": "sha512-sd4CHI9eTJXTH2vF3RGtGkqvWRwhsSSUFsXD4oG38GZzSZ0tNPbWikd2AbOAcKxCXhOg57fL8FPxjpfSzb2pIQ==", "dev": true }, "ajv": { @@ -7007,14 +6997,14 @@ } }, "@storybook/channel-postmessage": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.3.9.tgz", - "integrity": "sha512-1kyBpKuHDaohX8btXmD3hdkosYWJFcVy8VhOe8hVhBHScXwxSb+5Fycy38IlAQE/PSrcw5cII9x6vMvtzK/ojA==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.3.12.tgz", + "integrity": "sha512-Ou/2Ga3JRTZ/4sSv7ikMgUgLTeZMsXXWLXuscz4oaYhmOqAU9CrJw0G1NitwBgK/+qC83lEFSLujHkWcoQDOKg==", "dev": true, "requires": { - "@storybook/channels": "6.3.9", - "@storybook/client-logger": "6.3.9", - "@storybook/core-events": "6.3.9", + "@storybook/channels": "6.3.12", + "@storybook/client-logger": "6.3.12", + "@storybook/core-events": "6.3.12", "core-js": "^3.8.2", "global": "^4.4.0", "qs": "^6.10.0", @@ -7033,9 +7023,9 @@ } }, "@storybook/channels": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-6.3.9.tgz", - "integrity": "sha512-ZeHXLFJ43Wn6HJMiGgKUWUMtKcXDoWxL50Qr5Wwbsnmtp2BX7R8aak/Vw9TVT46J86QXkdI3CAKAEvb6esiLRQ==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-6.3.12.tgz", + "integrity": "sha512-l4sA+g1PdUV8YCbgs47fIKREdEQAKNdQIZw0b7BfTvY9t0x5yfBywgQhYON/lIeiNGz2OlIuD+VUtqYfCtNSyw==", "dev": true, "requires": { "core-js": "^3.8.2", @@ -7044,16 +7034,16 @@ } }, "@storybook/client-api": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.3.9.tgz", - "integrity": "sha512-epHqkyQu8BSNecuK5yLGBooCC+SoX5HhED2i5TS5o85sO8lB4ujPMrgKqEH3oSKwiy6gHgafewdgs0nczoP2Lw==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.3.12.tgz", + "integrity": "sha512-xnW+lKKK2T774z+rOr9Wopt1aYTStfb86PSs9p3Fpnc2Btcftln+C3NtiHZl8Ccqft8Mz/chLGgewRui6tNI8g==", "dev": true, "requires": { - "@storybook/addons": "6.3.9", - "@storybook/channel-postmessage": "6.3.9", - "@storybook/channels": "6.3.9", - "@storybook/client-logger": "6.3.9", - "@storybook/core-events": "6.3.9", + "@storybook/addons": "6.3.12", + "@storybook/channel-postmessage": "6.3.12", + "@storybook/channels": "6.3.12", + "@storybook/client-logger": "6.3.12", + "@storybook/core-events": "6.3.12", "@storybook/csf": "0.0.1", "@types/qs": "^6.9.5", "@types/webpack-env": "^1.16.0", @@ -7087,9 +7077,9 @@ } }, "@storybook/client-logger": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.3.9.tgz", - "integrity": "sha512-oww+P062SaOQfsTphAQBL6xe5DCv78Po/f/ROk7iYGAbV8HcCCscpzyJSeLfus2CunFYS2ngPcllbvEnqWk7dQ==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.3.12.tgz", + "integrity": "sha512-zNDsamZvHnuqLznDdP9dUeGgQ9TyFh4ray3t1VGO7ZqWVZ2xtVCCXjDvMnOXI2ifMpX5UsrOvshIPeE9fMBmiQ==", "dev": true, "requires": { "core-js": "^3.8.2", @@ -7097,15 +7087,15 @@ } }, "@storybook/components": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/components/-/components-6.3.9.tgz", - "integrity": "sha512-bArMbnzK9esdrgYHG/WAHC+NIMmEzgypvaTs0oEG4lK3q1LiBdrCrLRSCd31oR3RT5a8e06QXZ1rla3OhuZrfg==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-6.3.12.tgz", + "integrity": "sha512-kdQt8toUjynYAxDLrJzuG7YSNL6as1wJoyzNUaCfG06YPhvIAlKo7le9tS2mThVFN5e9nbKrW3N1V1sp6ypZXQ==", "dev": true, "requires": { "@popperjs/core": "^2.6.0", - "@storybook/client-logger": "6.3.9", + "@storybook/client-logger": "6.3.12", "@storybook/csf": "0.0.1", - "@storybook/theming": "6.3.9", + "@storybook/theming": "6.3.12", "@types/color-convert": "^2.0.0", "@types/overlayscrollbars": "^1.12.0", "@types/react-syntax-highlighter": "11.0.5", @@ -7152,28 +7142,28 @@ } }, "@storybook/core": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/core/-/core-6.3.9.tgz", - "integrity": "sha512-A4Vp0tmFBMUBn3U9QKGFDZr0166YjD3oaR7uvR/PWrDPXwnxNXtXQvWeZrFAW4edcNZB8WllatkBg6cWGVKbQg==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/core/-/core-6.3.12.tgz", + "integrity": "sha512-FJm2ns8wk85hXWKslLWiUWRWwS9KWRq7jlkN6M9p57ghFseSGr4W71Orcoab4P3M7jI97l5yqBfppbscinE74g==", "dev": true, "requires": { - "@storybook/core-client": "6.3.9", - "@storybook/core-server": "6.3.9" + "@storybook/core-client": "6.3.12", + "@storybook/core-server": "6.3.12" } }, "@storybook/core-client": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/core-client/-/core-client-6.3.9.tgz", - "integrity": "sha512-EKajuMFaFHJJW4WKfY9s1lMLG1mcg7hB634M/jw/bi1IwK6RI4T/RNp8ptlixTkjnlV5i1dA9DGRGx1P8ZxCHQ==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/core-client/-/core-client-6.3.12.tgz", + "integrity": "sha512-8Smd9BgZHJpAdevLKQYinwtjSyCZAuBMoetP4P5hnn53mWl0NFbrHFaAdT+yNchDLZQUbf7Y18VmIqEH+RCR5w==", "dev": true, "requires": { - "@storybook/addons": "6.3.9", - "@storybook/channel-postmessage": "6.3.9", - "@storybook/client-api": "6.3.9", - "@storybook/client-logger": "6.3.9", - "@storybook/core-events": "6.3.9", + "@storybook/addons": "6.3.12", + "@storybook/channel-postmessage": "6.3.12", + "@storybook/client-api": "6.3.12", + "@storybook/client-logger": "6.3.12", + "@storybook/core-events": "6.3.12", "@storybook/csf": "0.0.1", - "@storybook/ui": "6.3.9", + "@storybook/ui": "6.3.12", "airbnb-js-shims": "^2.2.1", "ansi-to-html": "^0.6.11", "core-js": "^3.8.2", @@ -7204,9 +7194,9 @@ } }, "@storybook/core-common": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-6.3.9.tgz", - "integrity": "sha512-1dStGSXKuABour3jXrfAvMVLb31rNgOQVMowxaROaPPkP0qyZexpUA2OmOAci+MTmincYgcMPWqi/9Cf1D80qQ==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-6.3.12.tgz", + "integrity": "sha512-xlHs2QXELq/moB4MuXjYOczaxU64BIseHsnFBLyboJYN6Yso3qihW5RB7cuJlGohkjb4JwY74dvfT4Ww66rkBA==", "dev": true, "requires": { "@babel/core": "^7.12.10", @@ -7230,7 +7220,7 @@ "@babel/preset-react": "^7.12.10", "@babel/preset-typescript": "^7.12.7", "@babel/register": "^7.12.1", - "@storybook/node-logger": "6.3.9", + "@storybook/node-logger": "6.3.12", "@storybook/semver": "^7.3.2", "@types/glob-base": "^0.3.0", "@types/micromatch": "^4.0.1", @@ -7347,9 +7337,9 @@ } }, "@storybook/node-logger": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.9.tgz", - "integrity": "sha512-tAo7sLNDGkL20NNGjwFgsywrl5rf/ImJaD6DnhSJDncaMcDy5xOA5Fn1IbkQuWUoKexE+nCkTiiynoP4Nzcp4w==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.12.tgz", + "integrity": "sha512-iktOem/Ls2+dsZY9PhPeC6T1QhX/y7OInP88neLsqEPEbB2UXca3Ydv7OZBhBVbvN25W45b05MRzbtNUxYLNRw==", "dev": true, "requires": { "@types/npmlog": "^4.1.2", @@ -7382,9 +7372,9 @@ } }, "@types/node": { - "version": "14.17.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.20.tgz", - "integrity": "sha512-gI5Sl30tmhXsqkNvopFydP7ASc4c2cLfGNQrVKN3X90ADFWFsPEsotm/8JHSUJQKTHbwowAHtcJPeyVhtKv0TQ==", + "version": "14.17.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.29.tgz", + "integrity": "sha512-sd4CHI9eTJXTH2vF3RGtGkqvWRwhsSSUFsXD4oG38GZzSZ0tNPbWikd2AbOAcKxCXhOg57fL8FPxjpfSzb2pIQ==", "dev": true }, "ansi-styles": { @@ -7516,9 +7506,9 @@ } }, "fork-ts-checker-webpack-plugin": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.3.3.tgz", - "integrity": "sha512-S3uMSg8IsIvs0H6VAfojtbf6RcnEXxEpDMT2Q41M2l0m20JO8eA1t4cCJybvrasC8SvvPEtK4B8ztxxfLljhNg==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.4.0.tgz", + "integrity": "sha512-3I3wFkc4DbzaUDPWEi96wdYGu4EKtxBafhZYm0o4mX51d9bphAY4P3mBl8K5mFXFJqVzHfmdbm9kLGnm7vwwBg==", "dev": true, "requires": { "@babel/code-frame": "^7.8.3", @@ -7537,9 +7527,9 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", "dev": true, "requires": { "@babel/highlight": "^7.14.5" @@ -7766,27 +7756,27 @@ } }, "@storybook/core-events": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.3.9.tgz", - "integrity": "sha512-6ELOkroH0Oz7+OR1SqGMKAC1+ufituqSxDp08AyvrHPSYqK/db+P2kSCJBdqyUXTvt8lPvqlCOidkRhGrNB/+A==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.3.12.tgz", + "integrity": "sha512-SXfD7xUUMazaeFkB92qOTUV8Y/RghE4SkEYe5slAdjeocSaH7Nz2WV0rqNEgChg0AQc+JUI66no8L9g0+lw4Gw==", "dev": true, "requires": { "core-js": "^3.8.2" } }, "@storybook/core-server": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-6.3.9.tgz", - "integrity": "sha512-iUMtB0RqdX3YW7FMlg8lScUNrkfcbtLurH3hC+2CkJEailTRUQ8AjlwLc0gjIh0QgbsxUexo1iQW1NFuHBxpDw==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-6.3.12.tgz", + "integrity": "sha512-T/Mdyi1FVkUycdyOnhXvoo3d9nYXLQFkmaJkltxBFLzAePAJUSgAsPL9odNC3+p8Nr2/UDsDzvu/Ow0IF0mzLQ==", "dev": true, "requires": { "@discoveryjs/json-ext": "^0.5.3", - "@storybook/builder-webpack4": "6.3.9", - "@storybook/core-client": "6.3.9", - "@storybook/core-common": "6.3.9", - "@storybook/csf-tools": "6.3.9", - "@storybook/manager-webpack4": "6.3.9", - "@storybook/node-logger": "6.3.9", + "@storybook/builder-webpack4": "6.3.12", + "@storybook/core-client": "6.3.12", + "@storybook/core-common": "6.3.12", + "@storybook/csf-tools": "6.3.12", + "@storybook/manager-webpack4": "6.3.12", + "@storybook/node-logger": "6.3.12", "@storybook/semver": "^7.3.2", "@types/node": "^14.0.10", "@types/node-fetch": "^2.5.7", @@ -7817,9 +7807,9 @@ }, "dependencies": { "@storybook/node-logger": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.9.tgz", - "integrity": "sha512-tAo7sLNDGkL20NNGjwFgsywrl5rf/ImJaD6DnhSJDncaMcDy5xOA5Fn1IbkQuWUoKexE+nCkTiiynoP4Nzcp4w==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.12.tgz", + "integrity": "sha512-iktOem/Ls2+dsZY9PhPeC6T1QhX/y7OInP88neLsqEPEbB2UXca3Ydv7OZBhBVbvN25W45b05MRzbtNUxYLNRw==", "dev": true, "requires": { "@types/npmlog": "^4.1.2", @@ -7840,9 +7830,9 @@ } }, "@types/node": { - "version": "14.17.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.20.tgz", - "integrity": "sha512-gI5Sl30tmhXsqkNvopFydP7ASc4c2cLfGNQrVKN3X90ADFWFsPEsotm/8JHSUJQKTHbwowAHtcJPeyVhtKv0TQ==", + "version": "14.17.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.29.tgz", + "integrity": "sha512-sd4CHI9eTJXTH2vF3RGtGkqvWRwhsSSUFsXD4oG38GZzSZ0tNPbWikd2AbOAcKxCXhOg57fL8FPxjpfSzb2pIQ==", "dev": true }, "ansi-regex": { @@ -8068,9 +8058,9 @@ } }, "@storybook/csf-tools": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-6.3.9.tgz", - "integrity": "sha512-rgAcq/3x8HEIRJdEqw21YT6zDdvEoCUZOMS2XYISJstFZp3weK+PbQr5tsMefAoVjpsNCi9ypYSXMT6dxnNbPw==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-6.3.12.tgz", + "integrity": "sha512-wNrX+99ajAXxLo0iRwrqw65MLvCV6SFC0XoPLYrtBvyKr+hXOOnzIhO2f5BNEii8velpC2gl2gcLKeacpVYLqA==", "dev": true, "requires": { "@babel/generator": "^7.12.11", @@ -8138,20 +8128,20 @@ } }, "@storybook/manager-webpack4": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/manager-webpack4/-/manager-webpack4-6.3.9.tgz", - "integrity": "sha512-I5ckE4p1m8c1GcRi8BjXxqvdfo4gxX2Lhf7fH8udiSRO/MTyLeYHqmVmdKXcBpQRxHpmJ14oBoBBP5usWXgCVw==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/manager-webpack4/-/manager-webpack4-6.3.12.tgz", + "integrity": "sha512-OkPYNrHXg2yZfKmEfTokP6iKx4OLTr0gdI5yehi/bLEuQCSHeruxBc70Dxm1GBk1Mrf821wD9WqMXNDjY5Qtug==", "dev": true, "requires": { "@babel/core": "^7.12.10", "@babel/plugin-transform-template-literals": "^7.12.1", "@babel/preset-react": "^7.12.10", - "@storybook/addons": "6.3.9", - "@storybook/core-client": "6.3.9", - "@storybook/core-common": "6.3.9", - "@storybook/node-logger": "6.3.9", - "@storybook/theming": "6.3.9", - "@storybook/ui": "6.3.9", + "@storybook/addons": "6.3.12", + "@storybook/core-client": "6.3.12", + "@storybook/core-common": "6.3.12", + "@storybook/node-logger": "6.3.12", + "@storybook/theming": "6.3.12", + "@storybook/ui": "6.3.12", "@types/node": "^14.0.10", "@types/webpack": "^4.41.26", "babel-loader": "^8.2.2", @@ -8183,9 +8173,9 @@ }, "dependencies": { "@storybook/node-logger": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.9.tgz", - "integrity": "sha512-tAo7sLNDGkL20NNGjwFgsywrl5rf/ImJaD6DnhSJDncaMcDy5xOA5Fn1IbkQuWUoKexE+nCkTiiynoP4Nzcp4w==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.12.tgz", + "integrity": "sha512-iktOem/Ls2+dsZY9PhPeC6T1QhX/y7OInP88neLsqEPEbB2UXca3Ydv7OZBhBVbvN25W45b05MRzbtNUxYLNRw==", "dev": true, "requires": { "@types/npmlog": "^4.1.2", @@ -8202,9 +8192,9 @@ "dev": true }, "@types/node": { - "version": "14.17.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.20.tgz", - "integrity": "sha512-gI5Sl30tmhXsqkNvopFydP7ASc4c2cLfGNQrVKN3X90ADFWFsPEsotm/8JHSUJQKTHbwowAHtcJPeyVhtKv0TQ==", + "version": "14.17.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.29.tgz", + "integrity": "sha512-sd4CHI9eTJXTH2vF3RGtGkqvWRwhsSSUFsXD4oG38GZzSZ0tNPbWikd2AbOAcKxCXhOg57fL8FPxjpfSzb2pIQ==", "dev": true }, "ajv": { @@ -8782,27 +8772,27 @@ } }, "@storybook/postinstall": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/postinstall/-/postinstall-6.3.9.tgz", - "integrity": "sha512-XamQmpR56n2foah0Y3AcT2jn9t8rUdt7u1VfY92dM9zmGBUgL+2u4nsBcYWzWoFllAShengUUqrs1Ci7+rKpjw==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/postinstall/-/postinstall-6.3.12.tgz", + "integrity": "sha512-HkZ+abtZ3W6JbGPS6K7OSnNXbwaTwNNd5R02kRs4gV9B29XsBPDtFT6vIwzM3tmVQC7ihL5a8ceWp2OvzaNOuw==", "dev": true, "requires": { "core-js": "^3.8.2" } }, "@storybook/react": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-6.3.9.tgz", - "integrity": "sha512-LYlAURnKvFsf3CZG2CDvQwvlF/vy5VsKuNiNmyTk6zggmkbitXrwkuT7KoElYrz4REkyhd/HA6Uw6lfkPsyuHA==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-6.3.12.tgz", + "integrity": "sha512-c1Y/3/eNzye+ZRwQ3BXJux6pUMVt3lhv1/M9Qagl9JItP3jDSj5Ed3JHCgwEqpprP8mvNNXwEJ8+M7vEQyDuHg==", "dev": true, "requires": { "@babel/preset-flow": "^7.12.1", "@babel/preset-react": "^7.12.10", "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", - "@storybook/addons": "6.3.9", - "@storybook/core": "6.3.9", - "@storybook/core-common": "6.3.9", - "@storybook/node-logger": "6.3.9", + "@storybook/addons": "6.3.12", + "@storybook/core": "6.3.12", + "@storybook/core-common": "6.3.12", + "@storybook/node-logger": "6.3.12", "@storybook/react-docgen-typescript-plugin": "1.0.2-canary.253f8c1.0", "@storybook/semver": "^7.3.2", "@types/webpack-env": "^1.16.0", @@ -8822,9 +8812,9 @@ }, "dependencies": { "@storybook/node-logger": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.9.tgz", - "integrity": "sha512-tAo7sLNDGkL20NNGjwFgsywrl5rf/ImJaD6DnhSJDncaMcDy5xOA5Fn1IbkQuWUoKexE+nCkTiiynoP4Nzcp4w==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.12.tgz", + "integrity": "sha512-iktOem/Ls2+dsZY9PhPeC6T1QhX/y7OInP88neLsqEPEbB2UXca3Ydv7OZBhBVbvN25W45b05MRzbtNUxYLNRw==", "dev": true, "requires": { "@types/npmlog": "^4.1.2", @@ -9147,13 +9137,13 @@ } }, "@storybook/router": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/router/-/router-6.3.9.tgz", - "integrity": "sha512-uXNrZS9tsZr6fStIv/MHQfy3xSsc7RLYWbY4wkgZH+y5K97RtuwXgtbx7uyEpsQwse1Z4PikKu/ejN46F0oPGQ==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/router/-/router-6.3.12.tgz", + "integrity": "sha512-G/pNGCnrJRetCwyEZulHPT+YOcqEj/vkPVDTUfii2qgqukup6K0cjwgd7IukAURnAnnzTi1gmgFuEKUi8GE/KA==", "dev": true, "requires": { "@reach/router": "^1.3.4", - "@storybook/client-logger": "6.3.9", + "@storybook/client-logger": "6.3.12", "@types/reach__router": "^1.3.7", "core-js": "^3.8.2", "fast-deep-equal": "^3.1.3", @@ -9182,13 +9172,13 @@ } }, "@storybook/source-loader": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-6.3.9.tgz", - "integrity": "sha512-H0W4DK3EyXLLdzx5ThoXkC9mPKBAJUb8qoDL3jcTA1sQUDSOsxdosQIrJ97Ljcu21eQJs6egnxh3yPa1CWXTGA==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-6.3.12.tgz", + "integrity": "sha512-Lfe0LOJGqAJYkZsCL8fhuQOeFSCgv8xwQCt4dkcBd0Rw5zT2xv0IXDOiIOXGaWBMDtrJUZt/qOXPEPlL81Oaqg==", "dev": true, "requires": { - "@storybook/addons": "6.3.9", - "@storybook/client-logger": "6.3.9", + "@storybook/addons": "6.3.12", + "@storybook/client-logger": "6.3.12", "@storybook/csf": "0.0.1", "core-js": "^3.8.2", "estraverse": "^5.2.0", @@ -9220,15 +9210,15 @@ } }, "@storybook/theming": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-6.3.9.tgz", - "integrity": "sha512-vyMSLvEXrTC4rnUdWLUNmBNeOdBCl0Nt3R6y/laY+LQZ9Ljz/poRTrIYTkmenYieq4N7787s9zHmxvym/ZvKtw==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-6.3.12.tgz", + "integrity": "sha512-wOJdTEa/VFyFB2UyoqyYGaZdym6EN7RALuQOAMT6zHA282FBmKw8nL5DETHEbctpnHdcrMC/391teK4nNSrdOA==", "dev": true, "requires": { "@emotion/core": "^10.1.1", "@emotion/is-prop-valid": "^0.8.6", "@emotion/styled": "^10.0.27", - "@storybook/client-logger": "6.3.9", + "@storybook/client-logger": "6.3.12", "core-js": "^3.8.2", "deep-object-diff": "^1.1.0", "emotion-theming": "^10.0.27", @@ -9240,21 +9230,21 @@ } }, "@storybook/ui": { - "version": "6.3.9", - "resolved": "https://registry.npmjs.org/@storybook/ui/-/ui-6.3.9.tgz", - "integrity": "sha512-QyRwofApyHOvjWPXirNYFleSVsjluYl7QmZgkv+vT09sV6q0YS1M2YQiDjoPwSIG0OHvxNoY90yNHjx8aXo4gA==", + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/ui/-/ui-6.3.12.tgz", + "integrity": "sha512-PC2yEz4JMfarq7rUFbeA3hCA+31p5es7YPEtxLRvRwIZhtL0P4zQUfHpotb3KgWdoAIfZesAuoIQwMPQmEFYrw==", "dev": true, "requires": { "@emotion/core": "^10.1.1", - "@storybook/addons": "6.3.9", - "@storybook/api": "6.3.9", - "@storybook/channels": "6.3.9", - "@storybook/client-logger": "6.3.9", - "@storybook/components": "6.3.9", - "@storybook/core-events": "6.3.9", - "@storybook/router": "6.3.9", + "@storybook/addons": "6.3.12", + "@storybook/api": "6.3.12", + "@storybook/channels": "6.3.12", + "@storybook/client-logger": "6.3.12", + "@storybook/components": "6.3.12", + "@storybook/core-events": "6.3.12", + "@storybook/router": "6.3.12", "@storybook/semver": "^7.3.2", - "@storybook/theming": "6.3.9", + "@storybook/theming": "6.3.12", "@types/markdown-to-jsx": "^6.11.3", "copy-to-clipboard": "^3.3.1", "core-js": "^3.8.2", @@ -9375,6 +9365,145 @@ "unist-util-find-all-after": "^3.0.2" } }, + "@testing-library/dom": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.11.1.tgz", + "integrity": "sha512-3KQDyx9r0RKYailW2MiYrSSKEfH0GTkI51UGEvJenvcoDoeRYs0PZpi2SXqtnMClQvCqdtTTpOfFETDTVADpAg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^4.2.0", + "aria-query": "^5.0.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.4.4", + "pretty-format": "^27.0.2" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz", + "integrity": "sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true + }, + "@babel/highlight": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", + "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.15.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@testing-library/react": { + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.2.tgz", + "integrity": "sha512-ihQiEOklNyHIpo2Y8FREkyD1QAea054U0MVbwH1m8N9TxeFz+KoJ9LkqoKqJlzx2JDm56DVwaJ1r36JYxZM05g==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^8.0.0" + } + }, + "@testing-library/user-event": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", + "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5" + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -9423,6 +9552,12 @@ "@types/node": "*" } }, + "@types/aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==", + "dev": true + }, "@types/bad-words": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/bad-words/-/bad-words-3.0.1.tgz", @@ -9477,9 +9612,27 @@ "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" }, "@types/chai": { - "version": "4.2.19", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.19.tgz", - "integrity": "sha512-jRJgpRBuY+7izT7/WNXP/LsMO9YonsstuL+xuvycDyESpoDoIAsMd7suwpB4h9oEWB+ZlPTqJJ8EHomzNhwTPQ==" + "version": "4.2.22", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.22.tgz", + "integrity": "sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==" + }, + "@types/chai-datetime": { + "version": "0.0.37", + "resolved": "https://registry.npmjs.org/@types/chai-datetime/-/chai-datetime-0.0.37.tgz", + "integrity": "sha512-teAlKuUV2mxuN0hRxfSXnk7v5lDZUtQWMZ72pIvm5OJ8SuMmgjQgNiebha+MYr7EiSVCQxDY8yH1j7TIXy3nEQ==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, + "@types/chai-dom": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@types/chai-dom/-/chai-dom-0.0.11.tgz", + "integrity": "sha512-qLGx6pHrcCfjkpfuh8aBj7xBm8wo2F9aTgv9p+/X0zBAsR3c1wxJguDFnV5cXxUQJKY9BdwE6PVy7W4PT5IAlA==", + "dev": true, + "requires": { + "@types/chai": "*" + } }, "@types/chai-spies": { "version": "1.0.3", @@ -9525,6 +9678,12 @@ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, + "@types/cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", + "dev": true + }, "@types/dompurify": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.2.2.tgz", @@ -9535,9 +9694,9 @@ } }, "@types/ejson": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@types/ejson/-/ejson-2.1.2.tgz", - "integrity": "sha1-oMuiYNUAYxDch3kFRj2D1fPN8RI=", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/ejson/-/ejson-2.1.3.tgz", + "integrity": "sha512-Sd+XISmDWOypfDcsKeQkhykSMgYVAWJxhf7f0ySvfy2tYo+im26M/6FfqjCEiPSDAEugiuZKtA+wWeANKueWIg==", "dev": true }, "@types/elliptic": { @@ -9605,9 +9764,9 @@ } }, "@types/glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", "dev": true, "requires": { "@types/minimatch": "*", @@ -9692,9 +9851,9 @@ } }, "@types/jquery": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.1.tgz", - "integrity": "sha512-Tyctjh56U7eX2b9udu3wG853ASYP0uagChJcQJXLUXEU6C/JiW5qt5dl8ao01VRj1i5pgXPAf8f1mq4+FDLRQg==", + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.8.tgz", + "integrity": "sha512-cXk6NwqjDYg+UI9p2l3x0YmPa4m7RrXqmbK4IpVVpRJiYXU/QTo+UZrn54qfE1+9Gao4qpYqUnxm5ZCy2FTXAw==", "dev": true, "requires": { "@types/sizzle": "*" @@ -9711,6 +9870,15 @@ "@types/tough-cookie": "*" } }, + "@types/jsdom-global": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/jsdom-global/-/jsdom-global-3.0.2.tgz", + "integrity": "sha512-CFIPEDpO5vQWdQrVXrdSR2j5giiDuyb0hzZD04OJqMfizt7sh6WoqaLBdnP4w74yHDDcQkc0k5TzrXffhua3Jg==", + "dev": true, + "requires": { + "@types/jsdom": "*" + } + }, "@types/json-schema": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", @@ -9739,9 +9907,9 @@ "dev": true }, "@types/lodash": { - "version": "4.14.171", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.171.tgz", - "integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg==" + "version": "4.14.177", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.177.tgz", + "integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==" }, "@types/lodash.debounce": { "version": "4.0.6", @@ -9847,9 +10015,9 @@ } }, "@types/mocha": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.2.tgz", - "integrity": "sha512-Lwh0lzzqT5Pqh6z61P3c3P5nm6fzQK/MMHl9UKeneAeInVflBSz1O2EkX6gM6xfJd7FBXBY5purtLx7fUiZ7Hw==" + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.3.tgz", + "integrity": "sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==" }, "@types/mock-require": { "version": "2.0.0", @@ -9916,6 +10084,15 @@ } } }, + "@types/node-rsa": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/node-rsa/-/node-rsa-1.1.1.tgz", + "integrity": "sha512-itzxtaBgk4OMbrCawVCvas934waMZWjW17v7EYgFVlfYS/cl0/P7KZdojWCq9SDJMI5cnLQLUP8ayhVCTY8TEg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/nodemailer": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.2.tgz", @@ -9928,8 +10105,7 @@ "@types/normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", - "dev": true + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==" }, "@types/npmlog": { "version": "4.1.2", @@ -9985,6 +10161,12 @@ "@types/node": "*" } }, + "@types/photoswipe": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@types/photoswipe/-/photoswipe-4.1.2.tgz", + "integrity": "sha512-HA9TtCAQKToldgxRiyJ1DbsElg/cQV/SQ8COVjqIqghjy60Zxfh78E1WiFotthquqkS86nz13Za9wEbToe0svQ==", + "dev": true + }, "@types/pretty-hrtime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz", @@ -9992,9 +10174,9 @@ "dev": true }, "@types/prop-types": { - "version": "15.7.3", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", - "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", "dev": true }, "@types/psl": { @@ -10025,9 +10207,9 @@ } }, "@types/react": { - "version": "17.0.11", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.11.tgz", - "integrity": "sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA==", + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.35.tgz", + "integrity": "sha512-r3C8/TJuri/SLZiiwwxQoLAoavaczARfT9up9b4Jr65+ErAUX3MIkU0oMOQnrpfgHme8zIqZLX7O5nnjm5Wayw==", "dev": true, "requires": { "@types/prop-types": "*", @@ -10036,39 +10218,20 @@ }, "dependencies": { "csstype": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", - "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", + "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==", "dev": true } } }, "@types/react-dom": { - "version": "17.0.8", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.8.tgz", - "integrity": "sha512-0ohAiJAx1DAUEcY9UopnfwCE9sSMDGnY/oXjWMax6g3RpzmTt2GMyMVAXcbn0mo8XAff0SbQJl2/SBU+hjSZ1A==", + "version": "17.0.11", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz", + "integrity": "sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q==", "dev": true, "requires": { "@types/react": "*" - }, - "dependencies": { - "@types/react": { - "version": "16.14.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.8.tgz", - "integrity": "sha512-QN0/Qhmx+l4moe7WJuTxNiTsjBwlBGHqKGvInSQCBdo7Qio0VtOqwsC0Wq7q3PbJlB0cR4Y4CVo1OOe6BOsOmA==", - "dev": true, - "requires": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "csstype": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", - "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==", - "dev": true - } } }, "@types/react-syntax-highlighter": { @@ -10115,9 +10278,9 @@ "dev": true }, "@types/scheduler": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz", - "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==", + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", "dev": true }, "@types/semver": { @@ -10177,6 +10340,25 @@ "integrity": "sha512-+mdBIb+pxJ9SLwtjc2DgolMm8U7CG6qBdCevkjSsFB7ehJ0EExFd2ltKQ6m9CoKitqXwe6Tx5h+fAcklGQD0Bw==", "dev": true }, + "@types/superagent": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.13.tgz", + "integrity": "sha512-YIGelp3ZyMiH0/A09PMAORO0EBGlF5xIKfDpK74wdYvWUs2o96b5CItJcWPdH409b7SAXIIG6p8NdU/4U2Maww==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "@types/supertest": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.11.tgz", + "integrity": "sha512-uci4Esokrw9qGb9bvhhSVEjd6rkny/dk5PK/Qz4yxKiyppEI+dOPlNrZBahE3i+PoKFYyDxChVXZ/ysS/nrm1Q==", + "dev": true, + "requires": { + "@types/superagent": "*" + } + }, "@types/tapable": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", @@ -10189,9 +10371,9 @@ "integrity": "sha512-7cTXwKP/HLOPVgjg+YhBdQ7bMiobGMuoBmrGmqwIWJv8elC6t1DfVc/mn4fD9UE1IjhwmhaQ5pGVXkmXbH0rhg==" }, "@types/toastr": { - "version": "2.1.38", - "resolved": "https://registry.npmjs.org/@types/toastr/-/toastr-2.1.38.tgz", - "integrity": "sha512-zKF+vbPVkkwBaMy0lm5NdI117mOoxWOQf2eXOuP/upQ5lHDSfNK/bVoo/x8/IN1hLzO81g+JvTpZQhqr0gKyYg==", + "version": "2.1.39", + "resolved": "https://registry.npmjs.org/@types/toastr/-/toastr-2.1.39.tgz", + "integrity": "sha512-jgbMLTjj7dsSY/EYYOoZXhK6OY/bR909Bn6YNnwL3Oq+f0AeFKbYa198XU/6bsmUqqCCWw5VvCi11FDN1/fetw==", "dev": true, "requires": { "@types/jquery": "*" @@ -10281,9 +10463,9 @@ } }, "@types/webpack-env": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.16.2.tgz", - "integrity": "sha512-vKx7WNQNZDyJveYcHAm9ZxhqSGLYwoyLhrHjLBOkw3a7cT76sTdjgtwyijhk1MaHyRIuSztcVwrUOO/NEu68Dw==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.16.3.tgz", + "integrity": "sha512-9gtOPPkfyNoEqCQgx4qJKkuNm/x0R2hKR7fdl7zvTJyHnIisuE/LfvXOsYWL0o3qq6uiBnKZNNNzi3l0y/X+xw==", "dev": true }, "@types/webpack-sources": { @@ -10538,6 +10720,40 @@ } } }, + "@typescript-eslint/scope-manager": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.3.1.tgz", + "integrity": "sha512-XksFVBgAq0Y9H40BDbuPOTUIp7dn4u8oOuhcgGq7EoDP50eqcafkMVGrypyVGvDYHzjhdUCUwuwVUK4JhkMAMg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.3.1", + "@typescript-eslint/visitor-keys": "5.3.1" + }, + "dependencies": { + "@typescript-eslint/types": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.3.1.tgz", + "integrity": "sha512-bG7HeBLolxKHtdHG54Uac750eXuQQPpdJfCYuw4ZI3bZ7+GgKClMWM8jExBtp7NSP4m8PmLRM8+lhzkYnSmSxQ==", + "dev": true + }, + "@typescript-eslint/visitor-keys": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.3.1.tgz", + "integrity": "sha512-3cHUzUuVTuNHx0Gjjt5pEHa87+lzyqOiHXy/Gz+SJOCW1mpw9xQHIIEwnKn+Thph1mgWyZ90nboOcSuZr/jTTQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.3.1", + "eslint-visitor-keys": "^3.0.0" + } + }, + "eslint-visitor-keys": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", + "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", + "dev": true + } + } + }, "@typescript-eslint/types": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", @@ -11107,14 +11323,21 @@ } }, "ajv": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", - "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.7.1.tgz", + "integrity": "sha512-gPpOObTO1QjbnN1sVMjJcp1TF9nggMfO4MBR5uQl6ZVTOaEPq5i4oq/6R9q2alMMPB3eg53wFv1RuJBLuxf3Hw==", "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", "uri-js": "^4.2.2" + }, + "dependencies": { + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } } }, "ajv-errors": { @@ -11241,6 +11464,316 @@ } } }, + "anti-trojan-source": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anti-trojan-source/-/anti-trojan-source-1.3.2.tgz", + "integrity": "sha512-7ZlSTSBW+AULLsSkBuLU1naoWcV7pdjfYH54XgzXf4vZaxCgLF+iCiy4EOrpDtIXUQ1rOBdY91O2fxY/m0rXbg==", + "requires": { + "globby": "^12.0.2", + "meow": "^10.1.1" + }, + "dependencies": { + "@types/minimist": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==" + }, + "array-union": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", + "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==" + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==" + }, + "camelcase-keys": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.1.tgz", + "integrity": "sha512-P331lEls98pW8JLyodNWfzuz91BEDVA4VpW2/SwXnyv2K495tq1N777xzDbFgnEigfA7UIY0xa6PwR/H9jijjA==", + "requires": { + "camelcase": "^6.2.0", + "map-obj": "^4.1.0", + "quick-lru": "^5.1.1", + "type-fest": "^1.2.1" + } + }, + "decamelize": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz", + "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==" + }, + "fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "globby": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-12.0.2.tgz", + "integrity": "sha512-lAsmb/5Lww4r7MM9nCCliDZVIKbZTavrsunAsHLr9oHthrZP1qi7/gAnHOsUs9bLvEt2vKVJhHmxuL7QbDuPdQ==", + "requires": { + "array-union": "^3.0.1", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.7", + "ignore": "^5.1.8", + "merge2": "^1.4.1", + "slash": "^4.0.0" + } + }, + "hosted-git-info": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", + "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==" + }, + "is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "requires": { + "has": "^1.0.3" + } + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "requires": { + "p-locate": "^5.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "meow": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-10.1.1.tgz", + "integrity": "sha512-uzOAEBTGujHAD6bVzIQQk5kDTgatxmpVmr1pj9QhwsHLEG2AiB+9F08/wmjrZIk4h5pWxERd7+jqGZywYx3ZFw==", + "requires": { + "@types/minimist": "^1.2.2", + "camelcase-keys": "^7.0.0", + "decamelize": "^5.0.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.2", + "read-pkg-up": "^8.0.0", + "redent": "^4.0.0", + "trim-newlines": "^4.0.2", + "type-fest": "^1.2.2", + "yargs-parser": "^20.2.9" + } + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "requires": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "requires": { + "p-limit": "^3.0.2" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" + }, + "read-pkg": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-6.0.0.tgz", + "integrity": "sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==", + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^3.0.2", + "parse-json": "^5.2.0", + "type-fest": "^1.0.1" + } + }, + "read-pkg-up": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-8.0.0.tgz", + "integrity": "sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==", + "requires": { + "find-up": "^5.0.0", + "read-pkg": "^6.0.0", + "type-fest": "^1.0.1" + } + }, + "redent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", + "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", + "requires": { + "indent-string": "^5.0.0", + "strip-indent": "^4.0.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==" + }, + "strip-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", + "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", + "requires": { + "min-indent": "^1.0.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "trim-newlines": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.0.2.tgz", + "integrity": "sha512-GJtWyq9InR/2HRiLZgpIKv+ufIKrVrvjQWEj7PxAXNc5dwbNJkqhAUoAGgzRmULAnoOM5EIpveYd3J2VeSAIew==" + }, + "type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + } + } + }, "any-observable": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", @@ -11495,6 +12028,12 @@ } } }, + "aria-query": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz", + "integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==", + "dev": true + }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -12555,201 +13094,6 @@ } } }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "dev": true, - "requires": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" - }, - "dependencies": { - "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", - "dev": true - } - } - }, - "babel-helper-bindify-decorators": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", - "integrity": "sha1-FMGeXxQte0fxmlJDHlKxzLxAozA=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-builder-binary-assignment-operator-visitor": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", - "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", - "dev": true, - "requires": { - "babel-helper-explode-assignable-expression": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-builder-react-jsx": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz", - "integrity": "sha1-Of+DE7dci2Xc7/HzHTg+D/KkCKA=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "esutils": "^2.0.2" - } - }, - "babel-helper-call-delegate": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", - "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", - "dev": true, - "requires": { - "babel-helper-hoist-variables": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-define-map": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", - "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", - "dev": true, - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" - } - }, - "babel-helper-explode-assignable-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", - "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-explode-class": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", - "integrity": "sha1-fcKjkQ3uAHBW4eMdZAztPVTqqes=", - "dev": true, - "requires": { - "babel-helper-bindify-decorators": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", - "dev": true, - "requires": { - "babel-helper-get-function-arity": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-get-function-arity": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", - "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-hoist-variables": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", - "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-optimise-call-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", - "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-helper-regex": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", - "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" - } - }, - "babel-helper-remap-async-to-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", - "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", - "dev": true, - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helper-replace-supers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", - "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", - "dev": true, - "requires": { - "babel-helper-optimise-call-expression": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-helpers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, "babel-loader": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz", @@ -12889,77 +13233,6 @@ "babel-runtime": "^6.22.0" } }, - "babel-mocha-es6-compiler": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/babel-mocha-es6-compiler/-/babel-mocha-es6-compiler-0.1.0.tgz", - "integrity": "sha1-QMnkBoCvRhWP7usntJQUtrgOxDg=", - "dev": true, - "requires": { - "babel-core": "~6.9.0", - "babel-plugin-add-module-exports": "~0.2.1", - "babel-preset-es2015": "~6.3.13", - "babel-preset-react": "~6.3.13", - "babel-preset-stage-0": "~6.3.13" - }, - "dependencies": { - "babel-core": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.9.1.tgz", - "integrity": "sha1-SNRx7r9N5GngqUL+RW3MlLGL6A0=", - "dev": true, - "requires": { - "babel-code-frame": "^6.8.0", - "babel-generator": "^6.9.0", - "babel-helpers": "^6.8.0", - "babel-messages": "^6.8.0", - "babel-register": "^6.9.0", - "babel-runtime": "^6.9.1", - "babel-template": "^6.9.0", - "babel-traverse": "^6.9.0", - "babel-types": "^6.9.1", - "babylon": "^6.7.0", - "convert-source-map": "^1.1.0", - "debug": "^2.1.1", - "json5": "^0.4.0", - "lodash": "^4.2.0", - "minimatch": "^2.0.3", - "path-exists": "^1.0.0", - "path-is-absolute": "^1.0.0", - "private": "^0.1.6", - "shebang-regex": "^1.0.0", - "slash": "^1.0.0", - "source-map": "^0.5.0" - } - }, - "json5": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.4.0.tgz", - "integrity": "sha1-BUNS5MTIDIbAkjh31EneF2pzLI0=", - "dev": true - }, - "minimatch": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", - "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", - "dev": true, - "requires": { - "brace-expansion": "^1.0.0" - } - }, - "path-exists": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-1.0.0.tgz", - "integrity": "sha1-1aiZjrce83p0w06w2eum6HjuoIE=", - "dev": true - } - } - }, - "babel-plugin-add-module-exports": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-0.2.1.tgz", - "integrity": "sha1-mumh9KjcZ/DN7E9K7aHkOl/2XiU=", - "dev": true - }, "babel-plugin-add-react-displayname": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/babel-plugin-add-react-displayname/-/babel-plugin-add-react-displayname-0.0.5.tgz", @@ -12990,15 +13263,6 @@ "integrity": "sha1-z1RS6Bx7gD+3lZ8QRayI4uwo/3Y=", "dev": true }, - "babel-plugin-check-es2015-constants": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", - "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, "babel-plugin-dynamic-import-node": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", @@ -13044,15 +13308,15 @@ } }, "babel-plugin-istanbul": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", - "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, @@ -13157,613 +13421,12 @@ } } }, - "babel-plugin-syntax-async-functions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", - "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", - "dev": true - }, - "babel-plugin-syntax-async-generators": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", - "integrity": "sha1-a8lj67FuzLrmuStZbrfzXDQqi5o=", - "dev": true - }, - "babel-plugin-syntax-class-constructor-call": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz", - "integrity": "sha1-nLnTn+Q8hgC+yBRkVt3L1OGnZBY=", - "dev": true - }, - "babel-plugin-syntax-class-properties": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", - "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", - "dev": true - }, - "babel-plugin-syntax-decorators": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", - "integrity": "sha1-MSVjtNvePMgGzuPkFszurd0RrAs=", - "dev": true - }, - "babel-plugin-syntax-do-expressions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz", - "integrity": "sha1-V0d1YTmqJtOQ0JQQsDdEugfkeW0=", - "dev": true - }, - "babel-plugin-syntax-dynamic-import": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", - "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=", - "dev": true - }, - "babel-plugin-syntax-exponentiation-operator": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", - "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", - "dev": true - }, - "babel-plugin-syntax-export-extensions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz", - "integrity": "sha1-cKFITw+QiaToStRLrDU8lbmxJyE=", - "dev": true - }, - "babel-plugin-syntax-flow": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", - "integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=", - "dev": true - }, - "babel-plugin-syntax-function-bind": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz", - "integrity": "sha1-SMSV8Xe98xqYHnMvVa3AvdJgH0Y=", - "dev": true - }, "babel-plugin-syntax-jsx": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=", "dev": true }, - "babel-plugin-syntax-object-rest-spread": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", - "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", - "dev": true - }, - "babel-plugin-syntax-trailing-function-commas": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", - "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", - "dev": true - }, - "babel-plugin-transform-async-generator-functions": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", - "integrity": "sha1-8FiQAUX9PpkHpt3yjaWfIVJYpds=", - "dev": true, - "requires": { - "babel-helper-remap-async-to-generator": "^6.24.1", - "babel-plugin-syntax-async-generators": "^6.5.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-async-to-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", - "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", - "dev": true, - "requires": { - "babel-helper-remap-async-to-generator": "^6.24.1", - "babel-plugin-syntax-async-functions": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-class-constructor-call": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz", - "integrity": "sha1-gNwoVQWsBn3LjWxl4vbxGrd2Xvk=", - "dev": true, - "requires": { - "babel-plugin-syntax-class-constructor-call": "^6.18.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-class-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", - "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", - "dev": true, - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-plugin-syntax-class-properties": "^6.8.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-decorators": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", - "integrity": "sha1-eIAT2PjGtSIr33s0Q5Df13Vp4k0=", - "dev": true, - "requires": { - "babel-helper-explode-class": "^6.24.1", - "babel-plugin-syntax-decorators": "^6.13.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-do-expressions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz", - "integrity": "sha1-KMyvkoEtlJws0SgfaQyP3EaK6bs=", - "dev": true, - "requires": { - "babel-plugin-syntax-do-expressions": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-arrow-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", - "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-block-scoped-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", - "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-block-scoping": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", - "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "lodash": "^4.17.4" - } - }, - "babel-plugin-transform-es2015-classes": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", - "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", - "dev": true, - "requires": { - "babel-helper-define-map": "^6.24.1", - "babel-helper-function-name": "^6.24.1", - "babel-helper-optimise-call-expression": "^6.24.1", - "babel-helper-replace-supers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-computed-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", - "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-destructuring": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", - "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-for-of": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", - "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", - "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", - "dev": true, - "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", - "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-modules-commonjs": { - "version": "6.26.2", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", - "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", - "dev": true, - "requires": { - "babel-plugin-transform-strict-mode": "^6.24.1", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-types": "^6.26.0" - } - }, - "babel-plugin-transform-es2015-object-super": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", - "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", - "dev": true, - "requires": { - "babel-helper-replace-supers": "^6.24.1", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-parameters": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", - "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", - "dev": true, - "requires": { - "babel-helper-call-delegate": "^6.24.1", - "babel-helper-get-function-arity": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-shorthand-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", - "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-spread": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", - "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-sticky-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", - "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", - "dev": true, - "requires": { - "babel-helper-regex": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-plugin-transform-es2015-template-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", - "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-typeof-symbol": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", - "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-es2015-unicode-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", - "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", - "dev": true, - "requires": { - "babel-helper-regex": "^6.24.1", - "babel-runtime": "^6.22.0", - "regexpu-core": "^2.0.0" - } - }, - "babel-plugin-transform-exponentiation-operator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", - "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", - "dev": true, - "requires": { - "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", - "babel-plugin-syntax-exponentiation-operator": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-export-extensions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz", - "integrity": "sha1-U3OLR+deghhYnuqUbLvTkQm75lM=", - "dev": true, - "requires": { - "babel-plugin-syntax-export-extensions": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-flow-strip-types": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", - "integrity": "sha1-hMtnKTXUNxT9wyvOhFaNh0Qc988=", - "dev": true, - "requires": { - "babel-plugin-syntax-flow": "^6.18.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-function-bind": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz", - "integrity": "sha1-xvuOlqwpajELjPjqQBRiQH3fapc=", - "dev": true, - "requires": { - "babel-plugin-syntax-function-bind": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-object-rest-spread": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", - "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", - "dev": true, - "requires": { - "babel-plugin-syntax-object-rest-spread": "^6.8.0", - "babel-runtime": "^6.26.0" - } - }, - "babel-plugin-transform-react-display-name": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz", - "integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-react-jsx": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz", - "integrity": "sha1-hAoCjn30YN/DotKfDA2R9jduZqM=", - "dev": true, - "requires": { - "babel-helper-builder-react-jsx": "^6.24.1", - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-react-jsx-source": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz", - "integrity": "sha1-ZqwSFT9c0tF7PBkmj0vwGX9E7NY=", - "dev": true, - "requires": { - "babel-plugin-syntax-jsx": "^6.8.0", - "babel-runtime": "^6.22.0" - } - }, - "babel-plugin-transform-regenerator": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", - "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", - "dev": true, - "requires": { - "regenerator-transform": "^0.10.0" - } - }, - "babel-plugin-transform-strict-mode": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", - "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } - }, - "babel-polyfill": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", - "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "regenerator-runtime": "^0.10.5" - }, - "dependencies": { - "core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", - "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", - "dev": true - } - } - }, - "babel-preset-es2015": { - "version": "6.3.13", - "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.3.13.tgz", - "integrity": "sha1-l9zn7ykuGMubK3VF2AxZPCjZUX8=", - "dev": true, - "requires": { - "babel-plugin-check-es2015-constants": "^6.3.13", - "babel-plugin-transform-es2015-arrow-functions": "^6.3.13", - "babel-plugin-transform-es2015-block-scoped-functions": "^6.3.13", - "babel-plugin-transform-es2015-block-scoping": "^6.3.13", - "babel-plugin-transform-es2015-classes": "^6.3.13", - "babel-plugin-transform-es2015-computed-properties": "^6.3.13", - "babel-plugin-transform-es2015-destructuring": "^6.3.13", - "babel-plugin-transform-es2015-for-of": "^6.3.13", - "babel-plugin-transform-es2015-function-name": "^6.3.13", - "babel-plugin-transform-es2015-literals": "^6.3.13", - "babel-plugin-transform-es2015-modules-commonjs": "^6.3.13", - "babel-plugin-transform-es2015-object-super": "^6.3.13", - "babel-plugin-transform-es2015-parameters": "^6.3.13", - "babel-plugin-transform-es2015-shorthand-properties": "^6.3.13", - "babel-plugin-transform-es2015-spread": "^6.3.13", - "babel-plugin-transform-es2015-sticky-regex": "^6.3.13", - "babel-plugin-transform-es2015-template-literals": "^6.3.13", - "babel-plugin-transform-es2015-typeof-symbol": "^6.3.13", - "babel-plugin-transform-es2015-unicode-regex": "^6.3.13", - "babel-plugin-transform-regenerator": "^6.3.13" - } - }, - "babel-preset-react": { - "version": "6.3.13", - "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.3.13.tgz", - "integrity": "sha1-E9VeBqZfqqoHw5v2Op2DbgMhFvo=", - "dev": true, - "requires": { - "babel-plugin-syntax-flow": "^6.3.13", - "babel-plugin-syntax-jsx": "^6.3.13", - "babel-plugin-transform-flow-strip-types": "^6.3.13", - "babel-plugin-transform-react-display-name": "^6.3.13", - "babel-plugin-transform-react-jsx": "^6.3.13", - "babel-plugin-transform-react-jsx-source": "^6.3.13" - } - }, - "babel-preset-stage-0": { - "version": "6.3.13", - "resolved": "https://registry.npmjs.org/babel-preset-stage-0/-/babel-preset-stage-0-6.3.13.tgz", - "integrity": "sha1-eKN8VvCzmI8qeZMtywzrj/N3sNE=", - "dev": true, - "requires": { - "babel-plugin-transform-do-expressions": "^6.3.13", - "babel-plugin-transform-function-bind": "^6.3.13", - "babel-preset-stage-1": "^6.3.13" - } - }, - "babel-preset-stage-1": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz", - "integrity": "sha1-dpLNfc1oSZB+auSgqFWJz7niv7A=", - "dev": true, - "requires": { - "babel-plugin-transform-class-constructor-call": "^6.24.1", - "babel-plugin-transform-export-extensions": "^6.22.0", - "babel-preset-stage-2": "^6.24.1" - } - }, - "babel-preset-stage-2": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", - "integrity": "sha1-2eKWD7PXEYfw5k7sYrwHdnIZvcE=", - "dev": true, - "requires": { - "babel-plugin-syntax-dynamic-import": "^6.18.0", - "babel-plugin-transform-class-properties": "^6.24.1", - "babel-plugin-transform-decorators": "^6.24.1", - "babel-preset-stage-3": "^6.24.1" - } - }, - "babel-preset-stage-3": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", - "integrity": "sha1-g2raCp56f6N8sTj7kyb4eTSkg5U=", - "dev": true, - "requires": { - "babel-plugin-syntax-trailing-function-commas": "^6.22.0", - "babel-plugin-transform-async-generator-functions": "^6.24.1", - "babel-plugin-transform-async-to-generator": "^6.24.1", - "babel-plugin-transform-exponentiation-operator": "^6.24.1", - "babel-plugin-transform-object-rest-spread": "^6.22.0" - } - }, - "babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", - "dev": true, - "requires": { - "babel-core": "^6.26.0", - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "home-or-tmp": "^2.0.0", - "lodash": "^4.17.4", - "mkdirp": "^0.5.1", - "source-map-support": "^0.4.15" - }, - "dependencies": { - "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-generator": "^6.26.0", - "babel-helpers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-register": "^6.26.0", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "convert-source-map": "^1.5.1", - "debug": "^2.6.9", - "json5": "^0.5.1", - "lodash": "^4.17.4", - "minimatch": "^3.0.4", - "path-is-absolute": "^1.0.1", - "private": "^0.1.8", - "slash": "^1.0.0", - "source-map": "^0.5.7" - } - }, - "core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "dev": true - } - } - }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -13788,19 +13451,6 @@ } } }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" - } - }, "babel-traverse": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", @@ -14351,6 +14001,21 @@ } } }, + "broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "requires": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", @@ -14644,16 +14309,16 @@ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, "c8": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-7.9.0.tgz", - "integrity": "sha512-aQ7dC8gASnKdBwHUuYuzsdKCEDrKnWr7ZuZUnf4CNAL81oyKloKrs7H7zYvcrmCtIrMToudBSUhq2q+LLBMvgg==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.10.0.tgz", + "integrity": "sha512-OAwfC5+emvA6R7pkYFVBTOtI5ruf9DahffGmIqUc9l6wEh0h7iAFP6dt/V9Ioqlr2zW5avX9U9/w1I4alTRHkA==", "dev": true, "requires": { "@bcoe/v8-coverage": "^0.2.3", "@istanbuljs/schema": "^0.1.2", "find-up": "^5.0.0", "foreground-child": "^2.0.0", - "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-coverage": "^3.0.1", "istanbul-lib-report": "^3.0.0", "istanbul-reports": "^3.0.2", "rimraf": "^3.0.0", @@ -14855,9 +14520,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001257", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz", - "integrity": "sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA==" + "version": "1.0.30001271", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz", + "integrity": "sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA==" }, "capital-case": { "version": "1.0.4", @@ -15032,6 +14697,12 @@ "chai": ">1.9.0" } }, + "chai-dom": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/chai-dom/-/chai-dom-1.10.0.tgz", + "integrity": "sha512-/FE0NvEGMXx1x1YQlc8ihLrEhH8JawflchuGe6ypIAX/4Zwmkr4cC3mfR9pDytbxsE/2LSm719TeU7VF/TCmtg==", + "dev": true + }, "chai-spies": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/chai-spies/-/chai-spies-1.0.0.tgz", @@ -15404,9 +15075,9 @@ } }, "clean-css": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", - "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", "dev": true, "requires": { "source-map": "~0.6.0" @@ -15911,9 +15582,9 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", + "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==", "dev": true }, "copy-concurrently": { @@ -15981,9 +15652,9 @@ } }, "core-js-pure": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.18.1.tgz", - "integrity": "sha512-kmW/k8MaSuqpvA1xm2l3TVlBuvW+XBkcaOroFUpO3D4lsTGQWBTb/tBDCf/PNkkPLrwgrkQRIYNPB0CeqGJWGQ==", + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.18.3.tgz", + "integrity": "sha512-qfskyO/KjtbYn09bn1IPkuhHl5PlJ6IzJ9s9sraJ1EqcuGyLGKzhSM1cY0zgyL9hx42eulQLZ6WaeK5ycJCkqw==", "dev": true }, "core-util-is": { @@ -16970,7 +16641,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", - "dev": true, "requires": { "decamelize": "^1.1.0", "map-obj": "^1.0.0" @@ -16979,14 +16649,12 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" } } }, @@ -17153,20 +16821,16 @@ "repeat-string": "^1.5.4" } }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, + "detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + }, "detect-port-alt": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", @@ -17206,7 +16870,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, "requires": { "path-type": "^4.0.0" } @@ -17220,6 +16883,12 @@ "esutils": "^2.0.2" } }, + "dom-accessibility-api": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.10.tgz", + "integrity": "sha512-Xu9mD0UjrJisTmv7lmVSDMagQcU9R5hwAbxsaAE/35XPnPLJobbuREfV/rraiSaEj/UOvgrzQs66zyTWTlyd+g==", + "dev": true + }, "dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -17648,7 +17317,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "requires": { "is-arrayish": "^0.2.1" }, @@ -17656,8 +17324,7 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" } } }, @@ -18208,6 +17875,14 @@ } } }, + "eslint-plugin-anti-trojan-source": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-anti-trojan-source/-/eslint-plugin-anti-trojan-source-1.0.6.tgz", + "integrity": "sha512-UrX0RNLMvRaT0TJA+Hy7cEEJOvRHUOQs0umrPKqb54aylHwHAMy5Ms+nhgABTuDhGcbJgjAmTDX6cxxEFmx4Jg==", + "requires": { + "anti-trojan-source": "^1.3.1" + } + }, "eslint-plugin-import": { "version": "2.24.2", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.24.2.tgz", @@ -18427,176 +18102,365 @@ } } }, - "eslint-plugin-prettier": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz", - "integrity": "sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } + "eslint-plugin-prettier": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz", + "integrity": "sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-plugin-react": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.24.0.tgz", + "integrity": "sha512-KJJIx2SYx7PBx3ONe/mEeMz4YE0Lcr7feJTCMyyKb/341NcjuAgim3Acgan89GfPv7nxXK2+0slu0CWXYM4x+Q==", + "dev": true, + "requires": { + "array-includes": "^3.1.3", + "array.prototype.flatmap": "^1.2.4", + "doctrine": "^2.1.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.0.4", + "object.entries": "^1.1.4", + "object.fromentries": "^2.0.4", + "object.values": "^1.1.4", + "prop-types": "^15.7.2", + "resolve": "^2.0.0-next.3", + "string.prototype.matchall": "^4.0.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", + "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.3", + "is-string": "^1.0.6", + "object-inspect": "^1.10.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "dev": true + }, + "is-regex": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", + "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.2" + } + }, + "object-inspect": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", + "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.4.tgz", + "integrity": "sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.2" + } + }, + "object.values": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.4.tgz", + "integrity": "sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.2" + } + }, + "resolve": { + "version": "2.0.0-next.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", + "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + } + } + }, + "eslint-plugin-react-hooks": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz", + "integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==", + "dev": true }, - "eslint-plugin-react": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.24.0.tgz", - "integrity": "sha512-KJJIx2SYx7PBx3ONe/mEeMz4YE0Lcr7feJTCMyyKb/341NcjuAgim3Acgan89GfPv7nxXK2+0slu0CWXYM4x+Q==", + "eslint-plugin-testing-library": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.0.0.tgz", + "integrity": "sha512-lojlPN8nsb7JTFYhJuLNwwI8kALRC0TBz5JRO1lvV7Ifzqu7IoddjDFRCxeM+0d2/zuEO7Sb5oc7ErDqhd4MBw==", "dev": true, "requires": { - "array-includes": "^3.1.3", - "array.prototype.flatmap": "^1.2.4", - "doctrine": "^2.1.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.0.4", - "object.entries": "^1.1.4", - "object.fromentries": "^2.0.4", - "object.values": "^1.1.4", - "prop-types": "^15.7.2", - "resolve": "^2.0.0-next.3", - "string.prototype.matchall": "^4.0.5" + "@typescript-eslint/experimental-utils": "^5.0.0" }, "dependencies": { - "es-abstract": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", - "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", + "@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, + "@typescript-eslint/experimental-utils": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.3.1.tgz", + "integrity": "sha512-RgFn5asjZ5daUhbK5Sp0peq0SSMytqcrkNfU4pnDma2D8P3ElZ6JbYjY8IMSFfZAJ0f3x3tnO3vXHweYg0g59w==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.10.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.3.1", + "@typescript-eslint/types": "5.3.1", + "@typescript-eslint/typescript-estree": "5.3.1", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" } }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "@typescript-eslint/types": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.3.1.tgz", + "integrity": "sha512-bG7HeBLolxKHtdHG54Uac750eXuQQPpdJfCYuw4ZI3bZ7+GgKClMWM8jExBtp7NSP4m8PmLRM8+lhzkYnSmSxQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.3.1.tgz", + "integrity": "sha512-PwFbh/PKDVo/Wct6N3w+E4rLZxUDgsoII/GrWM2A62ETOzJd4M6s0Mu7w4CWsZraTbaC5UQI+dLeyOIFF1PquQ==", "dev": true, "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "@typescript-eslint/types": "5.3.1", + "@typescript-eslint/visitor-keys": "5.3.1", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", + "semver": "^7.3.5", + "tsutils": "^3.21.0" } }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true + "@typescript-eslint/visitor-keys": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.3.1.tgz", + "integrity": "sha512-3cHUzUuVTuNHx0Gjjt5pEHa87+lzyqOiHXy/Gz+SJOCW1mpw9xQHIIEwnKn+Thph1mgWyZ90nboOcSuZr/jTTQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.3.1", + "eslint-visitor-keys": "^3.0.0" + } }, - "is-callable": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", - "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "is-regex": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", - "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "has-symbols": "^1.0.2" + "ms": "2.1.2" } }, - "object-inspect": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", - "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", - "dev": true + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", + "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", "dev": true }, - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } } }, - "object.entries": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.4.tgz", - "integrity": "sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA==", + "globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.2" + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" } }, - "object.values": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.4.tgz", - "integrity": "sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==", + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.2" + "is-extglob": "^2.1.1" } }, - "resolve": { - "version": "2.0.0-next.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", - "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" + "yallist": "^4.0.0" } }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "lru-cache": "^6.0.0" } }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "tslib": "^1.8.1" } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, - "eslint-plugin-react-hooks": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz", - "integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==", - "dev": true - }, "eslint-scope": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", @@ -19129,9 +18993,9 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-diff": { "version": "1.2.0", @@ -19281,7 +19145,6 @@ "version": "1.11.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", - "dev": true, "requires": { "reusify": "^1.0.4" } @@ -19748,9 +19611,9 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", "dev": true, "requires": { "@babel/highlight": "^7.14.5" @@ -19815,9 +19678,9 @@ "dev": true }, "formidable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", - "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", + "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", "dev": true }, "forwarded": { @@ -21781,13 +21644,25 @@ "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + } } }, "hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==" }, "has": { "version": "1.0.3", @@ -22080,9 +21955,9 @@ "integrity": "sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==" }, "history": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/history/-/history-5.0.1.tgz", - "integrity": "sha512-5qC/tFUKfVci5kzgRxZxN5Mf1CV8NmJx9ByaPX0YTLx5Vz3Svh7NYp6eA4CpDq4iA9D0C1t8BNIfvQIrUI3mVw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.1.0.tgz", + "integrity": "sha512-zPuQgPacm2vH2xdORvGGz1wQMuHSIB56yNAy5FnLuwOwgSYyPKptJtcMm6Ev+hRGeS+GzhbmRacHzvlESbFwDg==", "dev": true, "requires": { "@babel/runtime": "^7.7.6" @@ -22112,16 +21987,6 @@ "react-is": "^16.7.0" } }, - "home-or-tmp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.1" - } - }, "hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -22615,8 +22480,7 @@ "ignore": { "version": "5.1.8", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", - "dev": true + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" }, "image-size": { "version": "1.0.0", @@ -23128,17 +22992,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -23437,18 +23291,19 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "istanbul-lib-coverage": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.1.tgz", - "integrity": "sha512-GvCYYTxaCPqwMjobtVcVKvSHtAGe48MNhGjpK8LtVF8K0ISX7hCKl85LgtuaSneWVyQmaGcW3iXVV3GaZSLpmQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", "dev": true }, "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.0.4.tgz", + "integrity": "sha512-W6jJF9rLGEISGoCyXRqa/JCGQGmmxPO10TMu7izaUTynxvBvTjqzAIIGCK9USBmIbQAaSWD6XJPrM9Pv5INknw==", "dev": true, "requires": { - "@babel/core": "^7.7.5", + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.0.0", "semver": "^6.3.0" @@ -23506,9 +23361,9 @@ } }, "istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.5.tgz", + "integrity": "sha512-5+19PlhnGabNWB7kOFnuxT8H3T/iIyQzIbQMxXsURmmvKg86P2sbkrGOT77VnHw0Qr0gc2XzRaRfMZYYbSQCJQ==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -23805,6 +23660,11 @@ "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==", "dev": true }, + "js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -23974,8 +23834,7 @@ "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "json-schema": { "version": "0.2.3", @@ -24660,8 +24519,7 @@ "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", - "dev": true + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" }, "linkify-it": { "version": "3.0.2", @@ -25219,6 +25077,12 @@ } } }, + "lz-string": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", + "dev": true + }, "mailparser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.2.0.tgz", @@ -25275,12 +25139,12 @@ "dev": true }, "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "dev": true, "requires": { - "tmpl": "1.0.x" + "tmpl": "1.0.5" } }, "map-age-cleaner": { @@ -25300,8 +25164,7 @@ "map-obj": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz", - "integrity": "sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==", - "dev": true + "integrity": "sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==" }, "map-or-similar": { "version": "1.5.0", @@ -25356,6 +25219,15 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==" }, + "match-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz", + "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==", + "requires": { + "@babel/runtime": "^7.12.5", + "remove-accents": "0.4.2" + } + }, "mathml-tag-names": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", @@ -25747,8 +25619,7 @@ "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, "merkle-lib": { "version": "2.0.10", @@ -26488,6 +26359,11 @@ "to-regex": "^3.0.2" } }, + "microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" + }, "miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", @@ -26556,8 +26432,7 @@ "min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" }, "minimalistic-assert": { "version": "1.0.1", @@ -26586,7 +26461,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dev": true, "requires": { "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", @@ -26596,14 +26470,12 @@ "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" } } }, @@ -26787,16 +26659,16 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, "mocha": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.0.0.tgz", - "integrity": "sha512-GRGG/q9bIaUkHJB9NL+KZNjDhMBHB30zW3bZW9qOiYr+QChyLjPzswaxFWkI1q6lGlSL28EQYzAi2vKWNkPx+g==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz", + "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==", "dev": true, "requires": { "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", - "chokidar": "3.5.1", - "debug": "4.3.1", + "chokidar": "3.5.2", + "debug": "4.3.2", "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", @@ -26807,13 +26679,12 @@ "log-symbols": "4.1.0", "minimatch": "3.0.4", "ms": "2.1.3", - "nanoid": "3.1.23", - "serialize-javascript": "5.0.1", + "nanoid": "3.1.25", + "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.1.4", + "workerpool": "6.1.5", "yargs": "16.2.0", "yargs-parser": "20.2.4", "yargs-unparser": "2.0.0" @@ -26840,25 +26711,10 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, "chalk": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", - "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -26876,22 +26732,6 @@ } } }, - "chokidar": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", - "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", - "dev": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.3.1", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" - } - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -26902,9 +26742,9 @@ } }, "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { "ms": "2.1.2" @@ -26924,15 +26764,6 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -26943,13 +26774,6 @@ "path-exists": "^4.0.0" } }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, "glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -26964,45 +26788,12 @@ "path-is-absolute": "^1.0.0" } }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -27037,6 +26828,12 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "nanoid": { + "version": "3.1.25", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", + "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", + "dev": true + }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -27070,19 +26867,10 @@ "safe-buffer": "^5.1.0" } }, - "readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, "serialize-javascript": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", - "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -27103,15 +26891,6 @@ "has-flag": "^4.0.0" } }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -27528,6 +27307,14 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=", + "requires": { + "big-integer": "^1.6.16" + } + }, "nanoid": { "version": "3.1.23", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", @@ -28350,6 +28137,11 @@ "integrity": "sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg==", "dev": true }, + "oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==" + }, "on-exit-leak-free": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", @@ -28451,12 +28243,6 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -28768,8 +28554,7 @@ "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, "path.js": { "version": "1.0.7", @@ -30438,6 +30223,105 @@ "renderkid": "^2.0.4" } }, + "pretty-format": { + "version": "27.3.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.3.1.tgz", + "integrity": "sha512-DR/c+pvFc52nLimLROYjnXPtolawm+uWDxr4FjuLDLUn+ktWnSN851KoHwHzzqq6rfCOjkzN8FLgDrSub6UDuA==", + "dev": true, + "requires": { + "@jest/types": "^27.2.5", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "@jest/types": { + "version": "27.2.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.2.5.tgz", + "integrity": "sha512-nmuM4VuDtCZcY+eTpw+0nvstwReMsjPoj7ZR80/BbixulhLaiX+fbv8oeLW8WZlJMcsGQsTmMKT/iTZu1Uy/lQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", @@ -30963,8 +30847,7 @@ "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" }, "quick-format-unescaped": { "version": "4.0.4", @@ -31453,9 +31336,9 @@ } }, "react-docgen-typescript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.1.0.tgz", - "integrity": "sha512-7kpzLsYzVxff//HUVz1sPWLCdoSNvHD3M8b/iQLdF8fgf7zp26eVysRrAUSxiAT4yQv2zl09zHjJEYSYNxQ8Jw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.1.1.tgz", + "integrity": "sha512-XWe8bsYqVjxciKdpNoufaHiB7FgUHIOnVQgxUolRL3Zlof2zkdTzuQH6SU2n3Ek9kfy3O1c63ojMtNfpiuNeZQ==", "dev": true }, "react-dom": { @@ -31479,19 +31362,26 @@ } }, "react-element-to-jsx-string": { - "version": "14.3.2", - "resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.2.tgz", - "integrity": "sha512-WZbvG72cjLXAxV7VOuSzuHEaI3RHj10DZu8EcKQpkKcAj7+qAkG5XUeSdX5FXrA0vPrlx0QsnAzZEBJwzV0e+w==", + "version": "14.3.4", + "resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.4.tgz", + "integrity": "sha512-t4ZwvV6vwNxzujDQ+37bspnLwA4JlgUPWhLjBJWsNIDceAf6ZKUTCjdm08cN6WeZ5pTMKiCJkmAYnpmR4Bm+dg==", "dev": true, "requires": { - "@base2/pretty-print-object": "1.0.0", - "is-plain-object": "3.0.1" + "@base2/pretty-print-object": "1.0.1", + "is-plain-object": "5.0.0", + "react-is": "17.0.2" }, "dependencies": { "is-plain-object": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz", - "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true } } @@ -31599,6 +31489,16 @@ "react-popper": "^2.2.4" } }, + "react-query": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.33.1.tgz", + "integrity": "sha512-RtvQKhD4sJkoAGbyFpLmftGezz1KL19SeGdmjIOLUulyPTZuhcn3gemee96yjEiBk/9LFB5CuSiqywZY20Qj5Q==", + "requires": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + } + }, "react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", @@ -31856,22 +31756,14 @@ } }, "refractor": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.4.0.tgz", - "integrity": "sha512-dBeD02lC5eytm9Gld2Mx0cMcnR+zhSnsTfPpWqFaMgUMJfC9A6bcN3Br/NaXrnBJcuxnLFR90k1jrkaSyV8umg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.5.0.tgz", + "integrity": "sha512-QwPJd3ferTZ4cSPPjdP5bsYHMytwWYnAN5EEnLtGvkqp/FCCnGsBgxrm9EuIDnjUC3Uc/kETtvVi7fSIVC74Dg==", "dev": true, "requires": { "hastscript": "^6.0.0", "parse-entities": "^2.0.0", - "prismjs": "~1.24.0" - }, - "dependencies": { - "prismjs": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.24.1.tgz", - "integrity": "sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow==", - "dev": true - } + "prismjs": "~1.25.0" } }, "regenerate": { @@ -31894,17 +31786,6 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" }, - "regenerator-transform": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", - "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", - "dev": true, - "requires": { - "babel-runtime": "^6.18.0", - "babel-types": "^6.19.0", - "private": "^0.1.6" - } - }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -31931,40 +31812,6 @@ "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", "dev": true }, - "regexpu-core": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", - "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", - "dev": true, - "requires": { - "regenerate": "^1.2.1", - "regjsgen": "^0.2.0", - "regjsparser": "^0.1.4" - } - }, - "regjsgen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", - "dev": true - }, - "regjsparser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - } - } - }, "relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -32029,9 +31876,9 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", "dev": true, "requires": { "@babel/highlight": "^7.14.5" @@ -32210,6 +32057,11 @@ "mdast-util-to-markdown": "^0.6.0" } }, + "remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=" + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -32243,9 +32095,9 @@ } }, "css-what": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz", - "integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", + "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==", "dev": true }, "dom-serializer": { @@ -32326,15 +32178,6 @@ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", "dev": true }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "^1.0.0" - } - }, "request": { "version": "2.88.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", @@ -32405,8 +32248,7 @@ "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" }, "require_optional": { "version": "1.0.1", @@ -32522,8 +32364,7 @@ "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" }, "rewire": { "version": "5.0.0", @@ -32572,7 +32413,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "requires": { "queue-microtask": "^1.2.2" } @@ -33067,9 +32907,9 @@ "dev": true }, "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, "slice-ansi": { @@ -33286,15 +33126,6 @@ "urix": "^0.1.0" } }, - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", - "dev": true, - "requires": { - "source-map": "^0.5.6" - } - }, "source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", @@ -33320,7 +33151,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -33329,14 +33159,12 @@ "spdx-exceptions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" }, "spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, "requires": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -33345,8 +33173,7 @@ "spdx-license-ids": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz", - "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==", - "dev": true + "integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==" }, "speakeasy": { "version": "2.0.0", @@ -34861,9 +34688,9 @@ } }, "mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true }, "ms": { @@ -35400,6 +35227,20 @@ "ajv": "^6.1.0", "ajv-errors": "^1.0.0", "ajv-keywords": "^3.1.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + } } }, "source-map": { @@ -35693,12 +35534,6 @@ "escape-string-regexp": "^1.0.2" } }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, "trim-trailing-lines": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz", @@ -36601,6 +36436,15 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" }, + "unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "requires": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -36959,7 +36803,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, "requires": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -37060,12 +36903,12 @@ "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==" }, "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", "dev": true, "requires": { - "makeerror": "1.0.x" + "makeerror": "1.0.12" } }, "warning": { @@ -37666,9 +37509,9 @@ } }, "workerpool": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.4.tgz", - "integrity": "sha512-jGWPzsUqzkow8HoAvqaPWTUPCrlPJaJ5tY8Iz7n1uCz3tTp6s3CDG0FF1NsX42WNlkRSW6Mr+CDZGnNoSsKa7g==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", + "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", "dev": true }, "wrap-ansi": { @@ -37964,8 +37807,7 @@ "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" }, "zip-stream": { "version": "2.1.3", diff --git a/package.json b/package.json index ac32ba44327e..deb4eb3ee8ad 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "Rocket.Chat", "description": "The Ultimate Open Source WebChat Platform", - "version": "4.1.2", + "version": "4.2.0", "author": { "name": "Rocket.Chat", "url": "https://rocket.chat/" @@ -28,8 +28,10 @@ "coverage": "nyc -r html mocha --config ./.mocharc.js", "testci": "node .scripts/start.js", "testui": "cypress run --project tests", - "testapi": "mocha --config ./mocha_end_to_end.opts.js", + "testapi": "mocha --config ./.mocharc.api.js", "testunit": "mocha --config ./.mocharc.js", + "testunit-client": "mocha --config ./.mocharc.client.js", + "testunit-definition": "mocha --config ./.mocharc.definition.js", "testunit-watch": "mocha --watch --config ./.mocharc.js", "test": "npm run testapi && npm run testui", "translation-diff": "node .scripts/translationDiff.js", @@ -39,7 +41,8 @@ "set-version": "node .scripts/set-version.js", "release": "meteor npm run set-version --silent", "storybook": "cross-env NODE_OPTIONS=--max-old-space-size=8192 start-storybook -p 6006", - "build-storybook": "cross-env NODE_OPTIONS=--max-old-space-size=8192 build-storybook" + "storybook-ee": "cross-env NODE_OPTIONS=--max-old-space-size=8192 EE=true start-storybook -p 6006", + "build-storybook": "cross-env NODE_OPTIONS=--max-old-space-size=8192 EE=true build-storybook " }, "license": "MIT", "repository": { @@ -58,26 +61,31 @@ "@babel/preset-react": "^7.14.5", "@babel/register": "^7.14.5", "@rocket.chat/eslint-config": "^0.4.0", - "@rocket.chat/livechat": "^1.9.6", + "@rocket.chat/livechat": "^1.10.0", "@settlin/spacebars-loader": "^1.0.9", - "@storybook/addon-essentials": "^6.3.9", + "@storybook/addon-essentials": "^6.3.12", "@storybook/addon-postcss": "^2.0.0", - "@storybook/addons": "^6.3.9", - "@storybook/react": "^6.3.9", + "@storybook/addons": "^6.3.12", + "@storybook/react": "^6.3.12", + "@testing-library/react": "^12.1.2", + "@testing-library/user-event": "^13.5.0", "@types/adm-zip": "^0.4.34", "@types/agenda": "^2.0.9", "@types/bad-words": "^3.0.1", "@types/bcrypt": "^5.0.0", "@types/body-parser": "^1.19.0", - "@types/chai": "^4.2.19", + "@types/chai": "^4.2.22", + "@types/chai-datetime": "0.0.37", + "@types/chai-dom": "0.0.11", "@types/chai-spies": "^1.0.3", "@types/clipboard": "^2.0.1", "@types/dompurify": "^2.2.2", - "@types/ejson": "^2.1.2", + "@types/ejson": "^2.1.3", "@types/express": "^4.17.12", "@types/fibers": "^3.1.0", "@types/imap": "^0.8.35", "@types/jsdom": "^16.2.12", + "@types/jsdom-global": "^3.0.2", "@types/ldapjs": "^2.2.1", "@types/less": "^3.0.2", "@types/lodash.get": "^4.4.6", @@ -85,21 +93,24 @@ "@types/marked": "^1.2.2", "@types/meteor": "1.4.74", "@types/mkdirp": "^1.0.1", - "@types/mocha": "^8.2.2", + "@types/mocha": "^8.2.3", "@types/mock-require": "^2.0.0", "@types/moment-timezone": "^0.5.30", "@types/mongodb": "^3.6.19", "@types/node": "^12.20.10", + "@types/node-rsa": "^1.1.1", "@types/nodemailer": "^6.4.2", "@types/parseurl": "^1.3.1", + "@types/photoswipe": "^4.1.2", "@types/psl": "^1.1.0", - "@types/react": "^17.0.11", - "@types/react-dom": "^17.0.8", + "@types/react": "^17.0.35", + "@types/react-dom": "^17.0.11", "@types/rewire": "^2.5.28", "@types/semver": "^7.3.6", "@types/sharp": "^0.28.3", "@types/string-strip-html": "^5.0.0", - "@types/toastr": "^2.1.38", + "@types/supertest": "^2.0.11", + "@types/toastr": "^2.1.39", "@types/underscore.string": "0.0.38", "@types/use-subscription": "^1.0.0", "@types/uuid": "^8.3.1", @@ -110,11 +121,10 @@ "autoprefixer": "^9.8.6", "babel-eslint": "^10.1.0", "babel-loader": "^8.2.2", - "babel-mocha-es6-compiler": "^0.1.0", "babel-plugin-array-includes": "^2.0.3", - "babel-polyfill": "^6.26.0", "chai": "^4.3.4", "chai-datetime": "^1.8.0", + "chai-dom": "^1.10.0", "chai-spies": "^1.0.0", "cross-env": "^7.0.3", "cypress": "^4.12.1", @@ -125,11 +135,12 @@ "eslint-plugin-prettier": "^3.4.0", "eslint-plugin-react": "^7.24.0", "eslint-plugin-react-hooks": "^4.2.0", + "eslint-plugin-testing-library": "^5.0.0", "fast-glob": "^3.2.5", "husky": "^7.0.1", "i18next": "^20.3.2", - "jsdom-global": "3.0.2", - "mocha": "^9.0.0", + "jsdom-global": "^3.0.2", + "mocha": "^9.1.3", "mock-require": "^3.0.3", "pino-pretty": "^7.1.0", "postcss": "^8.3.5", @@ -177,10 +188,11 @@ "@rocket.chat/ui-kit": "^0.30.1", "@slack/client": "^4.12.0", "@types/cookie": "^0.4.1", - "@types/lodash": "^4.14.171", + "@types/lodash": "^4.14.177", "@types/lodash.debounce": "^4.0.6", "adm-zip": "0.4.14", "agenda": "github:RocketChat/agenda#3.1.2", + "ajv": "^8.7.1", "apn": "2.2.0", "archiver": "^3.1.1", "atlassian-crowd-patched": "^0.5.1", @@ -209,6 +221,7 @@ "ejson": "^2.2.1", "emailreplyparser": "^0.0.5", "emojione": "^4.5.0", + "eslint-plugin-anti-trojan-source": "^1.0.6", "eventemitter3": "^4.0.7", "exif-be-gone": "^1.2.0", "express": "^4.17.1", @@ -274,6 +287,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-keyed-flatten-children": "^1.3.0", + "react-query": "^3.33.1", "react-virtuoso": "^1.2.4", "semver": "^5.7.1", "sharp": "^0.22.1", @@ -304,7 +318,7 @@ "meteor": { "mainModule": { "client": "client/main.ts", - "server": "server/main.js" + "server": "server/main.ts" } }, "houston": { @@ -322,6 +336,7 @@ "last 2 versions" ], "volta": { - "node": "12.22.1" + "node": "12.22.1", + "npm": "6.14.12" } } diff --git a/packages/rocketchat-i18n/i18n/af.i18n.json b/packages/rocketchat-i18n/i18n/af.i18n.json index 2c83ea2baafa..5d56a61b8ef0 100644 --- a/packages/rocketchat-i18n/i18n/af.i18n.json +++ b/packages/rocketchat-i18n/i18n/af.i18n.json @@ -555,6 +555,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Deurlopende klank kennisgewings vir nuwe livechat kamer", "Conversation": "gesprek", "Conversation_closed": "Gesprek gesluit: __comment__.", + "Conversation_finished": "Gesprek afgehandel", "Conversation_finished_message": "Gesprek Beëindigde Boodskap", "conversation_with_s": "die gesprek met %s", "Convert_Ascii_Emojis": "Skakel ASCII om na Emoji", @@ -2682,6 +2683,7 @@ "Users_added": "Die gebruikers is bygevoeg", "Users_in_role": "Gebruikers in rol", "UTF8_Names_Slugify": "UTF8 Name Slugify", + "Videocall_enabled": "Video-oproep aangeskakel", "Validate_email_address": "Bevestig e-pos adres", "Verification": "Verifikasie", "Verification_Description": "U mag die volgende plekhouers gebruik:
    • [Verifikasie_Url] vir die verifikasie-URL.
    • [naam], [fname], [lname] vir die volle naam, voornaam of van die gebruiker se naam.
    • [e-pos] vir die gebruiker se e-pos.
    • [Site_Name] en [Site_URL] vir die Aansoek Naam en URL onderskeidelik.
    ", @@ -2696,7 +2698,6 @@ "Video_Conference": "Videokonferensie", "Video_message": "Video boodskap", "Videocall_declined": "Video-oproep geweier.", - "Videocall_enabled": "Video-oproep aangeskakel", "View_All": "Bekyk alle lede", "View_Logs": "Bekyk logs", "View_mode": "Kyk af", diff --git a/packages/rocketchat-i18n/i18n/ar.i18n.json b/packages/rocketchat-i18n/i18n/ar.i18n.json index b7fb71beaade..958898b741a7 100644 --- a/packages/rocketchat-i18n/i18n/ar.i18n.json +++ b/packages/rocketchat-i18n/i18n/ar.i18n.json @@ -22,6 +22,7 @@ "access-mailer_description": "التصريح بإرسال إيميلات دفعة واحدة إلى كل المستخدمين", "access-permissions": "الدخول إلى صفحة الصلاحيات والأذونات", "access-permissions_description": "تعديل الصلاحيات للأدوار المختلفة", + "access-setting-permissions": "تعديل الأذونات المستندة إلى الإعداد", "Accessing_permissions": "أذونات الوصول", "Account_SID": "SID حساب", "Accounts": "الحسابات", @@ -39,6 +40,7 @@ "Accounts_AllowUserAvatarChange": "السماح بتغيير الصورة الرمزية", "Accounts_AllowUsernameChange": "السماح بتغيير اسم المستخدم", "Accounts_AllowUserProfileChange": "السماح بتعديل الملف الشخصي للعضو", + "Accounts_AllowUserStatusMessageChange": "السماح برسالة الحالة المخصصة", "Accounts_AvatarCacheTime": "الصورة الرمزية وقت الكاش", "Accounts_AvatarCacheTime_description": "عدد الثواني التي يُطلب من بروتوكول http فيها تخزين الصور الرمزية في ذاكرة التخزين المؤقت.", "Accounts_AvatarResize": "تغيير حجم الصور الرمزية", @@ -56,6 +58,7 @@ "Accounts_Default_User_Preferences_not_available": "أخفق استرداد تفضيلات المستخدم لأنه لم يتم إعدادها من قبل المستخدم حتى الآن", "Accounts_DefaultUsernamePrefixSuggestion": "اقتراح بادئة اسم المستخدم الافتراضية", "Accounts_denyUnverifiedEmail": "رفض البريد الإلكتروني لم يتم التحقق منها", + "Accounts_Directory_DefaultView": "قائمة الدليل الافتراضية", "Accounts_Email_Activated": "[نيم]

    تم تنشيط حسابك.

    ", "Accounts_Email_Activated_Subject": "تم تفعيل الحساب", "Accounts_Email_Approved": "[نيم]

    تمت الموافقة على حسابك.

    ", @@ -88,6 +91,7 @@ "Accounts_OAuth_Custom_Roles_Claim": "اسم حقل الأدوار / المجموعات", "Accounts_OAuth_Custom_Scope": "الإطار", "Accounts_OAuth_Custom_Secret": "سر", + "Accounts_OAuth_Custom_Show_Button_On_Login_Page": "إظهار الزر في صفحة تسجيل الدخول", "Accounts_OAuth_Custom_Token_Path": "مسار رمزي", "Accounts_OAuth_Custom_Token_Sent_Via": "رمزي المرسلة عن طريق", "Accounts_OAuth_Custom_Username_Field": "حقل اسم المستخدم", @@ -96,19 +100,19 @@ "Accounts_OAuth_Drupal_id": "رمز تعريف العميل الخاص بدوربال oAuth2", "Accounts_OAuth_Drupal_secret": "مفتاح العميل السري الخاص بدوربال oAuth2", "Accounts_OAuth_Facebook": "تسجيل الدخول الى الفيسبوك", - "Accounts_OAuth_Facebook_callback_url": "URL الفيسبوك الاستدعاء", + "Accounts_OAuth_Facebook_callback_url": "رابط الفيسبوك الاستدعاء", "Accounts_OAuth_Facebook_id": "الفيسبوك معرف التطبيق", "Accounts_OAuth_Facebook_secret": "الفيسبوك السرية", "Accounts_OAuth_Github": "أوث ممكن", - "Accounts_OAuth_Github_callback_url": "URL جيثب الاستدعاء", + "Accounts_OAuth_Github_callback_url": "رابط جيثب الاستدعاء", "Accounts_OAuth_GitHub_Enterprise": "أوث ممكن", - "Accounts_OAuth_GitHub_Enterprise_callback_url": "URL جيثب المؤسسة الاستدعاء", + "Accounts_OAuth_GitHub_Enterprise_callback_url": "رابط جيثب المؤسسة الاستدعاء", "Accounts_OAuth_GitHub_Enterprise_id": "رقم العميل", "Accounts_OAuth_GitHub_Enterprise_secret": "سر العميل", "Accounts_OAuth_Github_id": "رقم العميل", "Accounts_OAuth_Github_secret": "سر العميل", "Accounts_OAuth_Gitlab": "أوث ممكن", - "Accounts_OAuth_Gitlab_callback_url": "URL GitLab الاستدعاء", + "Accounts_OAuth_Gitlab_callback_url": "رابط GitLab الاستدعاء", "Accounts_OAuth_Gitlab_id": "GitLab رقم", "Accounts_OAuth_Gitlab_identity_path": "مسار الهوية", "Accounts_OAuth_Gitlab_secret": "سر العميل", @@ -170,6 +174,10 @@ "Accounts_Registration_AuthenticationServices_Default_Roles": "الأدوار الافتراضية لخدمات المصادقة", "Accounts_Registration_AuthenticationServices_Default_Roles_Description": "سيتم تعيين الأدوار الافتراضية للمستخدمين عند التسجيل عبر خدمات المصادقة (افصل بينها بفاصلة)", "Accounts_Registration_AuthenticationServices_Enabled": "تسجيل مع خدمات المصادقة", + "Accounts_Registration_Users_Default_Roles": "الأدوار الافتراضية للمستخدمين", + "Accounts_Registration_Users_Default_Roles_Description": "سيتم منح المستخدمين الأدوار الافتراضية (مفصولة بفواصل) عند التسجيل من خلال التسجيل اليدوي (بما في ذلك عبر واجهة برمجة التطبيقات)", + "Accounts_Registration_Users_Default_Roles_Enabled": "تمكين الأدوار الافتراضية للتسجيل اليدوي", + "Accounts_Registration_InviteUrlType": " نوع رابط الدعوة", "Accounts_RegistrationForm": "استمارة التسجيل", "Accounts_RegistrationForm_Disabled": "معطل", "Accounts_RegistrationForm_LinkReplacementText": "نص نموذج التسجيل رابط بديل", @@ -183,6 +191,7 @@ "Accounts_SetDefaultAvatar": "ضبط إعدادات الصورة الرمزية", "Accounts_SetDefaultAvatar_Description": "يتم المحاولة لتعيين الصورة الرمزية الافتراضية بناءً على حساب OAuth أو حساب Gravatar", "Accounts_ShowFormLogin": "يستند النموذج مشاهدة الدخول", + "Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In_Description": "سيتم تمكين المصادقة الثنائية عبر البريد الإلكتروني للمستخدمين الجدد افتراضيًا. سيكونون قادرين على تعطيله في صفحة ملفهم الشخصي.", "Accounts_TwoFactorAuthentication_Enabled": "تمكين المصادقة الثنائية", "Accounts_TwoFactorAuthentication_MaxDelta": "أقصى دلتا", "Accounts_TwoFactorAuthentication_MaxDelta_Description": "يحدد Maximum Delta عدد الرموز المميزة في أي وقت معين. يتم إنشاء الرموز المميزة كل 30 ثانية ، وتكون صالحة لمدة (30 * Maximum Delta) ثانية.
    مثال: مع تعيين الحد الأقصى من دلتا على 10 ، يمكن استخدام كل رمز مميز حتى 300 ثانية قبل أو بعد طابعه الزمني. هذا مفيد عندما لا تتم مزامنة ساعة العميل بشكل صحيح مع الخادم.", @@ -198,7 +207,7 @@ "Add_agent": "أضف وكيل", "Add_custom_oauth": "إضافة أوث مخصصة", "Add_Domain": "إضافة اسم مجال", - "Add_files_from": "إضافة ملفات من", + "Add_files_from": "إضافة ملفات من:", "Add_manager": "أضف مدير", "Add_Reaction": "إضافة تفاعل", "Add_Role": "إضافة دور", @@ -225,6 +234,7 @@ "Admin_Info": "معلومات المسؤول", "Administration": "الإدارة", "Adult_images_are_not_allowed": "لا يسمح بالصور للبالغين", + "Aerospace_and_Defense": "الفضاء والدفاع", "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "بعد المصادقة في OAuth2، سيتم إعادة توجيه المستخدمين إلى هذا الرابط", "Agent": "الموظف", "Agent_added": "تمت إضافة الوكيل", @@ -280,11 +290,15 @@ "API_Enable_CORS": "تفعيل CORS", "API_Enable_Direct_Message_History_EndPoint": "تفعيل نهاية سجل الرسائل المباشرة ", "API_Enable_Direct_Message_History_EndPoint_Description": "وهذا يتيح `/ أبي / v1 / im.history.others` الذي يسمح بعرض الرسائل المباشرة المرسلة من قبل المستخدمين الآخرين أن المتصل ليست جزءا من.", + "API_Enable_Rate_Limiter_Limit_Calls_Default": "يدعو الرقم الافتراضي إلى محدد السعر", + "API_Enable_Rate_Limiter_Limit_Calls_Default_Description": "عدد الاستدعاءات الافتراضية لكل نقطة نهاية لواجهة برمجة تطبيقات REST ، المسموح بها خلال النطاق الزمني المحدد أدناه", + "API_Enable_Rate_Limiter_Limit_Time_Default": "الحد الزمني الافتراضي لمحدد المعدل (بالمللي ثانية)", + "API_Enable_Rate_Limiter_Limit_Time_Default_Description": "المهلة الافتراضية للحد من عدد المكالمات في كل نقطة نهاية لواجهة برمجة تطبيقات REST (بالمللي ثانية)", "API_Enable_Shields": "تمكين الدروع", "API_Enable_Shields_Description": "تمكين الدروع المتاحة في `/ أبي / V1 / shield.svg`", - "API_GitHub_Enterprise_URL": "URL الخادم", + "API_GitHub_Enterprise_URL": "رابط الخادم", "API_GitHub_Enterprise_URL_Description": "مثال: http://domain.com (بدون الشرطة المائلة في الأخير)", - "API_Gitlab_URL": "URL GitLab", + "API_Gitlab_URL": "رابط GitLab", "API_Shield_Types": "أنواع الدرع", "API_Shield_Types_Description": "أنواع الدروع للتمكين كقائمة مفصولة بفواصل، اختر من `online` أو` channel` أو `*` للجميع", "API_Token": "API رمز", @@ -293,7 +307,7 @@ "API_Upper_Count_Limit": "الحد الأقصى لقيمة القيد", "API_Upper_Count_Limit_Description": "ما هو الحد الأقصى لعدد السجلات التي يجب أن تعودها واجهة برمجة تطبيقات ريست (عندما لا تكون غير محدودة)؟", "API_User_Limit": "حد المستخدم لإضافة جميع الاعضاء للقناة", - "API_Wordpress_URL": "URL ورد", + "API_Wordpress_URL": "رابط ورد", "Apiai_Key": "Api.ai مفتاح", "Apiai_Language": "Api.ai اللغة", "App_author_homepage": "المؤلف الصفحة الرئيسية", @@ -317,6 +331,8 @@ "Apply_and_refresh_all_clients": "تطبيق وتحديث كافة عملاء", "Apps": "التطبيقات", "Apps_Framework_enabled": "تمكين إطار التطبيق", + "Apps_Permissions_upload_read": "الوصول للملفات التي تم تحميلها على هذا الخادم", + "Apps_Permissions_upload_write": "تحميل الملفات على هذا الخادم", "Apps_Settings": "إعدادات التطبيق", "Apps_WhatIsIt": "التطبيقات: ما هي؟", "Apps_WhatIsIt_paragraph1": "أيقونة جديدة في منطقة الإدارة! ماذا يعني هذا وما هي التطبيقات؟", @@ -329,7 +345,7 @@ "are_typing": "يكتبون", "Are_you_sure": "هل أنت متأكد؟", "Are_you_sure_you_want_to_delete_your_account": "هل أنت متأكد من أنك تريد حذف حسابك؟", - "Are_you_sure_you_want_to_disable_Facebook_integration": "هل تريد بالتأكيد تعطيل دمج فاسيبوك؟", + "Are_you_sure_you_want_to_disable_Facebook_integration": "هل تريد بالتأكيد تعطيل دمج فيسبوك؟", "Assign_admin": "تعيين المدير", "assign-admin-role": "تعيين دور المدير", "assign-admin-role_description": "التصريح بمنح المستخدمين الآخرين دور المدير", @@ -390,11 +406,12 @@ "Backup_codes": "شيفرات التخزين الاحتياطي", "ban-user": "حظر المستخدم", "ban-user_description": "التصريح لحظر مستخدم من القناة", + "BBB_Video_Call": "جلسة BBB", "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "ميزة تجريبية. تعتمد على تفعيل إقامة مؤتمر عبر الفيديو.", "Block_User": "حظر المستخدم", "Blockchain": "Blockchain", "Body": "الجسم", - "bold": "عريض", + "bold": "غامق", "bot_request": "طلب الروبوت", "BotHelpers_userFields": "حقول المستخدم", "BotHelpers_userFields_Description": "كسف من حقول المستخدم التي يمكن الوصول إليها عن طريق طرق مساعد السير.", @@ -403,10 +420,11 @@ "Broadcast_channel": "قناة البث", "Broadcast_channel_Description": "يمكن للمستخدمين المصرح لهم فقط كتابة رسائل جديدة ، ولكن سيتمكن المستخدمون الآخرون من الرد", "Broadcast_Connected_Instances": "بث حالات متصلة", + "Browse_Files": "تصفح ملفات", "Bugsnag_api_key": "مفتاح بوجسناغ أبي", "Build_Environment": "بناء البيئة", - "bulk-register-user": "إنشاء قنوات دفعة واحدة", - "bulk-register-user_description": "التصريح بإنشاء قنوات دفعة واحدة", + "bulk-register-user": "إنشاء مستخدمين دفعة واحدة", + "bulk-register-user_description": "التصريح بإنشاء مستخدمين دفعة واحدة", "busy": "مشغول", "Busy": "مشغول", "busy_female": "مشغولة", @@ -415,6 +433,8 @@ "Busy_male": "مشغول", "by": "بواسطة", "cache_cleared": "تم مسح التخزين المؤقت", + "Call": "مكالمة", + "Caller": "متصل", "Cancel": "إلغاء", "Cancel_message_input": "لإلغاء التغييرات", "Cannot_invite_users_to_direct_rooms": "لا يمكن دعوة المستخدمين إلى غرف توجيه", @@ -453,7 +473,7 @@ "Channel_Name_Placeholder": "الرجاء إدخال اسم القناة ...", "Channel_to_listen_on": "السماع إلى هذه القناة", "Channel_Unarchived": "وكانت قناة مع اسم `# %s` إلغاء أرشفة بنجاح", - "Channels": "القنوات", + "Channels": "Channel", "Channels_are_where_your_team_communicate": "القنوات هي المكان الذي يتواصل فيه فريقك", "Channels_list": "قائمة القنوات العامة", "Chat_button": "زر الدردشة", @@ -539,6 +559,7 @@ "Closed": "مغلق", "Closed_by_visitor": "تم الإغلاق من قبل الزائر", "Closing_chat": "إغلاق الدردشة", + "Cloud_logout": "تسجيل الخروج من سحابة Rocket.Chat", "Collaborative": "تعاونية", "Collapse_Embedded_Media_By_Default": "إخفاء الوسائط المدمجة بشكل تلقائي", "color": "اللون", @@ -564,6 +585,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "إخطارات صوتية مستمرة لغرفة livechat الجديدة", "Conversation": "محادثة", "Conversation_closed": "المحادثة أغلقت: __comment__.", + "Conversation_finished": "تم إنهاء المحادثة", "Conversation_finished_message": "المحادثة انتهى الرسالة", "conversation_with_s": "المحادثة مع %s", "Convert_Ascii_Emojis": "حول محارف الأسكي إلى اموجي", @@ -824,6 +846,8 @@ "create-d_description": "التصريح بالبدء بإرسال رسائل مباشرة", "create-p": "إنشاء قنوات خاصة", "create-p_description": "التصريح بإنشاء قنوات خاصة", + "create-personal-access-tokens": "قم بإنشاء رموز وصول شخصية", + "create-personal-access-tokens_description": "إذن لإنشاء رموز وصول شخصية", "create-user": "إنشاء مستخدم", "create-user_description": "التصريح بإنشاء مستخدمين", "Created_at": "أنشئت في", @@ -852,6 +876,8 @@ "Custom_oauth_unique_name": "اسم فريد أوث مخصص", "Custom_Script_Logged_In": "سيناريو المخصصة لتسجيل الدخول للمستخدمين", "Custom_Script_Logged_Out": "سيناريو مخصصة للمستخدمين تسجيل الخروج", + "Custom_Script_On_Logout": "برنامج نصي مخصص لتدفق تسجيل الخروج", + "Custom_Script_On_Logout_Description": "البرنامج النصي المخصص الذي سيتم تشغيله عند تنفيذ تدفق الخروج فقط", "Custom_Scripts": "سكربتات مخصصة", "Custom_Sound_Add": "إضافة صوت مخصص", "Custom_Sound_Delete_Warning": "لا يمكن التراجع عن حذف الصوت", @@ -876,6 +902,7 @@ "Deactivate": "تعطيل", "Decline": "إلغاء", "Default": "افتراضي", + "Default_value": "القيمة الافتراضية", "Delete": "حذف", "Delete_message": "حذف رسالة", "Delete_my_account": "حذف حسابي", @@ -911,6 +938,7 @@ "Desktop_Notifications_Enabled": "تنبيهات سطح المكتب مفعلة", "Different_Style_For_User_Mentions": "نمط مختلف للمستخدم يذكر", "Direct_message_someone": "رسالة شخص المباشر", + "Direct_message_you_have_joined": "لقد انضممت إلى رسالة مباشرة جديدة مع ", "Direct_Messages": "الرسائل المباشرة", "Direct_Reply": "الرد المباشر", "Direct_Reply_Debug": "تصحيح الرد المباشر", @@ -920,7 +948,7 @@ "Direct_Reply_Frequency": "تردد التحقق من البريد الإلكتروني", "Direct_Reply_Frequency_Description": "(بالدقائق، الافتراضي / الحد الأدنى 2)", "Direct_Reply_Host": "الرد المباشر المضيف", - "Direct_Reply_IgnoreTLS": "IgnoreTLS", + "Direct_Reply_IgnoreTLS": "تجاهل TLS", "Direct_Reply_Password": "كلمة السر", "Direct_Reply_Port": "Direct_Reply_Port", "Direct_Reply_Protocol": "بروتوكول الرد المباشر", @@ -931,15 +959,23 @@ "Directory": "دليل", "Disable_Facebook_integration": "تعطيل التكامل الفيسبوك", "Disable_Notifications": "إلغاء تفعيل الإشعارات", - "Disable_two-factor_authentication": "إلغاء تفعيل المصادقة بخطوتين", + "Disable_two-factor_authentication": "تعطيل المصادقة الثنائية عبر TOTP", + "Disable_two-factor_authentication_email": "تعطيل المصادقة الثنائية عبر البريد الإلكتروني", "Disabled": "تعطيل", "Disallow_reacting": "عدم الاستجابة", "Disallow_reacting_Description": "لا تسمح بالتفاعل", + "Discussion": "مناقشة", "Discussion_description": "ساهم بإعطاء نظرة عامة لما يجري. عند إنشاء مناقشة يتم إنشاء قناة فرعية وربطها بالقناة المحددة", + "Discussion_first_message_disabled_due_to_e2e": "يمكنك البدء في إرسال رسائل مشفرة من طرف إلى طرف في هذه المناقشة بعد إنشائها.", "Discussion_name": "اسم النقاش", "Discussion_start": "ابدأ نقاش", "Discussion_target_channel": "القناة أو المجموعة الأب", + "Discussion_target_channel_prefix": "أنت تقوم بإنشاء مناقشة في", + "Discussion_title": "قم بإنشاء مناقشة جديدة", "Discussions": "مناقشات", + "Display": "عرض", + "Display_avatars": "عرض الصور الرمزية", + "Display_Avatars_Sidebar": "عرض الصور الرمزية في الشريط الجانبي", "Display_offline_form": "عرض النموذج متواجد حاليا", "Display_unread_counter": "عرض عدد الرسائل غير المقروءة", "Displays_action_text": "نص العمل يعرض", @@ -1009,11 +1045,14 @@ "Emoji_provided_by_JoyPixels": "JoyPixels رموز تعبيرية مقدمة من", "EmojiCustomFilesystem": "ملفات الرموز التعبيرية المخصصة", "Empty_title": "عنوان فارغ", + "Enable_message_parser_early_adoption": "قم بتمكين المحلل اللغوي للرسائل", + "Enable_message_parser_early_adoption_alert": "هذه ميزة تجريبية وستظل على هذا النحو على الأقل حتى الإصدار 3.19.0 ، وهذا الخيار هو مساعدتنا في الاختبارات. بمجرد عدم العثور على أي مشاكل أخرى ، سنقوم بإزالة هذا الخيار والانتقال إلى الحل الجديد", "Enable": "تمكين", "Enable_Auto_Away": "تمكين السيارات بعيدا", "Enable_Desktop_Notifications": "تفعيل تنبيهات سطح المكتب", "Enable_Svg_Favicon": "تفعيل أيقونة SVG ", - "Enable_two-factor_authentication": "تفعيل المصادقة بخطوتين", + "Enable_two-factor_authentication": "تمكين المصادقة الثنائية عبر TOTP", + "Enable_two-factor_authentication_email": "قم بتمكين المصادقة الثنائية عبر البريد الإلكتروني", "Enabled": "مفعل", "Encrypted": "مشفر", "Encrypted_message": "رسالة مشفرة", @@ -1087,10 +1126,10 @@ "error-invalid-subscription": "الاشتراك غير صالح", "error-invalid-token": "رمز غير صحيح", "error-invalid-triggerWords": "triggerWords غير صالح", - "error-invalid-urls": "عنوان URL غير صالح", + "error-invalid-urls": "روابط غير صالحة", "error-invalid-user": "مستخدم غير صالح", "error-invalid-username": "اسم المستخدم غير صالح", - "error-invalid-webhook-response": "رد URL webhook مع أي وضع آخر من 200", + "error-invalid-webhook-response": "رد رابط webhook مع أي وضع آخر من 200", "error-logged-user-not-in-room": "أنت لست في الغرفة ` %s`", "error-message-deleting-blocked": "يتم حظر رسالة حذف", "error-message-editing-blocked": "تم حظر تحرير رسالة", @@ -1107,6 +1146,7 @@ "error-password-policy-not-met-oneSpecial": "لا تتوافق كلمة المرور مع سياسة الخادم ذات الحرف الخاص واحد على الأقل", "error-password-policy-not-met-oneUppercase": "كلمة المرور لا تتوافق مع نهج الخادم ذي الحرف الكبير واحد على الأقل", "error-password-policy-not-met-repeatingCharacters": "لا تتوافق كلمة المرور مع سياسة الخادم الخاصة بأحرف مكررة ممنوعة (لديك الكثير من نفس الأحرف بجانب بعضها البعض)", + "error-personal-access-tokens-are-current-disabled": "رموز الوصول الشخصية معطلة حاليًا", "error-push-disabled": "تم تعطيل دفع", "error-remove-last-owner": "هذا هو صاحب الماضي. يرجى تحديد المالك الجديد قبل إزالة هذه واحدة.", "error-role-in-use": "لا يمكن حذف دور لأنه في استخدام", @@ -1120,7 +1160,7 @@ "error-user-limit-exceeded": "يتجاوز عدد المستخدمين الذين تحاول دعوتهم إلى #channel_name الحد الذي حدده المشرف", "error-user-not-in-room": "المستخدم ليس في هذه الغرفة", "error-user-registration-disabled": "تم تعطيل تسجيل المستخدم", - "error-user-registration-secret": "يسمح تسجيل المستخدم فقط عبر URL السري", + "error-user-registration-secret": "يسمح تسجيل المستخدم فقط عبر رابط سري", "error-you-are-last-owner": "كنت صاحب الماضي. الرجاء تعيين المالك الجديد قبل مغادرة الغرفة.", "Esc_to": "زر الهروب", "Event_Trigger": "مشغل العملية", @@ -1151,11 +1191,12 @@ "Favorites": "المفضلة", "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "تعتمد هذه الميزة على \"إرسال محفوظات تنقل الزائر كرسالة\" ليتم تمكينها.", "Features_Enabled": "الميزات الممكنة", + "Federation_Invite_Users_To_Private_Rooms": "من الآن فصاعدًا ، يمكنك دعوة المستخدمين المتحدين فقط إلى الغرف الخاصة أو المناقشات.", "Federation_Domain": "نطاق", "FEDERATION_Domain": "نطاق", "FEDERATION_Status": "الحالة", "Field": "حقل", - "Field_removed": "إزالة الميدان", + "Field_removed": "إزالة الحقل", "Field_required": "حقل مطلوب", "File_exceeds_allowed_size_of_bytes": "يتجاوز حجم الملف المسموح به بايت __size__", "File_name_Placeholder": "ابحث في الملفات...", @@ -1180,9 +1221,9 @@ "FileUpload_GoogleStorage_Bucket": "اسم سلة تخزين غوغل", "FileUpload_GoogleStorage_Bucket_Description": "اسم الدلو الذي يجب تحميل الملفات إليه.", "FileUpload_GoogleStorage_Proxy_Avatars": "تجسيد الوكيل", - "FileUpload_GoogleStorage_Proxy_Avatars_Description": "يتم إرسال ملفات الملف الشخصي في برنامج الخادم الوكيل عبر الخادم بدلاً من الوصول المباشر إلى عنوان URL للأصل", + "FileUpload_GoogleStorage_Proxy_Avatars_Description": "يتم إرسال ملفات الملف الشخصي في برنامج الخادم الوكيل عبر الخادم بدلاً من الوصول المباشر إلى عنوان رابط للأصل", "FileUpload_GoogleStorage_Proxy_Uploads": "تحميل الوكيل", - "FileUpload_GoogleStorage_Proxy_Uploads_Description": "يتم إرسال عمليات نقل ملف الخادم عبر الخادم بدلاً من الوصول المباشر إلى عنوان URL للأصل", + "FileUpload_GoogleStorage_Proxy_Uploads_Description": "يتم إرسال عمليات نقل ملف الخادم عبر الخادم بدلاً من الوصول المباشر إلى عنوان رابط للأصل", "FileUpload_GoogleStorage_Secret": "المفتاح السري لتخزين غوغل", "FileUpload_GoogleStorage_Secret_Description": "يرجى اتباع هذه الإرشاداتولصق النتيجة هنا.", "FileUpload_MaxFileSize": "الحد الأقصى لتحميل الملف الحجم (بايت)", @@ -1196,13 +1237,13 @@ "FileUpload_S3_AWSAccessKeyId": "الأمازون S3 AWSAccessKeyId", "FileUpload_S3_AWSSecretAccessKey": "الأمازون S3 AWSSecretAccessKey", "FileUpload_S3_Bucket": "الأمازون S3 اسم دلو", - "FileUpload_S3_BucketURL": "URL دلو", + "FileUpload_S3_BucketURL": "رابط دلو", "FileUpload_S3_CDN": "نطاق كندي للتنزيل", "FileUpload_S3_ForcePathStyle": "فرض مظهر المسار", "FileUpload_S3_Proxy_Avatars": "تجسيد الوكيل", - "FileUpload_S3_Proxy_Avatars_Description": "يتم إرسال ملفات الملف الشخصي في برنامج الخادم الوكيل عبر الخادم بدلاً من الوصول المباشر إلى عنوان URL للأصل", + "FileUpload_S3_Proxy_Avatars_Description": "يتم إرسال ملفات الملف الشخصي في برنامج الخادم الوكيل عبر الخادم بدلاً من الوصول المباشر إلى عنوان رابط للأصل", "FileUpload_S3_Proxy_Uploads": "تحميل الوكيل", - "FileUpload_S3_Proxy_Uploads_Description": "يتم إرسال عمليات نقل ملف الخادم عبر الخادم بدلاً من الوصول المباشر إلى عنوان URL للأصل", + "FileUpload_S3_Proxy_Uploads_Description": "يتم إرسال عمليات نقل ملف الخادم عبر الخادم بدلاً من الوصول المباشر إلى عنوان رابط للأصل", "FileUpload_S3_Region": "منطقة", "FileUpload_S3_SignatureVersion": "نسخة التوقيع", "FileUpload_S3_URLExpiryTimeSpan": "فترة انتهاء صلاحية الروابط", @@ -1210,10 +1251,10 @@ "FileUpload_Storage_Type": "نوع التخزين", "FileUpload_Webdav_Password": "WebDAV كلمة السر", "FileUpload_Webdav_Proxy_Avatars": "تجسيد الوكيل", - "FileUpload_Webdav_Proxy_Avatars_Description": "يتم إرسال ملفات الملف الشخصي في برنامج الخادم الوكيل عبر الخادم بدلاً من الوصول المباشر إلى عنوان URL للأصل", + "FileUpload_Webdav_Proxy_Avatars_Description": "يتم إرسال ملفات الملف الشخصي في برنامج الخادم الوكيل عبر الخادم بدلاً من الوصول المباشر إلى عنوان رابط للأصل", "FileUpload_Webdav_Proxy_Uploads": "تحميل الوكيل", - "FileUpload_Webdav_Proxy_Uploads_Description": "يتم إرسال عمليات نقل ملف الخادم عبر الخادم بدلاً من الوصول المباشر إلى عنوان URL للأصل", - "FileUpload_Webdav_Server_URL": "عنوان URL لدخول خادم WebDAV", + "FileUpload_Webdav_Proxy_Uploads_Description": "يتم إرسال عمليات نقل ملف الخادم عبر الخادم بدلاً من الوصول المباشر إلى عنوان رابط للأصل", + "FileUpload_Webdav_Server_URL": "عنوان رابط لدخول خادم WebDAV", "FileUpload_Webdav_Upload_Folder_Path": "تحميل مسار المجلد", "FileUpload_Webdav_Upload_Folder_Path_Description": "مسار مجلد WebDAV الذي يجب أن يتم تحميل الملفات إليه", "FileUpload_Webdav_Username": "اسم مستخدم WebDAV", @@ -1222,7 +1263,9 @@ "Financial_Services": "الخدمات المالية", "First_Channel_After_Login": "القناة الأولى بعد تسجيل الدخول", "Flags": "أعلام", + "Follow_message": "اتبع الرسالة", "Follow_social_profiles": "اتبع محات اجتماعية لدينا، مفترق لنا على جيثب وتبادل الأفكار حول التطبيق rocket.chat على متن trello لدينا.", + "Following": "تابع", "Fonts": "الخطوط", "Food_and_Drink": "طعام و مشروبات", "Footer": "تذييل", @@ -1250,8 +1293,9 @@ "From_Email": "من البريد الإلكتروني", "From_email_warning": "تحذير: حقل من يخضع لإعدادات خادم البريد الخاص بك.", "Gaming": "الألعاب", - "General": "العامة", + "General": "عام", "Generate_New_Link": "إنشاء رابط جديد", + "Get_link": "إنسخ الرابط", "github_no_public_email": "ليس لديك أي بريد الإلكتروني عام في حسابك على Github", "Give_a_unique_name_for_the_custom_oauth": "تعطي اسما فريدا لأوث مخصصة", "Give_the_application_a_name_This_will_be_seen_by_your_users": "إعطاء التطبيق اسما. وسوف يظهر هذا من قبل المستخدمين.", @@ -1264,6 +1308,7 @@ "GoogleTagManager_id": "جوجل مدير العلامات معرف", "Government": "الحكومي", "Group_by_Type": "المجموعة حسب النوع", + "Group_discussions": "مناقشات جماعية", "Group_favorites": "مجموعة المفضلة", "Group_mentions_disabled_x_members": "تشير المجموعة إلى \"@ all\" و \"@ here\" تم تعطيلها للغرف التي تضم أكثر من __total__ أعضاء.", "Group_mentions_only": "تشير المجموعة فقط", @@ -1359,7 +1404,7 @@ "Install_FxOs_error": "عذرا، لم ينجح على النحو المنشود! ظهر الخطأ التالي:", "Install_FxOs_follow_instructions": "الرجاء التأكد من تثبيت التطبيق على جهازك (اضغط على \"تثبيت\" عندما دفع).", "Install_package": "ثبت المجموعة", - "Installation": "تركيب", + "Installation": "تنصيب", "Installed_at": "تثبيت في", "Instance_Record": "مثال على قيد", "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "تعليمات لزائرك ملء النموذج لإرسال رسالة", @@ -1497,6 +1542,7 @@ "Keyboard_Shortcuts_Keys_5": "أمر(أو ألت) + سهم لليمين", "Keyboard_Shortcuts_Keys_6": "أمر(أو ألت) + سهم لأسفل", "Keyboard_Shortcuts_Keys_7": "شيفت+ أدخل", + "Keyboard_Shortcuts_Mark_all_as_read": "حدد جميع الرسائل (في جميع القنوات) كمقروءة", "Keyboard_Shortcuts_Move_To_Beginning_Of_Message": "الانتقال إلى بداية الرسالة", "Keyboard_Shortcuts_Move_To_End_Of_Message": "الانتقال إلى نهاية الرسالة", "Keyboard_Shortcuts_New_Line_In_Message": "سطر جديد في إدخال رسالة إنشاء", @@ -1536,6 +1582,7 @@ "LDAP_BaseDN_Description": "اسم المؤهل بالكامل المميز (DN) من الشجرة الفرعية LDAP تريد البحث للمستخدمين والمجموعات. يمكنك إضافة ما تريد. ومع ذلك، يجب أن تحدد كل مجموعة في قاعدة نطاق نفس المستخدمين التي تنتمي إليها. إذا قمت بتحديد مجموعات المستخدمين المحظورة، وفقط للمستخدمين الذين ينتمون لتلك الجماعات أن يكون في نطاق. نوصي تحديد المستوى الخاص بك شجرة دليل LDAP كقاعدة المجال الخاص بك واستخدام فلتر البحث للتحكم في الوصول.", "LDAP_CA_Cert": "CA سيرت", "LDAP_Connect_Timeout": "انتهاء مدة الاتصال (بالملي ثانية)", + "LDAP_DataSync_AutoLogout": "تسجيل الخروج التلقائي للمستخدمين المعطّلون", "LDAP_Default_Domain": "المجال الافتراضي", "LDAP_Default_Domain_Description": "إذا تم توفير النطاق الافتراضي سيتم استخدامه لإنشاء بريد إلكتروني فريد للمستخدمين حيث لم يتم استيراد البريد الإلكتروني من لداب. سيتم تثبيت الرسالة الإلكترونية باسم `أوزرنام @ default_domain` أور` unique_id @ default_domain`.
    مثال: `rocket.chat`", "LDAP_Enable": "تفعيل", @@ -1576,19 +1623,21 @@ "LDAP_Search_Page_Size_Description": "سيعود الحد الأقصى لعدد الإدخالات لكل صفحة نتيجة ليتم معالجتها", "LDAP_Search_Size_Limit": "حجم البحث الحد", "LDAP_Search_Size_Limit_Description": "الحد الأقصى لعدد الإدخالات المراد إرجاعها.
    ** الاهتمام ** يجب أن يزيد هذا الرقم عن ** حجم صفحة البحث **", + "LDAP_Sync_AutoLogout_Enabled": "تفعيل تسجيل الخروج التلقائي", + "LDAP_Sync_AutoLogout_Interval": "الفاصل الزمني لتسجيل الخروج التلقائي", "LDAP_Sync_Now": "خلفية مزامنة الآن", "LDAP_Sync_Now_Description": "سيتم تنفيذ ** مزامنة الخلفية ** الآن بدلا من الانتظار ** مزامنة الفاصل الزمني ** حتى لو ** مزامنة الخلفية ** هو خطأ.
    هذا الإجراء غير متزامن، يرجى الاطلاع على سجلات لمزيد من المعلومات حول معالجة", "LDAP_Sync_User_Avatar": "تزامن العضو الرمزية", "LDAP_Timeout": "مهلة (مللي ثانية)", "LDAP_Timeout_Description": "كم عدد الأميال التي تنتظر نتيجة بحث قبل إرجاع خطأ", - "LDAP_Unique_Identifier_Field": "معرف الميدان وفريدة من نوعها", + "LDAP_Unique_Identifier_Field": "حقل المعرف الفريد", "LDAP_Unique_Identifier_Field_Description": "التي الحقل سيتم استخدامها لربط المستخدم LDAP والمستخدم Rocket.Chat. يمكنك إبلاغ قيم متعددة مفصولة بفواصل في محاولة للحصول على قيمة من سجل LDAP.
    القيمة الافتراضية هي `objectGUID يتم، آي بي إم، entryUUID، GUID، dominoUNID، nsuniqueId، uidNumber`", "LDAP_User_Search_Field": "مجال البحث", "LDAP_User_Search_Field_Description": "السمة LDAP أن يحدد المستخدم LDAP الذي يحاول المصادقة. وينبغي أن يكون هذا المجال `sAMAccountName` للمنشآت الأكثر نشاطا دليل، ولكنه قد يكون` uid` عن حلول LDAP الأخرى، مثل ب OpenLDAP. يمكنك استخدام `mail` لتعريف المستخدمين عن طريق البريد الإلكتروني أو أيا كان السمة التي تريد.
    يمكنك استخدام قيم متعددة مفصولة بفواصل للسماح للمستخدمين تسجيل الدخول باستخدام معرفات متعددة مثل اسم المستخدم أو البريد الإلكتروني.", "LDAP_User_Search_Filter": "فلتر", "LDAP_User_Search_Filter_Description": "إذا كان سيتم السماح محددة، فقط للمستخدمين تطابق هذا المرشح لتسجيل الدخول. إذا لم يتم تحديد مرشح، جميع المستخدمين ضمن نطاق قاعدة المجال المحدد سوف تكون قادرة على تسجيل الدخول.
    على سبيل المثال ل Active Directory `memberOf = CN = ROCKET_CHAT، أوو = العام Groups`.
    على سبيل المثال لب OpenLDAP (للمد بحث مباراة) `أوو: DN: = ROCKET_CHAT`.", "LDAP_User_Search_Scope": "نطاق", - "LDAP_Username_Field": "اسم المستخدم الميدان", + "LDAP_Username_Field": "حقل اسم المستخدم", "LDAP_Username_Field_Description": "التي المجال سوف تستخدم * اسم المستخدم * للمستخدمين الجدد. اتركه فارغا لاستخدام اسم المستخدم على علم صفحة تسجيل الدخول.
    يمكنك استخدام العلامات قالب جدا، مثل `#{givenName}.#{sn}`.
    القيمة الافتراضية هي `sAMAccountName`.", "Lead_capture_email_regex": "التقاط التعبير العادي للبريد الإلكتروني", "Lead_capture_phone_regex": "يؤدي التقاط التعبير عن الهاتف الهاتف", @@ -1607,6 +1656,7 @@ "Livechat": "محادثة مباشرة", "Livechat_agents": "وكلاء المحادثات الحية", "Livechat_AllowedDomainsList": "أسماء المجالات المسموحة في الدردشة المباشرة", + "Livechat_auto_close_on_hold_chats_timeout_Description": "حدد المدة التي ستظل فيها الدردشة في قائمة الانتظار حتى يتم إغلاقها تلقائيًا بواسطة النظام. الوقت بالثواني", "Livechat_Dashboard": "لوحة المحادثات الحية", "Livechat_enabled": "تفعيل المحادثات الحية", "Livechat_Facebook_API_Key": "أومنيشانل أبي مفتاح", @@ -1656,11 +1706,11 @@ "Login": "تسجيل الدخول", "Login_with": "تسجيل الدخول بـ %s", "Logistics": "الخدمات اللوجستية", - "Logout": "تسجيل خروج", + "Logout": "تسجيل ألخروج", "Logout_Others": "تسجيل الخروج من الأجهزة الأخرى", "Mail_Message_Invalid_emails": "لقد قدمت رسائل البريد الإلكتروني واحدة أو أكثر غير صالحة:٪ ق", "Mail_Message_Missing_to": "يجب عليك اختيار واحد أو أكثر من المستخدمين أو تقديم واحدة أو أكثر من عناوين البريد الإلكتروني، مفصولة بفواصل.", - "Mail_Message_No_messages_selected_select_all": "لم تقم بتحديد أي رسالة. هل تريد أن تحدد جميع الرسائل الظاهرة؟", + "Mail_Message_No_messages_selected_select_all": "لم تقم بتحديد أي رسائل", "Mail_Messages": "إرسال الرسائل للبريد الألكتروني", "Mail_Messages_Instructions": "حدد الرسائل المراد إرسالها بالبريد الإلكتروني بالضغط على تلك الرسائل", "Mail_Messages_Subject": "مجموعة مختارة من رسائل %s", @@ -1692,9 +1742,10 @@ "Managing_integrations": "إدارة التكامل", "Manufacturing": "تصنيع", "MapView_Enabled": "تفعيل عرض الخريطة", - "MapView_Enabled_Description": "يعرض تفعيل عرض الخريطة زر مشاركة الموقع على يسار حقل رسالة الدردشة", + "MapView_Enabled_Description": "سيؤدي تمكين عرض الخريطة إلى عرض زر مشاركة الموقع على يمين حقل إدخال الدردشة.", "MapView_GMapsAPIKey": "مفتاح واجهة برمجة التطبيقات لخرائط غوغل الثابتة", "MapView_GMapsAPIKey_Description": "ويمكن الحصول على هذا من غوغل ديفيلوبيرز كونسول مجانا.", + "Mark_all_as_read": "حدد جميع الرسائل (في جميع القنوات) كمقروءة", "Mark_as_read": "تعليم كمقروء", "Mark_as_unread": "تعيين كغير مقروء", "Mark_unread": "علامة غير مقروء", @@ -1830,6 +1881,7 @@ "mute-user": "كتم المستخدم", "mute-user_description": "التصريح بكتم المستخدمين الآخرين في نفس القناة", "Muted": "صامتة", + "My Data": "بياناتي", "My_Account": "حسابي", "My_location": "موقعي", "n_messages": "%s رسائل", @@ -1845,6 +1897,8 @@ "New_Application": "تطبيق جديد", "New_Custom_Field": "حقل مخصص جديد", "New_Department": "الإدارة الجديدة", + "New_discussion": "مناقشة جديدة", + "New_discussion_first_message": "عادة ، تبدأ المناقشة بسؤال ، مثل \"كيف يمكنني تحميل صورة؟\"", "New_discussion_name": "اسم معبر لغرفة النقاش", "New_integration": "التكامل الجديد", "New_line_message_compose_input": "` %s` - سطر جديد في إدخال رسالة الإنشاء", @@ -1861,11 +1915,17 @@ "New_visitor_navigation": "التنقل الجديد: __history__", "Newer_than": "أحدث من", "Newer_than_may_not_exceed_Older_than": "\"أحدث من\" قد لا يتجاوز \"أقدم من\"", + "Nickname": "اسمك المستعار", + "Nickname_Placeholder": "أدخل اسمك المستعار...", "No": "لا", "No_available_agents_to_transfer": "لا يوجد أي موظفين ليتم نقلهم", "No_channel_with_name_%s_was_found": "لا توجد قناة باسم \"%s\"", "No_channels_yet": "لست جزء من أي قناة حتى الآن.", + "No_data_found": "لم يتم العثور على بيانات", "No_direct_messages_yet": "لم تبدأ أي محادثات حتى الآن.", + "No_Discussions_found": "لم يتم العثور على مناقشات", + "No_discussions_yet": "لا توجد مناقشات حتى الآن", + "No_emojis_found": "لم يتم العثور على رموز تعبيرية", "No_Encryption": "لا التشفير", "No_group_with_name_%s_was_found": "لا توجد مجموعة خاصة باسم \"%s\"", "No_groups_yet": "لا يوجد لديك مجموعات خاصة حتى الآن.", @@ -1888,11 +1948,14 @@ "Normal": "عادي", "Not_authorized": "غير مصرح", "Not_Available": "غير متاح", + "Not_following": "عدم اتباع", + "Not_Following": "عدم اتباع", "Not_found_or_not_allowed": "غير موجود أو غير مسموح", "Nothing": "لا شيء", "Nothing_found": "لا يوجد شيء", "Notification_Desktop_Default_For": "عرض الإخطارات سطح المكتب ل", "Notification_Push_Default_For": "دفع الإخطارات موبايل ل", + "Notification_RequireInteraction": "يتطلب التفاعل لرفض إعلام سطح المكتب", "Notifications": "الإشعارات", "Notifications_Max_Room_Members": "أقصى أعضاء الغرفة قبل تعطيل جميع الإخطارات رسالة", "Notifications_Max_Room_Members_Description": "الحد الأقصى لعدد الأعضاء في الغرفة عند تعطيل الإشعارات لجميع الرسائل. يمكن للمستخدمين لا يزال تغيير في إعداد غرفة لتلقي جميع الإخطارات على أساس فردي. (0 لتعطيل)", @@ -1931,6 +1994,7 @@ "Only_authorized_users_can_write_new_messages": "يمكن للمستخدمين المصرح لهم فقط كتابة رسائل جديدة", "Only_from_users": "فقط محتوى التقليم من هؤلاء المستخدمين (اتركه فارغًا لضبط محتوى كل شخص)", "Only_On_Desktop": "وضع المكتب -إرسال من خلال النقر على إدخال على سطح المكتب-", + "Only_works_with_chrome_version_greater_50": "يعمل فقط مع إصدارات متصفح كروم الإصدار اكبر من 50", "Only_you_can_see_this_message": "يمكنك أنت فقط رؤية هذه الرسالة", "Oops_page_not_found": "عفوًا ، لم يتم العثور على الصفحة", "Oops!": "عذرا", @@ -1979,6 +2043,7 @@ "People": "الناس", "Permalink": "الرابط الثابت", "Permissions": "التصريحات", + "Personal_Access_Tokens": "رموز الوصول الشخصية", "Phone": "الهاتف", "Pin": "ثبت", "Pin_Message": "تثبيث الرسالة", @@ -1997,7 +2062,7 @@ "PiwikAnalytics_siteId_Description": "هوية الموقع لاستخدامها لتحديد هذا الموقع. على سبيل المثال: 17", "PiwikAnalytics_url_Description": "عنوان الموقع حيث يتواجد Piwik، ومن المؤكد أن تشمل مائلة للتجريب. على سبيل المثال: //piwik.rocket.chat/", "Placeholder_for_email_or_username_login_field": "نائب عن البريد الإلكتروني أو تسجيل الدخول باسم المستخدم المجال", - "Placeholder_for_password_login_field": "نائب عن مجال تسجيل الدخول كلمة المرور", + "Placeholder_for_password_login_field": "عنصر نائب لحقل تسجيل الدخول بكلمة المرور", "Please_add_a_comment": "الرجاء إضافة تعليق", "Please_add_a_comment_to_close_the_room": "يرجى إضافة تعليق لإغلاق الغرفة", "Please_answer_survey": "يرجى ان نتوقف لحظة للرد على مسح سريع حول هذه الدردشة", @@ -2157,6 +2222,7 @@ "RetentionPolicy_AppliesToDMs": "ينطبق على الرسائل المباشرة", "RetentionPolicy_AppliesToGroups": "ينطبق على المجموعات الخاصة", "RetentionPolicy_Description": "تلقائيا prunes الرسائل القديمة عبر مثيل Rocket.Chat الخاص بك.", + "RetentionPolicy_DoNotPruneDiscussion": "لا تقم بتقليم رسائل المناقشة", "RetentionPolicy_Enabled": "مُفعّل", "RetentionPolicy_ExcludePinned": "استبعاد الرسائل المثبتة", "RetentionPolicy_FilesOnly": "فقط حذف الملفات", @@ -2168,6 +2234,8 @@ "RetentionPolicy_MaxAge_Groups": "الحد الأقصى لعمر الرسالة في المجموعات الخاصة", "RetentionPolicy_Precision": "الدقة الموقت", "RetentionPolicy_Precision_Description": "كم مرة يجب أن يتم تشغيل جهاز توقيت التقليم. يؤدي تعيين هذا إلى قيمة أكثر دقة إلى جعل القنوات ذات الموقتات السريعة للاحتفاظ تعمل بشكل أفضل ، ولكنها قد تكلف طاقة معالجة إضافية في المجتمعات الكبيرة.", + "RetentionPolicy_RoomWarning_FilesOnly": null, + "RetentionPolicy_RoomWarning_UnpinnedFilesOnly": null, "RetentionPolicyRoom_Enabled": "تقليم الرسائل القديمة تلقائيا", "RetentionPolicyRoom_ExcludePinned": "استبعاد الرسائل المثبتة", "RetentionPolicyRoom_FilesOnly": "ملفات التقليم فقط ، الاحتفاظ بالرسائل", @@ -2219,6 +2287,7 @@ "Same_As_Token_Sent_Via": "نفس \"الرمز المرسل عن طريق\"", "Same_Style_For_Mentions": "نفس النمط للإشارة", "SAML": "SAML", + "SAML_General": "عام", "SAML_Custom_Cert": "شهادة مخصصة", "SAML_Custom_Entry_point": "عرف نقطة إدخال", "SAML_Custom_Generate_Username": "توليد اسم المستخدم", @@ -2231,7 +2300,7 @@ "SAML_Custom_Private_Key": "محتويات المفتاح الخاص", "SAML_Custom_Provider": "مزود مخصص", "SAML_Custom_Public_Cert": "محتويات الشهادة العامة", - "SAML_Custom_user_data_fieldmap": "العضو بيانات خريطة الميدان", + "SAML_Custom_user_data_fieldmap": "خريطة حقل بيانات المستخدم", "SAML_Section_1_User_Interface": "واجهة المستخدم", "SAML_Section_4_Roles": "أدوار", "Saturday": "السبت", @@ -2276,7 +2345,7 @@ "Send": "إرسال", "Send_a_message": "إرسال رسالة", "Send_a_test_mail_to_my_user": "إرسال بريد إلكتروني إلى اختبار المستخدم الخاص بي", - "Send_a_test_push_to_my_user": "إرسال دفعة اختبار لالمستخدم الخاص بي", + "Send_a_test_push_to_my_user": "إرسال دفعة اختبار للمستخدم الخاص بي", "Send_confirmation_email": "إرسال رسالة تأكيد", "Send_data_into_RocketChat_in_realtime": "إرسال البيانات إلى Rocket.Chat في الوقت الحقيقي.", "Send_email": "إرسال البريد الإلكتروني", @@ -2327,6 +2396,7 @@ "Show_Avatars": "عرض الصور الرمزية", "Show_counter": "إظهار عداد", "Show_email_field": "إظهار حقل البريد الإلكتروني", + "Show_Message_In_Main_Thread": "إظهار رسائل الموضوع في الموضوع الرئيسي", "Show_more": "عرض المزيد", "Show_name_field": "إظهار حقل الاسم", "show_offline_users": "إظهار المستخدمين الموجودون حالياً -أونلاين-", @@ -2362,6 +2432,8 @@ "Slash_Gimme_Description": "يعرض (つ ◕_◕) つ قبل رسالتك", "Slash_LennyFace_Description": "يعرض (͡ ° ͜ʖ ͡ °) بعد رسالتك", "Slash_Shrug_Description": "يعرض ¯ \\ _ (ツ) _ / ¯ بعد رسالتك", + "Slash_Status_Description": "قم بتعيين رسالة الحالة الخاصة بك", + "Slash_Status_Params": "رسالة الحالة", "Slash_Tableflip_Description": "يعرض (╯ □ ° °) ╯( ┻━┻", "Slash_TableUnflip_Description": "يعرض ┬─┬ ノ (゜ - ゜ ノ)", "Slash_Topic_Description": "ضبط الموضوع", @@ -2406,6 +2478,10 @@ "Start_video_call": "بدء مكالمة فيديو", "Start_video_conference": "هل تريد بدء مؤتمر فيديو؟", "Start_with_s_for_user_or_s_for_channel_Eg_s_or_s": "ابدأ بـ %s للمستخدم أو %s للقناة. على سبيل المثال: %s أو %s", + "start-discussion": "ابدأ المناقشة", + "start-discussion_description": "طلب إذن لبدء المناقشة", + "start-discussion-other-user": "بدء المناقشة (مستخدم آخر)", + "start-discussion-other-user_description": "إذن ببدء مناقشة ، والذي يمنح الإذن للمستخدم بإنشاء مناقشة من رسالة أرسلها مستخدم آخر أيضًا", "Started_a_video_call": "بدء مكالمة فيديو", "Started_At": "كتبت في", "Statistics": "الإحصائيات", @@ -2416,7 +2492,7 @@ "Stats_Avg_Private_Group_Users": "متوسط مستخدمي المجموعات الخاصة", "Stats_Away_Users": "المستخدمين البعيدين", "Stats_Max_Room_Users": "أقصى عدد للمستخدمين في الغرف", - "Stats_Non_Active_Users": "المستخدمين الغير نشطين", + "Stats_Non_Active_Users": "المستخدمين الغير نشظين", "Stats_Offline_Users": "المستخدمون الغير متصلون", "Stats_Online_Users": "المستخدمون المتصلون", "Stats_Total_Channels": "مجموع القنوات", @@ -2431,8 +2507,13 @@ "Stats_Total_Rooms": "عدد الغرف", "Stats_Total_Users": "عدد الأعضاء", "Status": "الحالة", + "StatusMessage": "رسالة الحالة", + "StatusMessage_Change_Disabled": "قام مسؤول النظام بتعطيل تغيير رسائل الحالة", + "StatusMessage_Changed_Successfully": "تم تغيير رسالة الحالة بنجاح.", "StatusMessage_Placeholder": "ما الذي تفعله حالياً؟", + "StatusMessage_Too_Long": "يجب أن تكون رسالة الحالة أقصر من 120 حرفًا.", "Step": "خطوة", + "Stop_call": "أوقف المكالمة", "Stop_Recording": "إيقاف التسجيل", "Store_Last_Message": "تخزين آخر رسالة", "Store_Last_Message_Sent_per_Room": "تخزين آخر رسالة أرسلت في كل غرفة.", @@ -2475,6 +2556,7 @@ "The_emails_are_being_sent": "يتم إرسال رسائل البريد الإلكتروني.", "The_field_is_required": "هذا الحقل %s مطلوب.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "وتغيير الحجم صورة لا تعمل لأننا لا يمكن الكشف عن يماغيماغيك أو GraphicsMagick المثبتة على الخادم الخاص بك.", + "The_message_is_a_discussion_you_will_not_be_able_to_recover": "الرسالة هي مناقشة لن تتمكن من استعادة الرسائل!", "The_redirectUri_is_required": "مطلوب redirectUri", "The_server_will_restart_in_s_seconds": "سيتم إعادة تشغيل الخادم في %s ثانية", "The_setting_s_is_configured_to_s_and_you_are_accessing_from_s": "تم تكوين الإعداد %s إلى %s والتي يتم الوصول من %s!", @@ -2528,6 +2610,7 @@ "There_are_no_applications": "لم تتم إضافة أي تطبيقات أووث حتى الآن.", "There_are_no_applications_installed": "لا توجد حاليًا أي تطبيقات Rocket.Chat مثبتة.", "There_are_no_integrations": "لا توجد التكامل", + "There_are_no_personal_access_tokens_created_yet": "لا توجد رموز وصول شخصية تم إنشاؤها حتى الآن.", "There_are_no_users_in_this_role": "هناك مستخدمين في هذا الدور.", "This_conversation_is_already_closed": "تم إغلاق الدردشة للتو", "This_email_has_already_been_used_and_has_not_been_verified__Please_change_your_password": "وقد تم بالفعل استخدام هذا البريد الإلكتروني ولم يتم التحقق منها. الرجاء قم بتغيير كلمة المرور الخاصة بك.", @@ -2562,6 +2645,7 @@ "Tokens_Required_Input_Error": "الرموز المطبوعة غير الصالحة.", "Tokens_Required_Input_Placeholder": "أسماء أصول الرموز المميزة", "Topic": "الموضوع", + "Total_Discussions": "مجموع المناقشات", "Total_messages": "مجموع الرسائل", "TOTP Invalid [totp-invalid]": "الرمز أو كلمة المرور خاطئة", "totp-invalid": "الرمز أو كلمة المرور خاطئة", @@ -2580,10 +2664,12 @@ "Tuesday": "الثلاثاء", "Turn_OFF": "أطفأ", "Turn_ON": "شغله", - "Two-factor_authentication": "توثيق ذو عاملين", + "Two-factor_authentication": "المصادقة الثنائية عبر TOTP", "Two-factor_authentication_disabled": "تم تعطيل المصادقة الثنائية", + "Two-factor_authentication_email": "المصادقة الثنائية عبر البريد الإلكتروني", + "Two-factor_authentication_email_is_currently_disabled": "المصادقة الثنائية عبر البريد الإلكتروني معطلة حاليًا", "Two-factor_authentication_enabled": "تم تمكين المصادقة الثنائية", - "Two-factor_authentication_is_currently_disabled": "المصادقة بخطوتين غير مُفعّل حالياً", + "Two-factor_authentication_is_currently_disabled": "المصادقة الثنائية عبر TOTP معطلة حاليًا", "Two-factor_authentication_native_mobile_app_warning": "تحذير: بمجرد تمكين هذا، فإنك لن تكون قادرا على تسجيل الدخول على تطبيقات الجوال الأصلي (Rocket.Chat +) باستخدام كلمة المرور الخاصة بك حتى تنفذ 2FA.", "Type": "النوع", "Type_your_email": "اكتب بريدك الالكتروني", @@ -2730,6 +2816,7 @@ "Users_added": "تم إضافة المستخدمين", "Users_in_role": "المستخدمين في دور", "UTF8_Names_Slugify": "UTF8 أسماء Slugify", + "Videocall_enabled": "تم تفعيل الاتصال عبر الفيديو", "Validate_email_address": "التحقق من صحة البريد الإلكتروني", "Verification": "التحقق", "Verification_Description": "يمكنك استخدام العناصر النائبة التالية:
    • [verify_Url] لعنوان ورل للتحقق.
    • [نيم] و [فنيم] و [لنيم] للاسم الكامل للمستخدم أو الاسم الأول أو اسم العائلة، على التوالي.
    • [إمايل] للبريد الإلكتروني للمستخدم.
    • [Site_Name] و [Site_URL] لاسم التطبيق وعنوان ورل على التوالي.
    ", @@ -2745,7 +2832,6 @@ "Video_Conference": "مؤتمر عبر الفيديو", "Video_message": "رسالة فيديو", "Videocall_declined": "تم إلغاء اتصال الفيديو", - "Videocall_enabled": "تم تفعيل الاتصال عبر الفيديو", "View_All": "مشاهدة الكل", "View_Logs": "عرض سجلات", "View_mode": "اسلوب العرض", @@ -2771,7 +2857,7 @@ "view-livechat-rooms_description": "التصريح بعرض قنوات الدردشة المباشرة الأخرى", "view-logs": "عرض السجلات", "view-logs_description": "التصريخ بعرض سجلات الخادم", - "view-other-user-channels": "عرض قنوات المستخدمين الآخرين", + "view-other-user-channels": "عرض Channel المستخدمين الآخرين", "view-other-user-channels_description": "التصريح بعرض قنوات المستخدمين الآخرين", "view-outside-room": "عرض غرفة خارج", "view-p-room": "عرض الغرفة الخاصة", @@ -2870,4 +2956,4 @@ "Your_push_was_sent_to_s_devices": "وقد أرسلت دفعك إلى أجهزة٪ الصورة", "Your_server_link": "رابط الخادم الخاص بك", "Your_workspace_is_ready": "مساحة العمل الخاصة بك جاهزة لاستخدام 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/az.i18n.json b/packages/rocketchat-i18n/i18n/az.i18n.json index a9dbf99c5b1c..1c19714ba2f1 100644 --- a/packages/rocketchat-i18n/i18n/az.i18n.json +++ b/packages/rocketchat-i18n/i18n/az.i18n.json @@ -555,6 +555,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Yeni livechat otağı üçün davamlı səs bildirişləri", "Conversation": "Söhbət", "Conversation_closed": "Söhbət bağlandı: __comment__.", + "Conversation_finished": "Söhbət başa çatdı", "Conversation_finished_message": "Söhbət sona çatdı", "conversation_with_s": "%s ilə söhbət", "Convert_Ascii_Emojis": "ASCII'yi Emoji'ye çevirmək", @@ -2682,6 +2683,7 @@ "Users_added": "İstifadəçilər əlavə edildi", "Users_in_role": "İstifadəçi rolu", "UTF8_Names_Slugify": "UTF8 adları Slugify", + "Videocall_enabled": "Video Zəngləri Enabled", "Validate_email_address": "E-poçt ünvanı təsdiqləyin", "Verification": "Doğrulama", "Verification_Description": "Aşağıdakı yer tutuculardan istifadə edə bilərsiniz: Doğrulama URL'si üçün
    • [Verification_Url].
    • [ad], [fname], [lname] istifadəçinin tam adı, soyadı və soyadı üçün müvafiq olaraq. Istifadəçinin e-poçtu üçün
    • [email].
    • [Site_Name] və [Site_URL] üçün ərizə adı və URL sırasıyla.
    ", @@ -2696,7 +2698,6 @@ "Video_Conference": "Video Konfransı", "Video_message": "Video mesajı", "Videocall_declined": "Video Çağırıldı.", - "Videocall_enabled": "Video Zəngləri Enabled", "View_All": "Bütün üzvləri bax", "View_Logs": "Günlükləri bax", "View_mode": "Görünüş Modu", diff --git a/packages/rocketchat-i18n/i18n/be-BY.i18n.json b/packages/rocketchat-i18n/i18n/be-BY.i18n.json index 6bb78ec72842..32abdbd24c77 100644 --- a/packages/rocketchat-i18n/i18n/be-BY.i18n.json +++ b/packages/rocketchat-i18n/i18n/be-BY.i18n.json @@ -2696,6 +2696,7 @@ "Users_added": "Карыстальнікі, якія былі дададзеныя", "Users_in_role": "Карыстальнікі ў ролі", "UTF8_Names_Slugify": "UTF8 Імёны Slugify", + "Videocall_enabled": "відэазванок Enabled", "Validate_email_address": "Пацвердзіць адрас электроннай пошты", "Verification": "верыфікацыя", "Verification_Description": "Вы можаце выкарыстоўваць наступныя запаўняльнікі:
    • [VERIFICATION_URL] для праверкі URL-адрасы.
    • [імя], [імя_файла], [LNAME] поўнае імя карыстальніка, імя або прозвішча, адпаведна.
    • [пошта] для электроннай пошты карыстальніка.
    • [site_name] і [site_url] для імя прыкладання і URL адпаведна.
    ", @@ -2710,7 +2711,6 @@ "Video_Conference": "відэаканферэнцыя", "Video_message": "відэазварот", "Videocall_declined": "Відэазванок Адхілена.", - "Videocall_enabled": "відэазванок Enabled", "View_All": "Прагляд ўсіх удзельнікаў", "View_Logs": "прагляд часопісаў", "View_mode": "рэжым прагляду", diff --git a/packages/rocketchat-i18n/i18n/bg.i18n.json b/packages/rocketchat-i18n/i18n/bg.i18n.json index 60fe2a3d0dfd..b334611ec0f3 100644 --- a/packages/rocketchat-i18n/i18n/bg.i18n.json +++ b/packages/rocketchat-i18n/i18n/bg.i18n.json @@ -555,6 +555,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Непрекъснати звукови известия за нова стая livechat", "Conversation": "разговор", "Conversation_closed": "Разговорът е затворен: __comment__.", + "Conversation_finished": "Разговорът завърши", "Conversation_finished_message": "Готово съобщение за разговор", "conversation_with_s": "разговора с %s", "Convert_Ascii_Emojis": "Конвертиране на ASCII в Emoji", @@ -2680,6 +2681,7 @@ "Users_added": "Потребителите са добавени", "Users_in_role": "Потребителите в ролята", "UTF8_Names_Slugify": "Имената на UTF8", + "Videocall_enabled": "Видеообаждането е активирано", "Validate_email_address": "Потвърдете имейл адреса", "Verification": "Проверка", "Verification_Description": "Можете да използвате следните заместващи символи:
    • [Verification_Url] за URL адреса за потвърждение.
    • [име], [fname], [име] за пълното име, съответно име или фамилия на потребителя.
    • [имейл] за имейла на потребителя.
    • [Site_Name] и [Site_URL] съответно за името на приложението и URL адреса.
    ", @@ -2694,7 +2696,6 @@ "Video_Conference": "Видео конференция", "Video_message": "Видео съобщение", "Videocall_declined": "Отхвърлено видеообаждане.", - "Videocall_enabled": "Видеообаждането е активирано", "View_All": "Преглед на всички членове", "View_Logs": "Преглед на регистрационните файлове", "View_mode": "Режим на преглед", diff --git a/packages/rocketchat-i18n/i18n/bs.i18n.json b/packages/rocketchat-i18n/i18n/bs.i18n.json index f4ab3df626ab..7ab17eca8410 100644 --- a/packages/rocketchat-i18n/i18n/bs.i18n.json +++ b/packages/rocketchat-i18n/i18n/bs.i18n.json @@ -2677,6 +2677,7 @@ "Users_added": "Korisnici su dodani", "Users_in_role": "Korisnici u ulozi", "UTF8_Names_Slugify": "UTF8 Imena Slugify", + "Videocall_enabled": "Videopoziv omogućen", "Validate_email_address": "Validiraj email adresu", "Verification": "Verifikacija", "Verification_Description": "Možete upotrebljavati sljedeća rezervirana mjesta:
    • [Verification_Url] za URL za potvrdu.
    • [ime], [fname], [lname] za puni naziv, ime ili prezime korisnika.
    • [e-pošta] za e-poštu korisnika.
    • [Site_Name] i [Site_URL] za naziv aplikacije i URL.
    ", @@ -2691,7 +2692,6 @@ "Video_Conference": "Video Konferencija", "Video_message": "Video poruka", "Videocall_declined": "Videopoziv odbijen", - "Videocall_enabled": "Videopoziv omogućen", "View_All": "Prikaži Sve", "View_Logs": "Pogledaj izvještaje", "View_mode": "Pregled", diff --git a/packages/rocketchat-i18n/i18n/ca.i18n.json b/packages/rocketchat-i18n/i18n/ca.i18n.json index cfa53ad1050c..a1360d4734cf 100644 --- a/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -25,7 +25,7 @@ "Accept_with_no_online_agents": "Acceptar sense agents en línia", "Access_not_authorized": "Accés no autoritzat", "Access_Token_URL": "URL Access Token", - "access-mailer": "Accedir a la pantalla d'enviament", + "access-mailer": "Accedir a la pantalla de correu", "access-mailer_description": "Permís per enviar correu-e massiu a tots els usuaris", "access-permissions": "Accés a la pantalla de permisos", "access-permissions_description": "Modifica permisos per a diversos rols", @@ -62,12 +62,12 @@ "Accounts_BlockedDomainsList": "Llista de dominis bloquejats", "Accounts_BlockedDomainsList_Description": "Llista de dominis bloquejats separada per comes", "Accounts_BlockedUsernameList": "Llista de noms d'usuari bloquejats", - "Accounts_BlockedUsernameList_Description": "Llista separada per comes de noms d'usuari bloquejats (no distingeix majúscules/minúscules)", + "Accounts_BlockedUsernameList_Description": "Llista de noms d'usuaris bloquejats separada per comes (no distingeix majúscules i minúscules)", "Accounts_CustomFields_Description": "Ha de ser un JSON vàlid, on les claus són els noms dels camps que contenen un diccionari de configuració de camps. Exemple:
    {\n \"role\": {\n \"type\": \"select\",\n \"defaultValue\": \"student\",\n \"options\": [\"teacher\", \"student\"],\n \"required\": true,\n \"modifyRecordField\": {\n \"array\": true,\n \"field\": \"roles\"\n }\n },\n \"twitter\": {\n \"type\": \"text\",\n \"required\": true,\n \"minLength\": 2,\n \"maxLength\": 10\n }\n} ", "Accounts_CustomFieldsToShowInUserInfo": "Camps personalitzats a mostrar a l'informació d'usuari", "Accounts_Default_User_Preferences": "Preferències d'usuari per defecte", "Accounts_Default_User_Preferences_audioNotifications": "Alerta de notificacions d'àudio per defecte", - "Accounts_Default_User_Preferences_desktopNotifications": "Alerta per defecte per a les notificacions d'escriptori", + "Accounts_Default_User_Preferences_desktopNotifications": "Alerta de notificacions d'escriptori per defecte", "Accounts_Default_User_Preferences_pushNotifications": "Alerta per defecte notificacions mòbil", "Accounts_Default_User_Preferences_not_available": "No es van poder recuperar les preferències de l'usuari perquè l'usuari encara no les ha configurat", "Accounts_DefaultUsernamePrefixSuggestion": "Prefix suggerit per al nom d'usuari per defecte", @@ -114,13 +114,15 @@ "Accounts_OAuth_Custom_Merge_Users": "Uneix usuaris", "Accounts_OAuth_Custom_Name_Field": "Camp de nom", "Accounts_OAuth_Custom_Roles_Claim": "Nom del camp Rols / Grups", + "Accounts_OAuth_Custom_Roles_To_Sync": "Rols per sincronitzar", + "Accounts_OAuth_Custom_Roles_To_Sync_Description": "Rols d'OAuth per sincronitzar a l'inici de sessió i la creació de l'usuari (separats per comes).", "Accounts_OAuth_Custom_Scope": "Àmbit (scope)", "Accounts_OAuth_Custom_Secret": "Secret", "Accounts_OAuth_Custom_Show_Button_On_Login_Page": "Mostra botó a la pàgina d'inici de sessió", "Accounts_OAuth_Custom_Token_Path": "Ruta del token", "Accounts_OAuth_Custom_Token_Sent_Via": "Token enviat via", "Accounts_OAuth_Custom_Username_Field": "Camp de nom d'usuari", - "Accounts_OAuth_Drupal": "Activa inici de sessió de Drupal", + "Accounts_OAuth_Drupal": "Inici de sessió de Drupal habilitat", "Accounts_OAuth_Drupal_callback_url": "Redirect URI de Drupal oAuth2", "Accounts_OAuth_Drupal_id": "Client ID de Drupal oAuth2", "Accounts_OAuth_Drupal_secret": "Client Secret de Drupal oAuth2", @@ -161,7 +163,7 @@ "Accounts_OAuth_Nextcloud_URL": "URL del servidor de Nextcloud", "Accounts_OAuth_Proxy_host": "Host del servidor intermediari (proxy)", "Accounts_OAuth_Proxy_services": "Serveis del servidor intermediari (proxy)", - "Accounts_OAuth_Tokenpass": "Tokenpass Login", + "Accounts_OAuth_Tokenpass": "Inici de sessió amb Tokenpass", "Accounts_OAuth_Tokenpass_callback_url": "Tokenpass Callback URL", "Accounts_OAuth_Tokenpass_id": "Tokenpass Id", "Accounts_OAuth_Tokenpass_secret": "Tokenpass Secret", @@ -173,7 +175,7 @@ "Accounts_OAuth_Wordpress_authorize_path": "Ruta d'autorització", "Accounts_OAuth_Wordpress_callback_url": "URL de retorn (callback) de WordPress", "Accounts_OAuth_Wordpress_id": "WordPress ID", - "Accounts_OAuth_Wordpress_identity_path": "Ruta de la identitat", + "Accounts_OAuth_Wordpress_identity_path": "Ruta d'identitat", "Accounts_OAuth_Wordpress_identity_token_sent_via": "Token d'identitat enviat mitjançant", "Accounts_OAuth_Wordpress_scope": "Scope", "Accounts_OAuth_Wordpress_secret": "WordPress Secret", @@ -200,7 +202,7 @@ "Accounts_Password_Policy_MinLength": "Longitud mínima", "Accounts_Password_Policy_MinLength_Description": "Assegura que les contrasenyes han de tenir al menys aquesta quantitat de caràcters. Utilitza `-1` per desactivar.", "Accounts_PasswordReset": "Restablir contrasenya", - "Accounts_Registration_AuthenticationServices_Default_Roles": "Rols per defecte per als serveis d'autenticació", + "Accounts_Registration_AuthenticationServices_Default_Roles": "Rols predeterminats per a Serveis d'Autenticació", "Accounts_Registration_AuthenticationServices_Default_Roles_Description": "Rols per defecte (separats per comes) que s'assignaran als usuaris quan es registrin a través dels serveis d'autenticació", "Accounts_Registration_AuthenticationServices_Enabled": "Registre mitjançant serveis d'autenticació", "Accounts_Registration_Users_Default_Roles": "Rols predeterminats per als usuaris", @@ -214,31 +216,31 @@ "Accounts_RegistrationForm_LinkReplacementText": "Text de substitució de l'enllaç del formulari de registre", "Accounts_RegistrationForm_Public": "Públic", "Accounts_RegistrationForm_Secret_URL": "URL secret", - "Accounts_RegistrationForm_SecretURL": "URL secret del fomulari de registre", - "Accounts_RegistrationForm_SecretURL_Description": "Cal proporcionar una cadena de text aleatori que s'afegirà a l'URL de registre. Exemple: https://open.rocket.chat/register/[secret_hash]", + "Accounts_RegistrationForm_SecretURL": "URL Secret del Fomulari de Registre", + "Accounts_RegistrationForm_SecretURL_Description": "Heu de proporcionar una cadena de text aleatori que s'afegirà a la URL de registre. Exemple: https://open.rocket.chat/register/[secret_hash]", "Accounts_RequireNameForSignUp": "Requerir el nom per registrar-se", "Accounts_RequirePasswordConfirmation": "Requereix confirmació de la contrasenya", "Accounts_RoomAvatarExternalProviderUrl": "Room URL de l'proveïdor extern d'Avatar", "Accounts_RoomAvatarExternalProviderUrl_Description": "Exemple: `https://acme.com/api/v1/{roomId}`", "Accounts_SearchFields": "Camps a considerar a la cerca", - "Accounts_Send_Email_When_Activating": "Enviar un correu electrònic a l'usuari quan estigui activat", + "Accounts_Send_Email_When_Activating": "EEnviar correu electrònic a l'usuari quan l'usuari està activat", "Accounts_Send_Email_When_Deactivating": "Enviar un correu electrònic a l'usuari quan estigui desactivat", - "Accounts_Set_Email_Of_External_Accounts_as_Verified": "Establir el correu electrònic dels comptes externes com verificat", + "Accounts_Set_Email_Of_External_Accounts_as_Verified": "Establir el correu electrònic dels comptes externs com a verificat", "Accounts_Set_Email_Of_External_Accounts_as_Verified_Description": "Els correus electrònics de comptes creats des de serveis externs, com LDAP, OAuth, etc., es marcaran com verificats automàticament.", "Accounts_SetDefaultAvatar": "Avatar per defecte", "Accounts_SetDefaultAvatar_Description": "Prova de determinar l'avatar per defecte basant-se en el compte d'OAuth o bé Gravatar", - "Accounts_ShowFormLogin": "Mostrar el formulari d'inici de sessió per defecte", + "Accounts_ShowFormLogin": "Mostra formulari d'inici de sessió per defecte", "Accounts_TwoFactorAuthentication_By_TOTP_Enabled": "Habiliteu l'autenticació de dos factors a través de TOTP", "Accounts_TwoFactorAuthentication_By_TOTP_Enabled_Description": "Els usuaris poden configurar el seu autenticació de dos factors utilitzant qualsevol aplicació TOTP, com Google Authenticator o Authy.", "Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In": "Establir segon factor d'autenticació via correu electrònic per defecte per a nous usuaris", "Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In_Description": "els nous usuaris tindran activat per defecte el segon factor d'autenticació per correu electrònic. Podran desactivar-lo en la seva pàgina de perfil.", "Accounts_TwoFactorAuthentication_By_Email_Code_Expiration": "Temps de caducitat del codi enviat per correu electrònic en segons", "Accounts_TwoFactorAuthentication_By_Email_Enabled": "Habilitar segon factor d'autenticació via correu electrònic", - "Accounts_TwoFactorAuthentication_By_Email_Enabled_Description": "Els usuaris amb correu electrònic verificat i la opció habilitada a la seva pàgina de perfil rebran un correu electrònic amb un codi temporal per autoritzar certes accions com iniciar sessió, desar el perfil, etc.", + "Accounts_TwoFactorAuthentication_By_Email_Enabled_Description": "Els usuaris amb correu electrònic verificat i l'opció habilitada a la pàgina de perfil rebran un correu electrònic amb un codi temporal per autoritzar certes accions com iniciar sessió, desar el perfil, etc.", "Accounts_TwoFactorAuthentication_Enabled": "Habilitar l'autenticació de dos factors a través d'TOTP", "Accounts_TwoFactorAuthentication_Enabled_Description": "Els usuaris poden configurar el seu segon factor d'autenticació.
    fent servir qualsevol aplicació TOTP, com Google Authenticator o Authy", "Accounts_TwoFactorAuthentication_Enforce_Password_Fallback": "Aplicar suport de contrasenya", - "Accounts_TwoFactorAuthentication_Enforce_Password_Fallback_Description": "Els usuaris es veuran obligats a ingressar la contrasenya, per a accions importants, si no s'habilita cap altre mètode d'autenticació de segon factor per a aquest usuari i s'estableix una contrasenya per a ell.", + "Accounts_TwoFactorAuthentication_Enforce_Password_Fallback_Description": "Els usuaris es veuran obligats a ingressar la contrasenya per a accions importants si no s'habilita cap altre mètode d'autenticació de dos factors per a aquest usuari i s'estableix una contrasenya per a ell.", "Accounts_TwoFactorAuthentication_MaxDelta": "Delta màxima", "Accounts_TwoFactorAuthentication_MaxDelta_Description": "El Maximum Delta determina quants tokens són vàlids en un moment donat. Els tokens es generen cada 30 segons i són vàlids per a (30 * Maximum Delta) segons.
    Exemple: amb un Delta màxim establert a 10, cada token es pot utilitzar fins a 300 segons abans o després de la marca de temps. Això és útil quan el rellotge del client no està correctament sincronitzat amb el servidor.", "Accounts_TwoFactorAuthentication_RememberFor": "Recordar segon factor d'autenticació durant (segons)", @@ -248,14 +250,14 @@ "Accounts_UserAddedEmail_Default": "

    Benvingut a [Site_Name]

    Vés a [Site_URL] i prova la millor eina de programari lliure per a treball a distància disponible actualment!

    Pots entrar utilitzant el teu correu-e: [email] i contrasenya: [password]. És possible que et demanem canviar-la quan entris per primera vegada.", "Accounts_UserAddedEmail_Description": "És possible utilitzar els marcadors:

    • [name], [fname], [lname] per al nom complet de l'usuari, nom o cognom, respectivament.
    • [email] per a l'adreça de correu electrònic de l'usuari.
    • [password] per la contrasenya.
    • [Site_Name] i [Site_URL] pel nom del lloc web i de l'adreça URL, respectivament.
    ", "Accounts_UserAddedEmailSubject_Default": "Se t'ha afegit a [Site_Name]", - "Accounts_Verify_Email_For_External_Accounts": "Verificar el correu electrònic per als comptes externes", + "Accounts_Verify_Email_For_External_Accounts": "Verificar el correu electrònic per als comptes externs", "Action": "Acció", "Action_required": "Acció requerida", "Activate": "Activa", "Active": "Actiu", "Active_users": "Usuaris actius", "Activity": "Activitat", - "Add": "Afegeix", + "Add": "Afegir", "Add_agent": "Afegeix agent", "Add_custom_emoji": "Afageix emoji personalitzat", "Add_custom_oauth": "Afegeix OAuth personalitzat", @@ -266,7 +268,7 @@ "Add_Reaction": "Afegeix reacció", "Add_Role": "Afegeix rol", "Add_Sender_To_ReplyTo": "Afegir remitent per respondre", - "Add_user": "Afegeix usuari", + "Add_user": "Afegir usuari", "Add_User": "Afegeix usuari", "Add_users": "Afegeix usuaris", "Add_members": "Afegir membres", @@ -278,7 +280,7 @@ "add-user_description": "Permís per afegir nous usuaris al servidor via la pantalla d'usuaris", "add-user-to-any-c-room": "Afegir usuari a canal públic", "add-user-to-any-c-room_description": "Permís per afegir un usuari a qualsevol canal públic", - "add-user-to-any-p-room": "Afegir usuari a canal privat", + "add-user-to-any-p-room": "Afegir usuari a qualsevol Channel privat", "add-user-to-any-p-room_description": "Permís per afegir un usuari a qualsevol canal privat", "add-user-to-joined-room": "Afegir usuari a canal on unit", "add-user-to-joined-room_description": "Permís per afegir un usuari a un canal on està unit", @@ -287,14 +289,14 @@ "Adding_user": "Afegint usuari", "Additional_emails": "Correus electrònics addicionals", "Additional_Feedback": "Retroalimentació addicional", - "additional_integrations_Bots": "Si esteu buscant com integrar el vostre bot, no busqueu més, el nostre adaptador Hubot. https://github.com/RocketChat/hubot-rocketchat", + "additional_integrations_Bots": "Si esteu buscant com integrar el vostre propi bot, no busqueu més que el nostre adaptador Hubot. https://github.com/RocketChat/hubot-rocketchat", "additional_integrations_Zapier": "Està buscant integrar un altre programari i aplicacions amb Rocket.Chat però no té temps per fer-ho manualment? Llavors, suggerim utilitzar Zapier, que és totalment compatible. Llegiu més sobre això a la nostra documentació https://rocket.chat/docs/administrator-guides/integrations/zapier/using-zaps/", "Admin_disabled_encryption": "El seu administrador no va habilitar encriptació E2E", "Admin_Info": "Informació d'administrador", "Administration": "Administració", "Adult_images_are_not_allowed": "Les imatges per a adults no són permeses", "Aerospace_and_Defense": "Aeroespacial i Defensa", - "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "Després de l'autenticació OAuth2, els usuaris seran redirigits a aquest URL. Pots afegir un URL per línia.", + "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "Després de l'autenticació OAuth2, els usuaris seran redirigits a una URL en aquesta llista. Podeu afegir un URL per línia.", "Agent": "Agent", "Agent_added": "Agent afegit", "Agent_Info": "Informació de l'agent", @@ -352,7 +354,7 @@ "API_Drupal_URL": "Adreça URL del servidor de Drupal", "API_Drupal_URL_Description": "Exemple: https://domini.com (sense la barra final)", "API_Embed": "Incrusta (embed)", - "API_Embed_Description": "Activa o no les previsualitzacions d'enllaços quan un usuari publica l'enllaç a un web.", + "API_Embed_Description": "Si les vistes prèvies denllaços incrustats estan habilitades o no quan un usuari publica un enllaç a un lloc web.", "API_Embed_UserAgent": "Incrusta user agent de la consulta", "API_EmbedCacheExpirationDays": "Caducitat de la memòria cau de les incrustacions (en dies)", "API_EmbedDisabledFor": "Deshabilitar la incrustació per als usuaris", @@ -370,6 +372,8 @@ "API_Enable_Rate_Limiter_Dev": "Habilitar el limitador de freqüència en desenvolupament", "API_Enable_Rate_Limiter_Dev_Description": "Hauria limitar la quantitat de trucades als punts finals en l'entorn de desenvolupament?", "API_Enable_Rate_Limiter_Limit_Calls_Default": "Nombre de trucades per defecte al limitador de velocitat", + "Rate_Limiter_Limit_RegisterUser": "Trucades de números predeterminats al limitador de velocitat per registrar un usuari", + "Rate_Limiter_Limit_RegisterUser_Description": "Nombre de trucades predeterminades per a usuaris que registren punts finals (REST i API en temps real), permeses dins del rang de temps definit a la secció API Rate Limiter.", "API_Enable_Rate_Limiter_Limit_Calls_Default_Description": "Nombre de trucades predeterminades per a cada punt final de l'API REST, permeses dins de la franja de temps definit a continuació", "API_Enable_Rate_Limiter_Limit_Time_Default": "Límit de temps predeterminat per al limitador de freqüència (en ms)", "API_Enable_Rate_Limiter_Limit_Time_Default_Description": "Temps d'espera per defecte per limitar el nombre de trucades en cada punt final de l'API REST (en ms)", @@ -385,16 +389,17 @@ "API_Personal_Access_Tokens_Regenerate_Modal": "Si va perdre o va oblidar el vostre token, pot tornar a generar-lo, però recordeu que totes les aplicacions que fan servir aquest token s'han d'actualitzar", "API_Personal_Access_Tokens_Remove_Modal": "Esteu segur que voleu eliminar aquest Token d'accés personal?", "API_Personal_Access_Tokens_To_REST_API": "Tokens d'accés personal a l'API REST", + "API_Rate_Limiter": "Limitador de taxa API", "API_Shield_Types": "Tipus d'escut", "API_Shield_Types_Description": "Tipus d'escut que s'activaran, com a llista separada per comes. Triar entre `online`, `channel` o `*` per a tots", "API_Shield_user_require_auth": "Requereix autenticació per als escuts dels usuaris", "API_Token": "API Token", "API_Tokenpass_URL": "URL del servidor Tokenpass", "API_Tokenpass_URL_Description": "Exemple: https://domain.com (excloent la barra inclinada final)", - "API_Upper_Count_Limit": "Nombre màxim de registres", + "API_Upper_Count_Limit": "Quantitat màxima de registre", "API_Upper_Count_Limit_Description": "Quin és el nombre màxim de registres que la API REST pot retornar (si no és il·limitat)?", "API_Use_REST_For_DDP_Calls": "Utilitza REST en lloc de websocket per a les trucades de Meteor", - "API_User_Limit": "Límit d'usuaris per afegir tots els usuaris a canal", + "API_User_Limit": "Límit d'usuari per afegir tots els usuaris a Channel", "API_Wordpress_URL": "URL de WordPress", "api-bypass-rate-limit": "Límit de velocitat d'omissió per a la API REST", "api-bypass-rate-limit_description": "Permís per trucar a l'API sense limitació de tarifes", @@ -408,7 +413,7 @@ "App_status_auto_enabled": "Actiu", "App_status_constructed": "Construït", "App_status_disabled": "Inactiu", - "App_status_error_disabled": "Desactivat: error no capturat", + "App_status_error_disabled": "Deshabilitat: error desconegut", "App_status_initialized": "Inicialitzat", "App_status_invalid_license_disabled": "Desactivat: Llicència no vàlida", "App_status_invalid_settings_disabled": "Deshabilitat: la configuració necessària", @@ -452,16 +457,16 @@ "Apps_Interface_IPostRoomDeleted": "Esdeveniment que passa després que una sala és eliminada", "Apps_Interface_IPostRoomUserJoined": "Esdeveniment que passa després que un usuari s'uneixi a una sala (pública, privada)", "Apps_Interface_IPreMessageDeletePrevent": "Esdeveniment que passa després que un missatge és eliminat", - "Apps_Interface_IPreMessageSentExtend": "Esdeveniment que passa abans que un missatge és enviat", + "Apps_Interface_IPreMessageSentExtend": "Esdeveniment que passa abans que s'enviï un missatge", "Apps_Interface_IPreMessageSentModify": "Esdeveniment que passa abans que un missatge és enviat", - "Apps_Interface_IPreMessageSentPrevent": "Esdeveniment que passa abans que un missatge és enviat", + "Apps_Interface_IPreMessageSentPrevent": "Esdeveniment que passa abans que s'enviï un missatge", "Apps_Interface_IPreMessageUpdatedExtend": "Esdeveniment que passa abans que un missatge és actualitzat", - "Apps_Interface_IPreMessageUpdatedModify": "Esdeveniment que passa abans que un missatge és actualitzat", + "Apps_Interface_IPreMessageUpdatedModify": "Esdeveniment que passa abans que s'actualitzi un missatge", "Apps_Interface_IPreMessageUpdatedPrevent": "Esdeveniment que passa abans que un missatge és actualitzat", "Apps_Interface_IPreRoomCreateExtend": "Esdeveniment que passa abans que una sala és creada", "Apps_Interface_IPreRoomCreateModify": "Esdeveniment que passa abans que una sala és creada", - "Apps_Interface_IPreRoomCreatePrevent": "Esdeveniment que passa abans que una sala és creada", - "Apps_Interface_IPreRoomDeletePrevent": "Esdeveniment que passa abans que una sala és eliminada", + "Apps_Interface_IPreRoomCreatePrevent": "Esdeveniment que passa abans que es creï una sala", + "Apps_Interface_IPreRoomDeletePrevent": "Esdeveniment que passa abans que s'elimini una sala", "Apps_Interface_IPreRoomUserJoined": "Esdeveniment que passa abans que un usuari s'uneixi a una sala (pública, privada)", "Apps_License_Message_appId": "No s'ha emès la llicència per a aquesta aplicació.", "Apps_License_Message_bundle": "Llicència emesa per un paquet que no conté l'aplicació", @@ -476,8 +481,8 @@ "Apps_Logs_TTL_30days": "30 dies", "Apps_Logs_TTL_Alert": "Depenent de la mida de la col·lecció de registres, canviar aquesta configuració pot causar lentitud per alguns moments", "Apps_Marketplace_Deactivate_App_Prompt": "Vol realment desactivar aquesta aplicació?", - "Apps_Marketplace_Login_Required_Description": "Comprar aplicacions de l'Marketplace Rocket.Chat requereix registrar el teu entorn de treball i iniciar sessió.", - "Apps_Marketplace_Login_Required_Title": "Identificació requerida al Marketplace", + "Apps_Marketplace_Login_Required_Description": "Comprar aplicacions de Rocket.Chat Marketplace requereix registrar el vostre espai de treball i iniciar sessió.", + "Apps_Marketplace_Login_Required_Title": "Es requereix l'inici de sessió al Marketplace", "Apps_Marketplace_Modify_App_Subscription": "Modificar la Subscripció", "Apps_Marketplace_pricingPlan_monthly": "__price__ / mes", "Apps_Marketplace_pricingPlan_monthly_perUser": "__price__ / mes per usuari", @@ -488,7 +493,7 @@ "Apps_Marketplace_pricingPlan_yearly": "__price__ / any", "Apps_Marketplace_pricingPlan_yearly_perUser": "__price__ / any per usuari", "Apps_Marketplace_Uninstall_App_Prompt": "Vols realment desinstal·lar aquesta aplicació?", - "Apps_Marketplace_Uninstall_Subscribed_App_Anyway": "Desinstal·leu-la de totes maneres", + "Apps_Marketplace_Uninstall_Subscribed_App_Anyway": "Desinstal·lar de totes maneres", "Apps_Marketplace_Uninstall_Subscribed_App_Prompt": "Aquesta aplicació té una subscripció activa i la desinstal·lació no la cancel·la. Si voleu fer això, modifiqui la seva subscripció abans de desinstal·lar.", "Apps_Permissions_Review_Modal_Title": "Permisos necessaris", "Apps_Permissions_Review_Modal_Subtitle": "Aquesta aplicació vol accedir als permisos següents. Estàs d'acord?", @@ -550,7 +555,7 @@ "assign-roles": "Assignar rols", "assign-roles_description": "Permís per assignar rols a altres usuaris", "at": "a", - "At_least_one_added_token_is_required_by_the_user": "A l'almenys un dels tokens afegits és requerit per l'usuari", + "At_least_one_added_token_is_required_by_the_user": "L'usuari requereix com a mínim un token agregat", "AtlassianCrowd": "Atlassian Crowd", "Attachment_File_Uploaded": "Fitxer pujat", "Attribute_handling": "Tractament d'atributs", @@ -569,9 +574,9 @@ "Authorize": "Autoritzar", "Auto_Load_Images": "Carregar automàticament les imatges", "Auto_Selection": "Selecció automàtica", - "Auto_Translate": "Autotraducció", - "auto-translate": "Auto-traducció", - "auto-translate_description": "Permís per utilitzar l'eina d'auto-traducció", + "Auto_Translate": "Traducció automàtica", + "auto-translate": "Traducció automàtica", + "auto-translate_description": "Permís per fer servir l'eina de traducció automàtica", "AutoLinker": "Enllaç automàtic", "AutoLinker_Email": "Auto-enllaça correu-e", "AutoLinker_Phone": "Auto-enllaça telèfon", @@ -585,7 +590,7 @@ "Automatic_Translation": "Traducció automàtica", "AutoTranslate": "Autotraducció", "AutoTranslate_APIKey": "Clau API", - "AutoTranslate_Change_Language_Description": "Canviar l'idioma d'autotraducció no traduirà els missatges anteriors.", + "AutoTranslate_Change_Language_Description": "Canviar l'idioma de traducció automàtica no tradueix els missatges anteriors.", "AutoTranslate_DeepL": "DeepL", "AutoTranslate_Enabled": "Activa autotraducció", "AutoTranslate_Enabled_Description": "L'activació de la traducció automàtica permetrà a les persones amb el permís traduir automàticament el permís de traduir tots els missatges automàticament al seu idioma seleccionat. Es poden aplicar tarifes.", @@ -600,10 +605,10 @@ "Avatar_changed_successfully": "Avatar canviat correctament", "Avatar_URL": "URL de l'avatar", "Avatar_url_invalid_or_error": "L'adreça URL proporcionada és invàlida o no accessible. Si us plau, torneu-ho a intentar amb una altra.", - "Avg_chat_duration": "Durada mitjana del xat", + "Avg_chat_duration": "Mitjana de durada del xat", "Avg_first_response_time": "Mitjana del temps de la primera resposta", "Avg_of_abandoned_chats": "Mitjana de xats abandonats", - "Avg_of_available_service_time": "Mitjana del temps de servei disponible", + "Avg_of_available_service_time": "Mitjana del temps disponible del servei", "Avg_of_chat_duration_time": "Mitjana del temps de durada del xat", "Avg_of_service_time": "Mitjana del temps de servei", "Avg_of_waiting_time": "Mitjana del temps d’espera", @@ -646,10 +651,10 @@ "Block_Multiple_Failed_Logins_By_Ip": "Bloquejar els intents fallits d'accés per IP", "Block_Multiple_Failed_Logins_By_User": "Bloquejar els intents fallits d'accés per nom d'usuari", "Block_Multiple_Failed_Logins_Enable_Collect_Login_data_Description": "Emmagatzemar la IP i el nom d'usuari dels intents d'accés en una col·lecció a la base de dades", - "Block_Multiple_Failed_Logins_Enabled": "Habilitar la recopilació de dades d'inici de sessió", + "Block_Multiple_Failed_Logins_Enabled": "Habilitar recopilar dades d'inici de sessió", "Block_Multiple_Failed_Logins_Ip_Whitelist": "Llista blanca d'IP", "Block_Multiple_Failed_Logins_Ip_Whitelist_Description": "Llista separada per comes d'IP permeses", - "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes": "Temps per desbloquejar la IP (En minuts)", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes": "Temps per desbloquejar IP (en minuts)", "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes": "Temps per desbloquejar a l'usuari (en minuts)", "Block_Multiple_Failed_Logins_Notify_Failed": "Notificació d'intents fallits d'inici de sessió", "Block_Multiple_Failed_Logins_Notify_Failed_Channel": "Channel per enviar les notificacions", @@ -676,8 +681,8 @@ "Broadcasting_enabled": "Transmissió activada", "Broadcasting_media_server_url": "URL del servidor de mitjans de transmissió", "Browse_Files": "Cerca de fitxers", - "Browser_does_not_support_audio_element": "El seu navegador no suporta l'element d'àudio.", - "Browser_does_not_support_video_element": "El seu navegador no suporta l'element de vídeo.", + "Browser_does_not_support_audio_element": "El vostre navegador no és compatible amb l'element d'àudio.", + "Browser_does_not_support_video_element": "El vostre navegador no és compatible amb l'element de vídeo.", "Bugsnag_api_key": "Clau API Bugsnag", "Build_Environment": "Entorn de compilació", "bulk-register-user": "Crear usuaris de forma massiva", @@ -701,6 +706,9 @@ "By_author": "Per __author__", "cache_cleared": "Memòria cau esborrada", "Call": "Trucada", + "Call_declined": "Trucada rebutjada!", + "Call_provider": "Proveïdor de trucades", + "Call_Already_Ended": "Trucada ja finalitzada", "call-management": "Gestió de trucades", "call-management_description": "Permís per iniciar una reunió", "Caller": "Emissor", @@ -715,13 +723,13 @@ "Canned_Response_Sharing_Private_Description": "Només vostè i els administradors de livechat poden accedir a aquesta resposta predefinida", "Canned_Response_Sharing_Public_Description": "Qualsevol pot accedir a aquesta resposta predefinida", "Canned_Responses": "Resposta predefinida", - "Canned_Responses_Enable": "Activa resposta predefinida", + "Canned_Responses_Enable": "Habilitar resposta predefinida", "Create_your_First_Canned_Response": "Crea primera resposta predefinida", "Cannot_invite_users_to_direct_rooms": "No es pot convidar els usuaris a les sales directes", "Cannot_open_conversation_with_yourself": "No es pot obrir una conversa amb un mateix", "Cannot_share_your_location": "No es posible compartir la ubicació ...", "CAS_autoclose": "Finestra emergent de tancament automàtic de sessió", - "CAS_base_url": "Adreça URL SSO base", + "CAS_base_url": "URL base de SSO", "CAS_base_url_Description": "Adreça URL base del servei extern SSO. Ex: https://sso.example.undef/sso/", "CAS_button_color": "Color de fons del botó d'inici de sessió", "CAS_button_label_color": "Color de text del botó d'inici de sessió", @@ -739,7 +747,7 @@ "CAS_Sync_User_Data_FieldMap": "Mapa d'atributs", "CAS_Sync_User_Data_FieldMap_Description": "Utilitza aquesta entrada JSON per construir atributs interns (claus) des d'atributs externs (valors). Els valors d'atributs externs que continguin '%' seran interpolats com a cadenes de caràcters del valor.
    Per exemple, `{\"email\":\"%email%\", \"nom\":\"%nom%, %cognoms%\"}`

    El mapa d'atributs sempre s'interpolarà. La versió CAS 1.0 només permet l'atribut `username`. Els atributs interns disponibles són: username, name, email, rooms; rooms és una llista separada per comes de sales a unir-se durant la creació d'un usuari. Ex: {\"rooms\": \"%team%,%department%\"} uniria els nous usuaris CAS creats a les sales del seu equip (team) i departament (department).", "CAS_trust_username": "Confiar en el nom d'usuari de CAS", - "CAS_trust_username_description": "Quan està habilitat, Rocket.Chat confiarà en que qualsevol nom d'usuari de CAS pertany a el mateix usuari en Rocket.Chat.
    Això pot ser necessari si es canvia el nom d'un usuari en CAS, però també pot permetre que les persones prenguin el control de Rocket. Converseu els comptes canviant el nom dels seus propis usuaris CAS", + "CAS_trust_username_description": "Quan està habilitat, Rocket.Chat confiarà que qualsevol nom d'usuari de CAS pertany al mateix usuari a Rocket.Chat.
    Això pot ser necessari si es canvia el nom d'un usuari a CAS, però també pot permetre que les persones prenguin el control de Rocket. Xategeu els comptes canviant el nom dels vostres propis usuaris de CAS.", "CAS_version": "Versió CAS", "CAS_version_Description": "Només utilitzis una versió CAS suportada pel teu servei CAS SSO.", "Categories": "Categories", @@ -760,7 +768,7 @@ "Channel_created": "Canal `#%s` creat.", "Channel_doesnt_exist": "El canal `#%s` no existeix", "Channel_Export": "Exportar canal", - "Channel_name": "Nom del canal", + "Channel_name": "Nom del Channel", "Channel_Name_Placeholder": "Sisplau, introdueix el nom de canal...", "Channel_to_listen_on": "Canal on escoltar", "Channel_Unarchived": "El canal `#%s` s'ha desarxivat correctament.", @@ -770,7 +778,7 @@ "Channels_list": "Llista de canals públics", "Channel_what_is_this_channel_about": "De què tracta aquest canal?", "Chart": "Gràfic", - "Chat_button": "botó de xat", + "Chat_button": "Botó de xat", "Chat_close": "Tancar Xat", "Chat_closed": "Xat tancat", "Chat_closed_by_agent": "Xat tancat per l'agent", @@ -802,7 +810,7 @@ "Chatpal_Batch_Size_Description": "La mida del lot dels documents d'índex (en l'arrencada)", "Chatpal_channel_not_joined_yet": "Channel encara no s'ha unit", "Chatpal_create_key": "Crea una clau", - "Chatpal_created_key_successfully": "La clau de l'API s'ha creat correctament", + "Chatpal_created_key_successfully": "API-Key creada amb èxit", "Chatpal_Current_Room_Only": "Mateixa sala", "Chatpal_Default_Result_Type": "Tipus de resultat per defecte", "Chatpal_Default_Result_Type_Description": "Defineix quin tipus de resultat es mostra per resultat. Tot vol dir que es proporciona una descripció general de tots els tipus.", @@ -850,10 +858,10 @@ "Choose_the_alias_that_will_appear_before_the_username_in_messages": "Tria l'àlies que apareixerà abans del nom d'usuari als missatges.", "Choose_the_username_that_this_integration_will_post_as": "Tria el nom d'usuari amb el qual aquesta integració publicarà.", "Choose_users": "Trieu usuaris", - "Clean_Usernames": "Esborreu noms d'usuari", - "clean-channel-history": "Esborrar l'historial de canal", + "Clean_Usernames": "Esborrar noms d'usuari", + "clean-channel-history": "Esborrar l'historial de Channel", "clean-channel-history_description": "Permís per esborrar l'historial dels canals", - "clear": "Esborra", + "clear": "Esborrar", "Clear_all_unreads_question": "Esborrar tots els missatges no llegits?", "clear_cache_now": "Esborra la memòria cau ara", "Clear_filters": "Esborra els filtres", @@ -873,12 +881,12 @@ "Close": "Tanca", "Close_chat": "Tancar xat", "Close_room_description": "Esteu a punt de tancar aquest xat. Esteu segur que voleu continuar?", - "Close_to_seat_limit_banner_warning": "* Et queden [__seats__] seients * \nAquest espai de treball s'acosta al seu límit de seients. Una vegada que s'aconsegueix el límit, no es poden afegir nous membres. * [Sol·licitar més seients] (__url__) *", + "Close_to_seat_limit_banner_warning": "* Et queden [__seats__] llocs * \nAquest espai de treball s'acosta al seu límit de llocs. Una vegada que s'aconsegueix el límit, no es poden afegir nous membres. * [Sol·licitar més llocs] (__url__) *", "Close_to_seat_limit_warning": "No es poden crear nous membres una vegada que s'arriba al límit de seients.", "close-livechat-room": "Tancar Room de Livechat", "close-livechat-room_description": "Permís per tancar la sala d'LiveChat actual", "Close_menu": "Tanca el menú", - "close-others-livechat-room": "Tancar un altre sala de Livechat", + "close-others-livechat-room": "Tancar un altre Room de Livechat", "close-others-livechat-room_description": "Permís per tancar altres canals de LiveChat", "Closed": "Tancat", "Closed_At": "Tancat a les", @@ -894,7 +902,7 @@ "Cloud_Invalid_license": "Llicència no vàlida!", "Cloud_Apply_license": "Aplicar llicència", "Cloud_connectivity": "Connectivitat al núvol", - "Cloud_address_to_send_registration_to": "La direcció a la qual enviar el correu electrònic de registre en el núvol.", + "Cloud_address_to_send_registration_to": "La direcció a la qual enviar el correu electrònic de registre al núvol.", "Cloud_click_here": "Després de copiar el text, vés a [la consola Cloud (fés clic aquí)](__cloudConsoleUrl__).", "Cloud_console": "Consola en el núvol", "Cloud_error_code": "Codi: __errorCode__", @@ -906,7 +914,7 @@ "Cloud_register_error": "Hi ha hagut un error a l'intentar processar la seva sol·licitud. Torneu-ho de nou més tard.", "Cloud_Register_manually": "Registra't sense connexió", "Cloud_register_offline_finish_helper": "Després de completar el procés de registre en Cloud Console, hauria d'aparèixer un text. Enganxeu-lo aquí per finalitzar el registre.", - "Cloud_register_offline_helper": "Els espais de treball es poden registrar manualment si es restringeix l'accés a la xarxa o l'espai d'aire. Copieu el text a continuació i aneu a la nostra consola en el núvol per completar el procés.", + "Cloud_register_offline_helper": "Els espais de treball es poden registrar manualment si restringiu l'accés a la xarxa o l'espai d'aire. Copieu el text a continuació i aneu a la nostra consola al núvol per completar el procés.", "Cloud_register_success": "El seu espai de treball s'ha registrat correctament!", "Cloud_registration_pending_html": " Les notificacions en dispositius mòbils no funcionessin fins que el registre hagi finalitzat. Llegir més ", "Cloud_registration_pending_title": "El registre en el núvol encara està pendent", @@ -917,27 +925,27 @@ "Cloud_Service_Agree_PrivacyTerms": "Acords i termes de privacitat de el servei en el núvol", "Cloud_Service_Agree_PrivacyTerms_Description": "Estic d'acord amb els Termes i la Política de privacitat", "Cloud_Service_Agree_PrivacyTerms_Login_Disabled_Warning": "Ha d'acceptar els termes de privacitat del núvol (Assistent de configuració> Informació del núvol> Acord de termes de privacitat de el servei de núvol) per connectar al seu espai de treball en el núvol.", - "Cloud_status_page_description": "Si un Servei de Cloud en particular té problemes, pot verificar els problemes coneguts a la nostra pàgina d'estat a", + "Cloud_status_page_description": "Si un Servei de Cloud en particular té problemes, podeu verificar els problemes coneguts a la nostra pàgina d'estat a", "Cloud_token_instructions": "Per registrar el seu espai de treball, aneu a Cloud Console. Inicieu sessió o creeu un compte i feu clic a registrar-autogestionat. Enganxeu el testimoni proporcionat a continuació", "Cloud_troubleshooting": "Resolució de problemes", - "Cloud_update_email": "Actualitza el correu electrònic", + "Cloud_update_email": "Actualitzar el correu electrònic", "Cloud_what_is_it": "Què és això?", "Cloud_what_is_it_additional": "A més, podrà administrar llicències, facturació i suport des de la consola del núvol Rocket.Chat.", - "Cloud_what_is_it_description": "Rocket.Chat Cloud Connect li permet connectar el seu espai de treball Rocket.Chat autohospedado als serveis que oferim al nostre núvol.", + "Cloud_what_is_it_description": "Rocket.Chat Cloud Connect us permet connectar el vostre espai de treball Rocket.Chat autoallotjat als serveis que brindem al nostre núvol.", "Cloud_what_is_it_services_like": "Serveis com:", "Cloud_workspace_connected": "El seu espai de treball està connectat a Rocket.Chat Cloud. Inicia la sessió al compte de Rocket.Chat Cloud aquí li permetrà interactuar amb alguns serveis com al marketplace.", - "Cloud_workspace_connected_plus_account": "El vostre espai de treball està connectat al Rocket.Chat Cloud i hi ha un compte associat.", - "Cloud_workspace_connected_without_account": "El seu espai de treball ara està connectat a Rocket.Chat Cloud. Si ho desitja, podeu entrar en Rocket.Chat Cloud i associar el seu espai de treball amb el seu compte en el núvol.", - "Cloud_workspace_disconnect": "Si ja no desitja utilitzar els serveis en el núvol, pot desconnectar el seu espai de treball de Rocket.Chat Cloud.", - "Cloud_workspace_support": "Si té problemes amb un servei en el núvol, intenti sincronitzar primer. Si el problema persisteix, obriu un tiquet de suport en Cloud Console.", + "Cloud_workspace_connected_plus_account": "El vostre espai de treball ara està connectat a Rocket.Chat Cloud i un compte està associat.", + "Cloud_workspace_connected_without_account": "El vostre espai de treball ara està connectat a Rocket.Chat Cloud. Si ho desitgeu, podeu iniciar sessió a Rocket.Chat Cloud i associar el vostre espai de treball amb el vostre compte al núvol.", + "Cloud_workspace_disconnect": "Si ja no voleu utilitzar els serveis al núvol, podeu desconnectar el vostre espai de treball de Rocket.Chat Cloud.", + "Cloud_workspace_support": "Si teniu problemes amb un servei al núvol, intenteu sincronitzar primer. Si el problema persisteix, obriu un tiquet de suport al Cloud Console.", "Collaborative": "Col·laboratiu", "Collapse": "Caiguda", - "Collapse_Embedded_Media_By_Default": "Contraure mitjans integrats per defecte", + "Collapse_Embedded_Media_By_Default": "Contreure mitjans integrats per defecte", "color": "Color", "Color": "Color", "Colors": "Colors", "Commands": "Ordres", - "Comment_to_leave_on_closing_session": "Comentari a deixar en tancar la sessió", + "Comment_to_leave_on_closing_session": "Comentari per deixar a la sessió de tancament", "Comment": "Comentari", "Common_Access": "Accés comú", "Community": "Comunitat", @@ -979,7 +987,7 @@ "Contact_Info": "Informació de contacte", "Content": "Contingut", "Continue": "Continuar", - "Continuous_sound_notifications_for_new_livechat_room": "Notificacions de so continus per a la nova sala de LiveChat", + "Continuous_sound_notifications_for_new_livechat_room": "Notificacions de so contínues per a una nova sala Livechat", "Conversation": "Conversa", "Conversation_closed": "Conversa tancada: __comment__.", "Conversation_closing_tags": "Etiquetes de tancament de la conversa", @@ -1114,7 +1122,7 @@ "Country_Kenya": "Kenya", "Country_Kiribati": "Kiribati", "Country_Korea_Democratic_Peoples_Republic_of": "República Popular Democràtica de Corea", - "Country_Korea_Republic_of": "Corea, República de", + "Country_Korea_Republic_of": " República de Corea", "Country_Kuwait": "Kuwait", "Country_Kyrgyzstan": "Kirguizistan", "Country_Lao_Peoples_Democratic_Republic": "República Democràtica Popular Lao", @@ -1255,7 +1263,7 @@ "create-d_description": "Permís per a iniciar missatges directes", "create-invite-links": "Crear enllaços d'invitació", "create-invite-links_description": "Permís per crear enllaços d'invitació als canals", - "create-p": "Crear canals privats", + "create-p": "Crear Channel privats", "create-p_description": "Permís per crear canals privats", "create-personal-access-tokens": "Crear Tokens d'accés personal", "create-personal-access-tokens_description": "Permís per crear tokens d'accés personal", @@ -1265,7 +1273,7 @@ "Created_as": "Creat com", "Created_at": "Creat a", "Created_at_s_by_s": "Creat a %s per %s", - "Created_at_s_by_s_triggered_by_s": "Creat el %s per %s i disparat per %s", + "Created_at_s_by_s_triggered_by_s": "Creat el %s per %s i activat per %s", "Created_by": "Creat per", "CRM_Integration": "Integració CRM", "CROWD_Allow_Custom_Username": "Permet un nom d'usuari personalitzat a Rocket.Chat", @@ -1284,7 +1292,7 @@ "Custom_Emoji": "Emoticona personalitzada", "Custom_Emoji_Add": "Afegir nova emoticona", "Custom_Emoji_Added_Successfully": "Emoticona personalitzada afegida correctament", - "Custom_Emoji_Delete_Warning": "L'eliminació d'una emoticona no es pot desfer.", + "Custom_Emoji_Delete_Warning": "Eliminar un emoji no es pot desfer.", "Custom_Emoji_Error_Invalid_Emoji": "Emoticona invàlida", "Custom_Emoji_Error_Name_Or_Alias_Already_In_Use": "L'emoticona personalitzada o un dels seus àlies ja s'utilitza.", "Custom_Emoji_Has_Been_Deleted": "L'emoticona personalitzada s'ha eliminat.", @@ -1314,15 +1322,15 @@ "Custom_Sounds": "Sons personalitzats", "Custom_Status": "Estat personalitzat", "Custom_Translations": "Traduccions personalitzades", - "Custom_Translations_Description": "Ha de ser un objecte JSON vàlid on les claus són el codi de l'idioma i contenen un diccionari de clau i traducció. Exemple:
    {\n \"en\": {\n \"Channels\": \"Rooms\"\n },\n \"pt\":{\n \"Channels\": \"Salas\"\n }\n} ", + "Custom_Translations_Description": "Ha de ser un JSON vàlid on les claus siguin idiomes que continguin un diccionari de clau i traduccions. Exemple:
    {\n \"en\": {\n \"Channels\": \"Rooms\"\n },\n \"pt\":{\n \"Channels\": \"Salas\"\n }\n} ", "Custom_User_Status": "Estat d’usuari personalitzat", "Custom_User_Status_Add": "Afegir estat d'usuari personalitzat", "Custom_User_Status_Added_Successfully": "Es va afegir correctament l'estat d'usuari personalitzat", "Custom_User_Status_Delete_Warning": "L'eliminació d'un estat d'usuari personalitzat no es pot desfer.", - "Custom_User_Status_Edit": "Edita l'estat de l'usuari personalitzat", + "Custom_User_Status_Edit": "Editar l'estat de l'usuari personalitzat", "Custom_User_Status_Error_Invalid_User_Status": "Estat d’usuari no vàlid", "Custom_User_Status_Error_Name_Already_In_Use": "El nom d’estat d’usuari personalitzat ja està en ús.", - "Custom_User_Status_Has_Been_Deleted": "L'estat d'usuari personalitzat s'ha eliminat", + "Custom_User_Status_Has_Been_Deleted": "S'ha eliminat l'estat d'usuari personalitzat", "Custom_User_Status_Info": "Informació sobre l'estat d'usuari personalitzat", "Custom_User_Status_Updated_Successfully": "Estat d'usuari personalitzat actualitzat amb èxit", "Customer_without_registered_email": "El client no té una adreça de correu electrònic registrada", @@ -1340,9 +1348,10 @@ "Days": "Díes", "DB_Migration": "Migració de base de dades", "DB_Migration_Date": "Data de migració de la BD", + "DDP_Rate_Limit": "Límit de taxa DDP", "DDP_Rate_Limit_Connection_By_Method_Enabled": "Límit de Connexió per Mètode: habilitat", - "DDP_Rate_Limit_Connection_By_Method_Interval_Time": "Límit de Connexió per Mètode: interval de temps", - "DDP_Rate_Limit_Connection_By_Method_Requests_Allowed": "Límit de Connexió per Mètode: peticions permeses", + "DDP_Rate_Limit_Connection_By_Method_Interval_Time": "Límit per connexió per mètode: temps d'interval", + "DDP_Rate_Limit_Connection_By_Method_Requests_Allowed": "Límit per connexió per mètode: sol·licituds permeses", "DDP_Rate_Limit_Connection_Enabled": "Límit per Connexió: habilitat", "DDP_Rate_Limit_Connection_Interval_Time": "Límit per Connexió: interval de temps", "DDP_Rate_Limit_Connection_Requests_Allowed": "Límit per connexió: sol·licituds permeses", @@ -1355,7 +1364,7 @@ "DDP_Rate_Limit_User_Enabled": "Límit per usuari: habilitat", "DDP_Rate_Limit_User_Interval_Time": "Límit per usuari: interval de temps", "DDP_Rate_Limit_User_Requests_Allowed": "Límit per usuari: peticions permeses", - "Deactivate": "Desactiva", + "Deactivate": "Desactivar", "Decline": "Rebutja", "Decode_Key": "Clau de descodificació", "Default": "Per defecte", @@ -1369,10 +1378,10 @@ "Delete_Role_Warning": "Eliminar un rol l'eliminarà per sempre. Això no es pot desfer.", "Delete_Room_Warning": "Eliminar una sala de xat esborra tots els missatges que conté. Aquesta acció no es pot desfer.", "Delete_User_Warning": "Eliminar un usuari també esborra tots els missatges que ha enviat. Aquesta acció no es pot desfer.", - "Delete_User_Warning_Delete": "Eliminar un usuari també esborra tots els missatges que ha enviat. Aquesta acció no es pot desfer.", - "Delete_User_Warning_Keep": "S'eliminarà l'usuari, però els seus missatges romandran visibles. Això no es pot desfer.", + "Delete_User_Warning_Delete": "Eliminar un usuari causarà l'eliminació de tots els missatges creats per aquest usuari. Aquesta acció no es pot desfer.", + "Delete_User_Warning_Keep": "L'usuari serà eliminat, però els vostres missatges romandran visibles. Això no es pot desfer.", "Delete_User_Warning_Unlink": "Eliminar un usuari eliminarà el nom d'usuari de tots els seus missatges. Això no es pot desfer.", - "delete-c": "Esborrar canals públics", + "delete-c": "Esborrar Channel públics", "delete-c_description": "Permís per esborrar canals públics", "delete-d": "Esborrar missatges directes", "delete-d_description": "Permís per esborrar missatges directes", @@ -1390,20 +1399,20 @@ "Department_not_found": "Departament no trobat", "Department_removed": "Departament eliminat", "Departments": "Departaments", - "Deployment_ID": "Deployment ID", + "Deployment_ID": "ID de desplegament", "Deployment": "Desplegament", "Description": "Descripció", "Desktop": "Escriptori", "Desktop_Notification_Test": "Prova de notificació d'escriptori", "Desktop_Notifications": "Notificacions d'escriptori", - "Desktop_Notifications_Default_Alert": "Alerta per defecte per a les notificacions d'escriptori", + "Desktop_Notifications_Default_Alert": "Alerta predeterminada de notificacions d'escriptori", "Desktop_Notifications_Disabled": "Les notificacions d'escriptori han estat desactivades. Canvia les preferències del navegador si vols tornar a activar-les.", "Desktop_Notifications_Duration": "Durada de les notificacions d'escriptori", "Desktop_Notifications_Duration_Description": "Segons de mostra de les notificacions d'escriptori. Això pot afectar al centre de notificacions del macOS. Introduïu 0 per utilitzar la configuració del navegador per defecte i no afectar al centre de notificacions.", "Desktop_Notifications_Enabled": "Les notificacions d'escriptori estan activades", "Desktop_Notifications_Not_Enabled": "Les notificacions d'escriptori no estan habilitades", "Details": "Detalls", - "Different_Style_For_User_Mentions": "Estil diferent per les mencions d'usuari", + "Different_Style_For_User_Mentions": "Estil diferent per a les mencions de lusuari", "Direct_Message": "Missatge directe", "Direct_message_creation_description": "Estàs a punt de crear un xat amb múltiples usuaris. Afegeix als usuaris amb els que t'agradaria parlar, tots en el mateix lloc, fent servir missatges directes.", "Direct_message_someone": "Envia un missatge directe a algú", @@ -1414,9 +1423,9 @@ "Direct_Reply_Debug": "Debug resposta directa", "Direct_Reply_Debug_Description": "[Compte] Activa el mode de depuració mostraria el seu 'Contrasenya de text sense format' a la consola d'administració.", "Direct_Reply_Delete": "Eliminar correus electrònics", - "Direct_Reply_Delete_Description": "[Atenció!] Si s'activa aquesta opció, tots els missatges no llegits seran eliminats irrevocablement, fins i tot els que no són respostes directes. La bústia de correu electrònic configurat està llavors sempre buit i no pot ser processat en \"paral·lel\" per humans.", + "Direct_Reply_Delete_Description": "[Atenció!] Si aquesta opció està activada, tots els missatges no llegits s'eliminen irrevocablement, fins i tot aquells que no són respostes directes. La bústia de correu electrònic configurada està sempre buida i no pot ser processada en \"paral·lel\" per humans.", "Direct_Reply_Enable": "Activa resposta directa", - "Direct_Reply_Enable_Description": "[Atenció!] Si \"Resposta Directa\" està habilitat, Rocket.Chat controlarà la bústia de correu electrònic configurat. Tots els correus electrònics no llegits són recuperats, marcats com a llegits i processats. \"Resposta directa\" només ha de ser activada si la bústia utilitzat està destinat exclusivament per a l'accés de Rocket.Chat i no és llegit / processat \"en paral·lel\" per humans.", + "Direct_Reply_Enable_Description": "[Atenció!] Si la \"Resposta directa\" està habilitada, Rocket.Chat controlarà la bústia de correu electrònic configurada. Tots els correus electrònics no llegits es recuperen, es marquen com a llegits i es processen. La \"Resposta directa\" només s'ha d'activar si la bústia de correu utilitzada està destinada exclusivament per a l'accés de Rocket.Chat i no és llegit / processat \"en paral·lel\" per humans.", "Direct_Reply_Frequency": "Freqüència de comprovació de correu-e", "Direct_Reply_Frequency_Description": "(en minuts, per defecte/mínim 2)", "Direct_Reply_Host": "Host de resposta directa", @@ -1428,7 +1437,7 @@ "Direct_Reply_Separator_Description": "[Modifiqueu només si sabeu exactament què feu, consulteu documents]
    Separador entre base i etiqueta del correu electrònic", "Direct_Reply_Username": "Nom d'usuari", "Direct_Reply_Username_Description": "Utilitzeu el correu electrònic absolut, l'etiquetatge no està permès, se sobreescriurà", - "Directory": "directori", + "Directory": "Directori", "Disable": "Desactivar", "Disable_Facebook_integration": "Desactiva la integració de Facebook", "Disable_Notifications": "Desactiva notificacions", @@ -1440,9 +1449,9 @@ "Discard": "Descartar", "Disconnect": "Desconnectar", "Discussion": "Discussió", - "Discussion_description": "Ajudeu a mantenir una visió general del que està succeint! A l'crear una discussió, es crea un subcanal de què va seleccionar i tots dos es vinculen.", + "Discussion_description": "Ajudeu a mantenir una visió general del que està succeint! En crear una discussió, es crea un subcanal del qual vau seleccionar i tots dos es vinculen.", "Discussion_first_message_disabled_due_to_e2e": "Pot començar a enviar missatges xifrats d'extrem a extrem en aquesta discussió després de la seva creació.", - "Discussion_first_message_title": "el teu missatge", + "Discussion_first_message_title": "El teu missatge", "Discussion_name": "Nom de la discussió", "Discussion_start": "Començar una discussió", "Discussion_target_channel": "Canal o grup pare", @@ -1454,7 +1463,7 @@ "Display": "Visualització", "Display_avatars": "Mostra avatars", "Display_Avatars_Sidebar": "Mostra avatars a la barra lateral", - "Display_chat_permissions": "Mostra permisos de xat", + "Display_chat_permissions": "Mostrar permisos de xat", "Display_offline_form": "Mostra el formulari de fora de línia", "Display_setting_permissions": "Mostra permisos per canviar la configuració", "Display_unread_counter": "Mostra el nombre de missatges no llegits", @@ -1463,7 +1472,7 @@ "Do_not_display_unread_counter": "No mostreu cap comptador d'aquest canal", "Do_not_provide_this_code_to_anyone": "No comparteixi aquest codi amb ningú.", "Do_Nothing": "No fer res", - "Do_you_want_to_accept": "Ho accepteu?", + "Do_you_want_to_accept": "Vols acceptar?", "Do_you_want_to_change_to_s_question": "Canvia a %s?", "Document_Domain": "Domini del document", "Domain": "Domini", @@ -1472,7 +1481,7 @@ "Domains": "dominis", "Domains_allowed_to_embed_the_livechat_widget": "Llista separada per comes dels dominis on es permet incloure el xat en viu. Deixeu en blanc per permetre'ls tots.", "Dont_ask_me_again": "No em tornis a preguntar!", - "Dont_ask_me_again_list": "No tornis a demanar-me una altra vegada", + "Dont_ask_me_again_list": "No tornis a preguntar-me llista", "Download": "Descarrega", "Download_Info": "Descarregar informació", "Download_My_Data": "Descarregar les meves dades (HTML)", @@ -1494,21 +1503,21 @@ "E2E_enable": "Habilitar E2E", "E2E_disable": "Deshabilitat E2E", "E2E_Enable_alert": "Aquesta funció està actualment en versió beta. Informeu d’errors a github.com/RocketChat/Rocket.Chat/issues i tingueu en compte:
    - Les operacions de cerca no trobaran els missatges xifrats de les sales xifrades.
    - És possible que les aplicacions mòbils no admetin els missatges encriptats. (ho estan implementant).
    - És possible que els robots no puguin veure els missatges xifrats fins que no implementin el suport.
    - Les càrregues no es xifraran en aquesta versió.", - "E2E_Enable_description": "Habiliteu l'opció per crear grups encriptades i poder canviar grups i missatges directes per ser encriptats", + "E2E_Enable_description": "Habiliteu l'opció per crear grups encriptats i poder canviar grups i missatges directes per ser encriptats", "E2E_Enabled": "E2E activat", "E2E_Enabled_Default_DirectRooms": "Habilitar l'encriptació per a les Rooms directes per defecte", "E2E_Enabled_Default_PrivateRooms": "Habilitar l'encriptació per a les Rooms privades per defecte", "E2E_Encryption_Password_Change": "Canviar la contrasenya de xifrat", - "E2E_Encryption_Password_Explanation": "Ara pot crear grups privats codificats i missatges directes. També pot canviar els grups privats o missatges directes existents a xifrats.

    1 /> Això és xifrat d'extrem a extrem, així que la clau per xifrar / desxifrar els teus missatges no es guardarà al servidor. Per aquesta raó necessites guardar la contrasenya en un lloc segur. Se't demanarà que la introdueixis en altres dispositius en els que vulguis utilitzar l'encriptació de E2E.", + "E2E_Encryption_Password_Explanation": "Ara podeu crear grups privats xifrats i missatges directes. També podeu canviar els grups privats o DM existents a xifrats.

    Aquest és un xifratge d'extrem a extrem, de manera que la clau per codificar / descodificar els seus missatges no es desarà al servidor. Per això, heu de desar la contrasenya en un lloc segur. Se us demanarà que l'introduïu en altres dispositius on vulgueu utilitzar el xifratge e2e.", "E2E_key_reset_email": "Notificació de reinici de clau E2E", "E2E_password_request_text": "Per accedir als seus grups privats xifrats i als missatges directes, introdueixi la contrasenya de xifrat.
    Necessites introduir aquesta contrasenya per xifrar / desxifrar els teus missatges en cada client que utilitzis, ja que la clau no s'emmagatzema en el servidor.", - "E2E_password_reveal_text": "Ara podeu crear grups privats xifrats i missatges directes. També podeu canviar els grups privats o DM existents a xifrats. [Html0]

    Això fa que el xifratge finalitzi, de manera que la clau per codificar / descodificar els vostres missatges no es desarà al servidor. Per aquest motiu, heu de desar aquesta contrasenya en un lloc segur. Haureu d’introduir-lo en altres dispositius en què vulgueu utilitzar el xifratge e2e. Més informació aquí!
    La seva contrassenya es:


    Aquesta és una contrasenya generada automàticament; podeu configurar una nova contrasenya per a la vostra clau de xifrat en qualsevol moment des de qualsevol navegador que hàgiu introduït la contrasenya existent.
    Aquesta contrasenya només s’emmagatzema en aquest del navegador fins que deseu la contrasenya i desactiveu aquest missatge.", - "E2E_Reset_Email_Content": "S'ha desconnectat automàticament. Quan torni a iniciar sessió, Rocket.Chat generarà una nova clau i restaurarà el seu accés a qualsevol sala xifrada que tingui un o més membres en línia. A causa de la naturalesa de l'xifrat E2E, Rocket.Chat no podrà restaurar l'accés a cap sala xifrada que no tingui membres en línia.", - "E2E_Reset_Key_Explanation": "Aquesta opció eliminarà la seva clau I2I actual i tancarà la sessió.
    Quan torni a iniciar sessió, Rocket.Chat li generarà una nova clau i restablirà el seu accés a qualsevol sala xifrada que tingui un o més membres en línia.
    A causa de la naturalesa de l'xifrat E2E, Rocket.Chat no podrà restaurar l'accés a cap sala xifrada que no tingui membres en línia.", + "E2E_password_reveal_text": "Ara podeu crear grups privats xifrats i missatges directes. També podeu canviar els grups privats o DM existents a xifrats.

    Aquest és un xifratge d'extrem a extrem, de manera que la clau per codificar / descodificar els seus missatges no es desarà al servidor. Per això, heu de desar aquesta contrasenya en un lloc segur. Se us demanarà que l'introduïu en altres dispositius on vulgueu utilitzar el xifratge e2e. Obtingueu més informació aquí!

    La vostra contrasenya és:% s

    Aquesta és una contrasenya generada automàticament, podeu configurar una nova contrasenya per a la vostra clau de xifrat en qualsevol moment des de qualsevol navegador que hagi introduït la contrasenya existent.
    Aquesta contrasenya només s'emmagatzema en aquest navegador fins que la deseu i descarteu aquest missatge.", + "E2E_Reset_Email_Content": "S'ha desconnectat automàticament. Quan torneu a iniciar sessió, Rocket.Chat generarà una nova clau i restaurarà el vostre accés a qualsevol sala xifrada que tingui un o més membres en línia. A causa de la naturalesa del xifratge E2E, Rocket.Chat no podrà restaurar l'accés a cap sala xifrada que no tingui membres en línia.", + "E2E_Reset_Key_Explanation": "Aquesta opció eliminarà la clau E2E actual i tancarà la sessió.
    Quan torneu a iniciar sessió, Rocket.Chat us generarà una nova clau i restaurarà l'accés a qualsevol sala xifrada que tingui un o més membres en línia.
    A causa de la naturalesa del xifratge E2E, Rocket.Chat no podrà restaurar l'accés a cap sala xifrada que no tingui membres en línia.", "E2E_Reset_Other_Key_Warning": "Restablir la clau E2E actual tancarà la sessió de l'usuari. Quan l'usuari torna a iniciar sessió, Rocket.Chat generarà una nova clau i restaurarà l'accés de l'usuari a qualsevol sala xifrada que tingui un o més membres en línia. A causa de la naturalesa de l'xifrat E2E, Rocket.Chat no podrà restaurar l'accés a cap sala xifrada que no tingui membres en línia.", "ECDH_Enabled": "Habiliteu el xifrat de segona capa per al transport de dades", "Edit": "Edita", - "Edit_Business_Hour": "Edita l'horari comercial", + "Edit_Business_Hour": "Edita l'horari d'oficina ", "Edit_Canned_Response": "Edita resposta predefinida", "Edit_Canned_Responses": "Edita respostes emmagatzemades", "Edit_Custom_Field": "Edita camp personalitzat", @@ -1518,7 +1527,7 @@ "Edit_Priority": "Edita la prioritat", "Edit_Status": "Edita l'estat", "Edit_Tag": "Edita l’etiqueta", - "Edit_Trigger": "Edita disparador", + "Edit_Trigger": "Edita activador", "Edit_Unit": "Edita la unitat", "Edit_User": "Edita l'usuari", "edit-livechat-room-customfields": "Edita camps personalitzats de Livechat Room", @@ -1532,14 +1541,14 @@ "edit-other-user-e2ee": "Edita el xifrat E2E d'un altre usuari", "edit-other-user-e2ee_description": "Permís per modificar el xifrat I2I d'un altre usuari.", "edit-other-user-info": "Editar la informació d'un altre usuari", - "edit-other-user-info_description": "Permís per canviar el nom, el nom d'usuari o l'adreça de correu-e d'altres usuaris", + "edit-other-user-info_description": "Permís per canviar el nom, el nom d'usuari o l'adreça electrònica d'un altre usuari.", "edit-other-user-password": "Editar la contrasenya d'un altre usuari", "edit-other-user-password_description": "Permís per modificar la contrasenya d'altres usuaris. Requereix el permís edit-other-user-info.", "edit-other-user-totp": "Edita el doble factor TOTP d'un altre usuari", "edit-other-user-totp_description": "Permís per editar el TOTP de dos factors d'un altre usuari", "edit-privileged-setting": "Edita la configuració privilegiada", "edit-privileged-setting_description": "Permís per editar la configuració", - "edit-room": "Editar sala", + "edit-room": "Editar Room", "edit-room_description": "Permís per editar el nom d'una sala, el tema, el tipus (privada o pública) o l'estat (actiu o arxivat)", "edit-room-avatar": "Edita Room Avatar", "edit-room-avatar_description": "Permís per editar l'avatar d'una sala.", @@ -1558,10 +1567,10 @@ "Email_already_exists": "L'adreça de correu electrònic ja existeix", "Email_body": "Cos del missatge", "Email_Change_Disabled": "El canvi de correu electrònic està desactivat", - "Email_Changed_Description": "Podeu utilitzar els següents marcadors de posició:
  • [e] per al correu electrònic de l'usuari.
  • [Site_Name] i [ Site_URL] per al nom de l'aplicació i la URL respectivament.
    • ", + "Email_Changed_Description": "Podeu utilitzar les adreces d'interès següents:
      • [email] per al correu electrònic de l'usuari.
      • [Site_Name] i [Site_URL] per al nom de l'aplicació i l'URL respectivament.
      ", "Email_Changed_Email_Subject": "[Site_name] - La direcció de correu electrònic ha estat modificada", "Email_changed_section": "Adreça de correu electrònic modificada", - "Email_Footer_Description": "És possible utilitzar els marcadors:
      • [Site_Name] i [Site_URL] pel nom del lloc web i de l'adreça URL, respectivament.
      ", + "Email_Footer_Description": "Podeu utilitzar les adreces d'interès següents:
      • [Site_Name] i [Site_URL] per al nom de l'aplicació i l'URL respectivament.
      ", "Email_from": "De", "Email_Header_Description": "És possible utilitzar els marcadors:
      • [Site_Name] i [Site_URL] pel nom del lloc web i de l'adreça URL, respectivament.
      ", "Email_Inbox": "Safata d'entrada de correu electrònic", @@ -1588,7 +1597,7 @@ "Enable_message_parser_early_adoption": "Habilitar l'analitzador de missatges", "Enable_message_parser_early_adoption_alert": "Aquesta és una característica experimental i seguirà sent-ho al menys fins a la versió 3.19.0, aquesta opció és per ajudar-nos amb proves i casos extrems. Tan aviat com no trobem més problemes, eliminarem aquesta opció i migrarem a la nova solució.", "See_on_Engagement_Dashboard": "Consulteu el tauler de participació", - "Enable": "Activa", + "Enable": "Habilitar", "Enable_Auto_Away": "Activa Auto Away", "Enable_CSP": "Habilitar política de seguretat de contingut", "Enable_CSP_Description": "No desactiveu aquesta opció a menys que tingui una compilació personalitzada i tingui problemes a causa de scripts en línia", @@ -1604,11 +1613,13 @@ "Encrypted": "Xifrat", "Encrypted_channel_Description": "Canal xifrat d'extrem a extrem. La cerca no funcionarà amb canals xifrats i és possible que les notificacions no mostrin el contingut dels missatges.", "Encrypted_message": "Missatge xifrat", - "Encrypted_setting_changed_successfully": "La configuració encriptada es va modificar correctament", + "Encrypted_setting_changed_successfully": "La configuració encriptada es va canviar correctament", "Encrypted_not_available": "No disponible per a Channels públics", - "Encryption_key_saved_successfully": "La seva clau d'encriptació es va guardar correctament", - "EncryptionKey_Change_Disabled": "No pot establir una contrasenya per a la clau de xifrat perquè la seva clau privada no està present en aquest client. Per establir una nova contrasenya, necessita carregar la seva clau privada amb el vostre contrasenya existent o utilitzar un client on la clau ja estigui carregada.", + "Encryption_key_saved_successfully": "la vostra clau de xifrat es va guardar correctament.", + "EncryptionKey_Change_Disabled": "No podeu establir una contrasenya per a la vostra clau de xifratge perquè la vostra clau privada no és present en aquest client. Per establir una contrasenya nova, heu de carregar la vostra clau privada utilitzant la vostra contrasenya existent o utilitzar un client on la clau ja estigui carregada.", "End": "Fi", + "End_call": "Finalitzar trucada", + "Expand_view": "Expandir vista", "End_OTR": "Finalitza OTR", "Engagement_Dashboard": "Tauler de participació", "Enter": "Entra", @@ -1630,7 +1641,7 @@ "Enter_your_E2E_password": "Introduïu la vostra contrasenya E2E", "Enterprise": "Empresa", "Enterprise_License": "Llicència d’empresa", - "Enterprise_License_Description": "Si el teu espai de treball està registrat i disposa d'una llicència proporcionada per Rocket.Chat Cloud no cal actualitzar aquesta llicència..", + "Enterprise_License_Description": "Si el vostre espai de treball està registrat i la llicència la proporciona Rocket.Chat Cloud, no cal que actualitzeu manualment la llicència aquí.", "Entertainment": "Entreteniment", "Error": "Error", "Error_404": "Error: 404", @@ -1656,14 +1667,14 @@ "error-cannot-delete-app-user": "No es permet esborrar l'usuari de l'aplicació, desinstal l'aplicació corresponent per eliminar-la.", "error-cant-invite-for-direct-room": "No es pot convidar a l'usuari a sales directes", "error-channels-setdefault-is-same": "La configuració predeterminada de canal és la mateixa a la qual es canviaria..", - "error-channels-setdefault-missing-default-param": "El bodyParam 'default' és obligatori", + "error-channels-setdefault-missing-default-param": "El bodyParam 'predeterminat' és obligatori", "error-could-not-change-email": "No s'ha pogut canviar el correu electrònic", "error-could-not-change-name": "No s'ha pogut canviar el nom", "error-could-not-change-username": "No s'ha pogut canviar el nom d'usuari", "error-custom-field-name-already-exists": "El nom de camp personalitzat ja existeix", "error-delete-protected-role": "No es pot eliminar un rol protegit", "error-department-not-found": "Departament no trobat", - "error-direct-message-file-upload-not-allowed": "Compartició d'arxius no permesa als missatges directes", + "error-direct-message-file-upload-not-allowed": "No es permet compartir fitxers en missatges directes", "error-duplicate-channel-name": "Un canal amb el nom '__channel_name__' ja existeix", "error-edit-permissions-not-allowed": "No es permet editar permisos", "error-email-domain-blacklisted": "El domini de l'adreça electrònica és a la llista negra", @@ -1672,13 +1683,13 @@ "error-field-unavailable": "__field__ ja s'utilitza :(", "error-file-too-large": "L'arxiu és massa gran", "error-forwarding-chat": "S'ha produït un error a l'enviar el xat. Torna-ho a intentar més tard.", - "error-forwarding-chat-same-department": "El departament seleccionat i l'actual departament de la sala són els mateixos", + "error-forwarding-chat-same-department": "El departament seleccionat i el departament sala actual són el mateix", "error-forwarding-department-target-not-allowed": "No es permet el reenviament a el departament de destinació.", "error-guests-cant-have-other-roles": "Els usuaris visitants no poden tenir cap altre rol.", "error-import-file-extract-error": "No s'ha pogut extreure el fitxer d'importació.", "error-import-file-is-empty": "L'arxiu importat sembla estar buit.", "error-import-file-missing": "No s'ha trobat el fitxer a importar a la ruta especificada.", - "error-importer-not-defined": "L'importador no s'ha definit correctament, no es troba la classe d'importació.", + "error-importer-not-defined": "L'importador no es va definir correctament, manca la classe Import.", "error-input-is-not-a-valid-field": "__input__ no és un __field__ vàlid", "error-invalid-account": "Compte no vàlid", "error-invalid-actionlink": "Enllaç d'acció (action link) invàlid", @@ -1744,9 +1755,9 @@ "error-password-policy-not-met-minLength": "La contrasenya no compleix amb la política del servidor de durada mínima (la contrasenya és massa curta).", "error-password-policy-not-met-oneLowercase": "La contrasenya no compleix amb la política del servidor d'almenys un caràcter en minúscules", "error-password-policy-not-met-oneNumber": "La contrasenya no compleix la política del servidor d'almenys un caràcter numèric", - "error-password-policy-not-met-oneSpecial": "La contrasenya no compleix amb la política del servidor d'almenys un caràcter especial", + "error-password-policy-not-met-oneSpecial": "La contrasenya no compleix la política del servidor d'almenys un caràcter especial", "error-password-policy-not-met-oneUppercase": "La contrasenya no compleix amb la política del servidor d'almenys un caràcter en majúscula", - "error-password-policy-not-met-repeatingCharacters": "La contrasenya no compleix la política del servidor de caràcters repetits prohibits (teniu massa dels mateixos caràcters al costat de l'altre)", + "error-password-policy-not-met-repeatingCharacters": "La contrasenya no compleix la política del servidor de caràcters repetits prohibits (té massa caràcters iguals un al costat de l'altre)", "error-password-same-as-current": "Va ingressar la mateixa contrasenya que la contrasenya actual", "error-personal-access-tokens-are-current-disabled": "Les claus d'accés personals estan actualment desactivades", "error-pinning-message": "No s'ha pogut fixar el missatge", @@ -1783,7 +1794,7 @@ "error-you-are-last-owner": "Ets l'últim propietari. Si us plau, estableix un nou propietari abans de sortir de la sala.", "Errors_and_Warnings": "Errors i advertències", "Esc_to": "Esc a", - "Estimated_due_time": "Temps previst previst (temps en minuts)", + "Estimated_due_time": "Temps estimat despera (temps en minuts)", "Estimated_due_time_in_minutes": "Temps de venciment previst (temps en minuts)", "Event_Trigger": "Disparador d'esdeveniments", "Event_Trigger_Description": "Selecciona quin tipus d'esdeveniment desencadenarà aquesta integració WebHook de sortida", @@ -1791,7 +1802,7 @@ "every_10_seconds": "Una vegada cada 10 segons", "every_30_minutes": "Cada 30 minuts", "every_day": "Una vegada al dia", - "every_hour": "Cada hora", + "every_hour": "Un cop cada hora", "every_minute": "Una vegada cada minut", "every_second": "Una vegada per segon", "every_six_hours": "Cada 6 hores", @@ -1809,9 +1820,9 @@ "Experimental_Feature_Alert": "Aquesta és una funció experimental! Recordeu que pot canviar, trencar-se o fins i tot eliminar-se en el futur sense previ avís.", "Expiration": "Caducitat", "Expiration_(Days)": "Caducitat (dies)", - "Export_as_file": "Exporta com a fitxer", - "Export_Messages": "Exporta missatges", - "Export_My_Data": "Exportar les meves dades (jSOM)", + "Export_as_file": "Exporta com axiu", + "Export_Messages": "Exportar missatges", + "Export_My_Data": "Exportar les meves dades (jSON)", "expression": "Expressió", "Extended": "Ampliat", "External": "Extern", @@ -1830,13 +1841,15 @@ "Failed_To_Load_Import_Data": "Error al carregar importació de dades", "Failed_To_Load_Import_History": "Error a l'carregar importació de històric", "Failed_To_Load_Import_Operation": "Error al cargar operación de importación", - "Failed_To_Start_Import": "Error al iniciar operación de importación", - "Failed_to_validate_invite_token": "Error al validar token de invitació", + "Failed_To_Start_Import": "Error al iniciar l'operació d'importació", + "Failed_to_validate_invite_token": "Error al validar el token d'invitació", "False": "No", "Favorite": "Preferit", "Favorite_Rooms": "Habilita sales favorites", "Favorites": "Favorits", + "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "Aquesta funció depèn del proveïdor de trucades seleccionat anteriorment que s'habilitarà des de la configuració d'administració.", "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "Aquesta funció depèn de \"Enviar l'historial de navegació del visitant com a missatge\" per estar habilitat.", + "Feature_Limiting": "Limitació de funcions", "Features": "Característiques", "Features_Enabled": "Funcionalitats habilitades", "Feature_Disabled": "Característica deshabilitada", @@ -1881,10 +1894,10 @@ "FEDERATION_Discovery_Method_Description": "Pot utilitzar el hub o un SRV i una entrada TXT en els seus registres DNS.", "FEDERATION_Domain": "Domini", "FEDERATION_Domain_Alert": "No canvieu això després d’habilitar la funció, encara no podem manejar els canvis de domini.", - "FEDERATION_Domain_Description": "Afegeix el domini al que aquest servidor hauria d’estar vinculat, per exemple: @ rocket.chat.", - "FEDERATION_Enabled": "Intentar integrar la federació de suport.", + "FEDERATION_Domain_Description": "Afegiu el domini al qual ha d'estar vinculat aquest servidor, per exemple: @rocket.chat", + "FEDERATION_Enabled": "IIntenteu integrar el suport de la federació.", "FEDERATION_Enabled_Alert": "La federació de suport està en progrés. El seu ús en un entorn de producció no es recomana de moment.", - "FEDERATION_Error_user_is_federated_on_rooms": "No es pot eliminar als usuaris federats que pertenecen a les sales", + "FEDERATION_Error_user_is_federated_on_rooms": "No podeu eliminar usuaris federats que pertanyen a sales", "FEDERATION_Hub_URL": "URL del Hub", "FEDERATION_Hub_URL_Description": "Configureu la URL del concentrador, per exemple: https://hub.rocket.chat. També es poden acceptar ports.", "FEDERATION_Public_Key": "Clau pública", @@ -1892,10 +1905,10 @@ "FEDERATION_Room_Status": "Estat de la federació", "FEDERATION_Status": "Estat", "FEDERATION_Test_Setup": "Configuració de prova", - "FEDERATION_Test_Setup_Error": "No es pot trobar el servidor utilitzant la seva configuració, revisar la seva configuració.", + "FEDERATION_Test_Setup_Error": "No s'ha pogut trobar el vostre servidor usant la vostra configuració, reviseu-ne la configuració.", "FEDERATION_Test_Setup_Success": "La configuració de la seva federació està funcionant i altres servidors poden trobar-se!", "FEDERATION_Unique_Id": "Identificador únic", - "FEDERATION_Unique_Id_Description": "Aquest és l'ID únic de la seva federació, que s'utilitza per identificar el seu parell a la xarxa.", + "FEDERATION_Unique_Id_Description": "Aquest és l'ID únic de la vostra federació, que s'utilitza per identificar el vostre parell a la xarxa.", "Field": "Camp", "Field_removed": "Camp eliminat", "Field_required": "Camp obligatori", @@ -1935,12 +1948,12 @@ "FileUpload_GoogleStorage_Bucket": "Nom del Bucket Google Storage", "FileUpload_GoogleStorage_Bucket_Description": "El nom del bucket on els arxius s'haurien de pujar.", "FileUpload_GoogleStorage_Proxy_Avatars": "Avatars Proxy", - "FileUpload_GoogleStorage_Proxy_Avatars_Description": "Transmissions d'arxius proxy avatar a través del seu servidor en lloc d'accés directe a l'URL de l'actiu", + "FileUpload_GoogleStorage_Proxy_Avatars_Description": "Transmissions de fitxers d'avatar de servidor intermediari a través del servidor en lloc d'accés directe a la URL de l'actiu", "FileUpload_GoogleStorage_Proxy_Uploads": "Càrregues proxy", "FileUpload_GoogleStorage_Proxy_Uploads_Description": "Proxy carrega les transmissions d'arxius a través del vostre servidor en lloc d'accedir directament a l'URL de l'actiu", "FileUpload_GoogleStorage_Secret": "Secret Google Storage", "FileUpload_GoogleStorage_Secret_Description": "Si us plau, segueix aquestes instruccions i enganxa el resultat aquí.", - "FileUpload_json_web_token_secret_for_files": "Carregar Secret del token web JSON", + "FileUpload_json_web_token_secret_for_files": "Pujar fitxer Secret del token web Json", "FileUpload_json_web_token_secret_for_files_description": "File Upload json web Token Secret (s'utilitza per poder accedir als arxius carregats sense autenticació)", "FileUpload_MaxFileSize": "Mida màxima de pujada (en bytes)", "FileUpload_MaxFileSizeDescription": "Establiu-lo a -1 per eliminar la limitació de la mida del fitxer.", @@ -1952,14 +1965,14 @@ "FileUpload_MediaTypeWhiteListDescription": "Llista de tipus d'arxiu separada per comes. Deixa-la en blanc per acceptar tots els tipus.", "FileUpload_ProtectFiles": "Protegir els arxius pujats", "FileUpload_ProtectFilesDescription": "Només els usuaris identificats hi tindran accés", - "FileUpload_RotateImages": "Rotar imatges a l'carregar", + "FileUpload_RotateImages": "Rotar imatges en carregar", "FileUpload_RotateImages_Description": "Habilitar aquesta configuració pot causar pèrdua de qualitat d'imatge", "FileUpload_S3_Acl": "Acl", "FileUpload_S3_AWSAccessKeyId": "Access Key", - "FileUpload_S3_AWSSecretAccessKey": "Secret Key", + "FileUpload_S3_AWSSecretAccessKey": "Clau Secreta", "FileUpload_S3_Bucket": "Bucket Name", "FileUpload_S3_BucketURL": "Bucket URL", - "FileUpload_S3_CDN": "Domini CDN per descàrregues", + "FileUpload_S3_CDN": "Domini CDN per a descàrregues", "FileUpload_S3_ForcePathStyle": "Force Path Style", "FileUpload_S3_Proxy_Avatars": "Avatars Proxy", "FileUpload_S3_Proxy_Avatars_Description": "Proxy transmissions d'arxius d'avatar a través del seu servidor en lloc d'accés directe a l'URL de l'actiu", @@ -1972,12 +1985,12 @@ "FileUpload_Storage_Type": "Tipus d'emmagatzematge", "FileUpload_Webdav_Password": "Contrasenya de WebDAV", "FileUpload_Webdav_Proxy_Avatars": "Avatars Proxy", - "FileUpload_Webdav_Proxy_Avatars_Description": "Transmissions d'arxius d'avatar a través del seu servidor en lloc d'accés directe a l'URL de l'actiu", + "FileUpload_Webdav_Proxy_Avatars_Description": "Transmissions de fitxers d'avatar de servidor intermediari a través del servidor en lloc d'accés directe a la URL de l'actiu", "FileUpload_Webdav_Proxy_Uploads": "Càrregues proxy", "FileUpload_Webdav_Proxy_Uploads_Description": "Transmissions d'arxius de càrrega proxy a través del seu servidor en lloc d'accés directe a l'URL de l'actiu", "FileUpload_Webdav_Server_URL": "URL d'accés al servidor WebDAV", "FileUpload_Webdav_Upload_Folder_Path": "Carrega la ruta de la carpeta", - "FileUpload_Webdav_Upload_Folder_Path_Description": "Ruta de la carpeta WebDAV en la qual s'han de carregar els arxius", + "FileUpload_Webdav_Upload_Folder_Path_Description": "Ruta de la carpeta WebDAV on s'han de carregar els arxius", "FileUpload_Webdav_Username": "Nom d'usuari de WebDAV", "Filter": "Filter", "Filters": "Filtres", @@ -1998,24 +2011,24 @@ "For_more_details_please_check_our_docs": "Per obtenir més informació, consulteu els nostres documents.", "For_your_security_you_must_enter_your_current_password_to_continue": "Per a la seva seguretat, ha de tornar a introduir la contrasenya per continuar", "Force_Disable_OpLog_For_Cache": "Forçar la desactivació de OpLog per la caché", - "Force_Disable_OpLog_For_Cache_Description": "No s'utilitzarà OpLog per sincronitzar la cache tot i estar disponible", + "Force_Disable_OpLog_For_Cache_Description": "No utilitzarà OpLog per sincronitzar la memòria cache fins i tot quan estigui disponible", "Force_Screen_Lock": "Forçar el bloqueig de pantalla", "Force_Screen_Lock_After": "Forçar el bloqueig de pantalla després de", - "Force_Screen_Lock_After_description": "El temps per a sol·licitar la contrasenya de nou després de finalitzar l'última sessió, en segons.", - "Force_Screen_Lock_description": "Quan estigui habilitat, obligarà els seus usuaris a utilitzar un PIN / BIOMETRIA / FaceID per desbloquejar l'aplicació.", + "Force_Screen_Lock_After_description": "El temps per sol·licitar la contrasenya novament després de finalitzar la darrera sessió, en segons.", + "Force_Screen_Lock_description": "Quan estigui habilitat, obligarà els usuaris a utilitzar un PIN / BIOMETRIA / FACEID per desbloquejar l'aplicació.", "Force_SSL": "Força SSL", - "Force_SSL_Description": "*Atenció!* _Force SSL_ mai ha de ser usat amb servidor intermediari invers. Si s'utilitza un proxy invers, s'ha de fer la redirecció AL PROXY. Aquesta opció existeix per a les instal·lacions tipus Heroku, que no permeten la configuració de redireccions al proxy invers.", + "Force_SSL_Description": "* Precaució! * _Force SSL_ mai ha d'usar-se amb proxy invers. Si teniu un servidor intermediari invers, heu de fer la redirecció ALLÁ. Aquesta opció existeix per a implementacions com Heroku, que no permet la configuració de redireccionament al servidor intermediari invers.", "Force_visitor_to_accept_data_processing_consent": "Obligar el visitant a acceptar el consentiment de l'processament de dades", "Force_visitor_to_accept_data_processing_consent_description": "Els visitants no poden començar a xatejar sense el seu consentiment.", "Force_visitor_to_accept_data_processing_consent_enabled_alert": "L'acord amb el processament de dades s'ha de basar en una comprensió transparent de l'motiu de l'processament. A causa d'això, ha de completar la configuració a continuació que es mostrarà als usuaris per proporcionar les raons per recopilar i processar la seva informació personal.", "force-delete-message": "Forçar esborrar missatge", "force-delete-message_description": "Permís per esborrar un missatge ignorant totes les restriccions", "Forgot_password": "Heu oblidat la contrasenya?", - "Forgot_Password_Description": "És possible utilitzar els marcadors:
      • [Forgot_Password_Url] per a l'adreça URL de recuperació de contrasenya.
      • [name], [fname], [lname] per al nom complet de l'usuari, nom o cognom, respectivament.
      • [email] per a l'adreça de correu electrònic de l'usuari.
      • [Site_Name] i [Site_URL] pel nom del lloc web i de l'adreça URL, respectivament.
      ", + "Forgot_Password_Description": "Podeu utilitzar les adreces d'interès següents:
      • [Forgot_Password_Url] per a la URL de recuperació de contrasenya.
      • [name], [fname] , [lname] per al nom complet, nom o cognom de l'usuari, respectivament.
      • [email] per al correu electrònic de l'usuari.
      • [Site_Name ] i [Site_URL] per al nom de l'aplicació i l'URL respectivament.
      ", "Forgot_Password_Email": "Fes clic aquí per restablir la teva contrasenya.", "Forgot_Password_Email_Subject": "[Site_Name] - Recuperació de contrasenya", "Forgot_password_section": "No recordo la contrasenya", - "Forward": "Remetre", + "Forward": "Reenviar", "Forward_chat": "Remetre xat", "Forward_to_department": "Remetre al departament", "Forward_to_user": "Remetre a l'usuari", @@ -2051,7 +2064,7 @@ "Global_purge_override_warning": "Hi ha una política de retenció global. Si deixa desactivada l'opció \"Anul·lar la política de retenció global\", només pot aplicar una política que sigui més estricta que la política global.", "Global_Search": "Cerca global", "Go_to_your_workspace": "Aneu a l'espai de treball", - "GoogleCloudStorage": "Emmagatzematge Google Cloud", + "GoogleCloudStorage": "Emmagatzematge al núvol de Google", "GoogleNaturalLanguage_ServiceAccount_Description": "Arxiu JSON amb la clau del compte de servei (\"Service account key\"). Pots trobar més informació [aquí](https://cloud.google.com/natural-language/docs/common/auth#set_up_a_service_account)", "GoogleTagManager_id": "ID de Google Tag Manager", "Government": "Govern", @@ -2071,9 +2084,9 @@ "Header_and_Footer": "Encapçalament i peu ", "Pharmaceutical": "Farmacèutica", "Healthcare": "Sanitat", - "Helpers": "Ajuda", + "Helpers": "Ajudants", "Here_is_your_authentication_code": "Aquest és el seu codi d'autenticació:", - "Hex_Color_Preview": "Previsualització del color", + "Hex_Color_Preview": "Vista prèvia de color hexadecimal", "Hi": "Hola", "Hi_username": "Hola __name__", "Hidden": "Ocult", @@ -2089,6 +2102,7 @@ "Hide_System_Messages": "Ocultar els missatges del sistema", "Hide_Unread_Room_Status": "Amaga l'estat de sales no llegides", "Hide_usernames": "Oculta els noms d'usuari", + "Hide_video": "Amagar vídeo", "Highlights": "Ressalta", "Highlights_How_To": "Per ser notificat quan algú esmenta una paraula o frase, afegeix-la aquí. Es poden separar les paraules o frases amb comes. No es distingeix entre majúscules i minúscules.", "Highlights_List": "Ressalta paraules", @@ -2099,11 +2113,11 @@ "hours": "hores", "Hours": "Hores", "How_friendly_was_the_chat_agent": "Ha sigut amable l'interlocutor?", - "How_knowledgeable_was_the_chat_agent": "Era un bon expert?", - "How_long_to_wait_after_agent_goes_offline": "Quant temps esperar un cop l'agent es posa fora de línia", + "How_knowledgeable_was_the_chat_agent": "Era un bon expert, en sabia?", + "How_long_to_wait_after_agent_goes_offline": "Quant de temps esperar després que l'agent es desconnecti", "How_long_to_wait_to_consider_visitor_abandonment": "Quant de temps esperar per considerar l'abandonament dels visitans?", "How_long_to_wait_to_consider_visitor_abandonment_in_seconds": "Quant de temps esperar per considerar l'abandonament dels visitans?", - "How_responsive_was_the_chat_agent": "Heu rebut respostes ràpides?", + "How_responsive_was_the_chat_agent": "Què tan receptiu va ser lagent de xat?", "How_satisfied_were_you_with_this_chat": "Ha quedat satisfet amb aquesta conversa?", "How_to_handle_open_sessions_when_agent_goes_offline": "Com gestionar sessions obertes quan l'agent es desconnecta", "I_Saved_My_Password": "He desat la meva contrasenya", @@ -2114,22 +2128,22 @@ "If_you_are_sure_type_in_your_password": "Si n'està segur escrigui la contrasenya:", "If_you_are_sure_type_in_your_username": "Si n'està segur escrigui el seu nom d'usuari:", "If_you_didnt_ask_for_reset_ignore_this_email": "Si no va sol·licitar el restabliment de la contrasenya, pot ignorar aquest correu electrònic.", - "If_you_didnt_try_to_login_in_your_account_please_ignore_this_email": "Si no va intentar iniciar la sessió al compte, ignori aquest correu electrònic.", - "If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours": "Si no n'hi ha, envieu un correu electrònic a [omni@rocket.chat] (mailto: omni@rocket.chat) per obtenir el vostre.", + "If_you_didnt_try_to_login_in_your_account_please_ignore_this_email": "Si no heu intentat iniciar sessió al vostre compte, ignoreu aquest correu electrònic.", + "If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours": "Si no en teniu un, envieu un correu electrònic a [omni@rocket.chat] (mailto: omni@rocket.chat) per obtenir el vostre.", "Iframe_Integration": "Integració Iframe", "Iframe_Integration_receive_enable": "Activa recepció", "Iframe_Integration_receive_enable_Description": "Permetre que la finestra pare enviï ordres a Rocket.Chat.", "Iframe_Integration_receive_origin": "Rebre orígens", - "Iframe_Integration_receive_origin_Description": "Origens amb prefix del protocol, separats per comes, dels quals es permet rebre comandes. Exemple 'http://localhost, https://localhost', o * per permetre rebre de qualsevol lloc.", + "Iframe_Integration_receive_origin_Description": "Orígens amb prefix de protocol, separats per comes, que poden rebre ordres, p. Ex. 'https:// localhost, http://localhost', o * per permetre rebre des de qualsevol lloc.", "Iframe_Integration_send_enable": "Activa enviament", "Iframe_Integration_send_enable_Description": "Envia esdeveniments a la finestra pare", "Iframe_Integration_send_target_origin": "Envia l'origen a l'objectiu", - "Iframe_Integration_send_target_origin_Description": "Origens amb prefix del protocol on les comandes són enviades. Exemple 'https://localhost', o * per permetre enviar a qualsevol lloc.", + "Iframe_Integration_send_target_origin_Description": "Origen amb prefix de protocol, al qual s'envien les ordres, p. Ex. 'https://localhost', o * per permetre l'enviament a qualsevol lloc.", "Iframe_Restrict_Access": "Restringir l'accés dins de qualsevol iframe", "Iframe_Restrict_Access_Description": "Aquesta configuració habilita / inhabilita les restriccions per carregar el RC dins de qualsevol iframe", "Iframe_X_Frame_Options": "Opcions de X-Frame-Options", "Iframe_X_Frame_Options_Description": "Opcions de X-Frame-Options. [Podeu consultar més detalls aquí.] (Https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options#Syntax)", - "Ignore": "Ignora", + "Ignore": "Ignorar", "Ignored": "Ignorat", "Images": "Imatges", "IMAP_intercepter_already_running": "L'interceptador IMAP ja està executant-se", @@ -2148,13 +2162,13 @@ "Importer_finishing": "Finalitza la importació.", "Importer_From_Description": "Importa les dades de __from__ a Rocket.Chat.", "Importer_HipChatEnterprise_BetaWarning": "Tingueu en compte que aquest sistema d'importació encara està en desenvolupament. Si us plau, notifiqueu-nos a GitHub els errors que es produeixin:", - "Importer_HipChatEnterprise_Information": "L'arxiu pujat ha de ser un tar.gz desencriptat. Si us plau, llegiu la documentació per a més informació:", + "Importer_HipChatEnterprise_Information": "L'arxiu carregat ha de ser un tar.gz desxifrat, llegiu la documentació per obtenir més informació:", "Importer_import_cancelled": "Importació cancel·lada.", "Importer_import_failed": "S'ha produït un error durant la importació.", "Importer_importing_channels": "Important els canals.", "Importer_importing_files": "Importació d'arxius.", "Importer_importing_messages": "Important els missatges.", - "Importer_importing_started": "Començant la importació.", + "Importer_importing_started": "Iniciant la importació.", "Importer_importing_users": "Important els usuaris.", "Importer_not_in_progress": "L'importador actualment no s'està executant.", "Importer_not_setup": "L'importador no està configurat correctament ja que no ha retornat cap dada.", @@ -2185,7 +2199,7 @@ "importer_status_uploading": "Pujant arxiu", "importer_status_user_selection": "Preparat per seleccionar què voleu importar", "Importer_Upload_FileSize_Message": "La configuració del servidor permet pujar arxius de mida fins __maxFileSize__.", - "Importer_Upload_Unlimited_FileSize": "La configuració del seu servidor permet la pujada d'arxius de qualsevol mida.", + "Importer_Upload_Unlimited_FileSize": "La configuració del vostre servidor permet la càrrega de fitxers de qualsevol mida.", "Importing_channels": "Important els canals", "Importing_Data": "Importació de dades", "Importing_messages": "Important els missatges.", @@ -2204,7 +2218,7 @@ "Install": "Instal·lar", "Install_Extension": "Instal·lar complement", "Install_FxOs": "Instal·lar el Rocket.Chat al Firefox", - "Install_FxOs_done": "Perfecte! Ja pots utilitzar el Rocket.Chat mitjançant la icona de l'escriptori.", + "Install_FxOs_done": "Excel·lent! Ara podeu utilitzar Rocket.Chat a través de la icona a la pantalla inicial. Diverteix-te amb Rocket.Chat!", "Install_FxOs_error": "Sentim que no hagi funcionat bé! S'ha topat amb el següent error:", "Install_FxOs_follow_instructions": "Si us plau, confirma la instal·lació de l'aplicació al teu dispositiu (polsi \"Instal·lar\" quan se us demani).", "Install_package": "Instal·leu el paquet", @@ -2236,23 +2250,23 @@ "Integration_Outgoing_WebHook_History_Http_Response_Error": "Error HTTP de resposta", "Integration_Outgoing_WebHook_History_Messages_Sent_From_Prepare_Script": "Missatges enviats durant el pas de preparació", "Integration_Outgoing_WebHook_History_Messages_Sent_From_Process_Script": "Missatges enviats durant el procés de la resposta", - "Integration_Outgoing_WebHook_History_Time_Ended_Or_Error": "Quan ha acabat o emès un error", + "Integration_Outgoing_WebHook_History_Time_Ended_Or_Error": "Hora de finalització o error", "Integration_Outgoing_WebHook_History_Time_Triggered": "Quan s'ha disparat la integració", "Integration_Outgoing_WebHook_History_Trigger_Step": "Darrer pas del disparador", - "Integration_Outgoing_WebHook_No_History": "Aquesta integració WebHook de sortida encara no té cap historial registrat.", + "Integration_Outgoing_WebHook_No_History": "Aquesta integració de webhook sortint encara no té cap historial registrat.", "Integration_Retry_Count": "Comptador de reintents", - "Integration_Retry_Count_Description": "Quantes vegades s'ha de reintentar la integració si la petició a l'adreça URL falla?", + "Integration_Retry_Count_Description": "Quantes vegades s'ha d'intentar la integració si falla la trucada a la URL?", "Integration_Retry_Delay": "Temps de reintent", "Integration_Retry_Delay_Description": "Quin algorisme de retard de reintent s'ha d'utilitzar? 10^x o 2^x o x*2", "Integration_Retry_Failed_Url_Calls": "Reintenta peticions d'URL fallades", - "Integration_Retry_Failed_Url_Calls_Description": "Si la petició a l'adreça URL falla, la integració ha d'esperar un temps raonable abans de reintentar-la?", + "Integration_Retry_Failed_Url_Calls_Description": "Hauríeu d'intentar la integració una quantitat de temps raonable si falla la trucada a la URL?", "Integration_Run_When_Message_Is_Edited": "Executa en edicions", "Integration_Run_When_Message_Is_Edited_Description": "Aquesta integració s'ha d'executar quan el missatge s'edita? Desactivar aquesta opció farà que la integració només s'executi en missatges nous .", "Integration_updated": "La integració s'ha actualitzat.", - "Integration_Word_Trigger_Placement": "Posició indeterminada de paraula", + "Integration_Word_Trigger_Placement": "Col·locació de paraules a qualsevol lloc", "Integration_Word_Trigger_Placement_Description": "S'hauria d'activar el disparador quan la paraula es troba en un lloc de la frase fora del començament?", "Integrations": "Integracions", - "Integrations_for_all_channels": "Introdueix all_public_channels per escoltar a totes les sales públiques, all_private_groups per escoltar a tots els grups privats i all_direct_messages per escoltar tots els missatges directes.", + "Integrations_for_all_channels": "Introduïu all_public_channels per escoltar a tots els canals públics, all_private_groups per escoltar a tots els grups privats i all_direct_messages per escoltar tots els missatges directes", "Integrations_Outgoing_Type_FileUploaded": "Arxiu pujat", "Integrations_Outgoing_Type_RoomArchived": "Sala arxivada", "Integrations_Outgoing_Type_RoomCreated": "Sala creada (pública i privada)", @@ -2290,7 +2304,7 @@ "Invitation": "Invitació", "Invitation_Email_Description": "Podeu utilitzar els següents marcadors:
      • [email] per a l'adreça del receptor del missatge.
      • [Site_Name] i [Site_URL] per al nom de l'aplicació i l'adreça URL, respectivament.
      ", "Invitation_HTML": "HTML de la invitació", - "Invitation_HTML_Default": "

      Se us ha convidat a [Site_Name]

      Aneu a [Site_URL] i proveu la millor solució de col·laboració a distància de codi lliure!

      ", + "Invitation_HTML_Default": "

      Se us ha convidat a [Site_Name]

      Aneu a [Site_URL] i provar la millor solució de xat de codi obert disponibles actualment!

      ", "Invitation_Subject": "Assumpte de la invitació", "Invitation_Subject_Default": "Se us ha convidat a [Site_Name]", "Invite": "Invitació", @@ -2300,10 +2314,10 @@ "Invite_user_to_join_channel_all_to": "Invita a tots els usuaris d'aquest canal a unir-se a [#channel]", "Invite_Users": "Convidar usuaris", "IP": "IP", - "IRC_Channel_Join": "Resposta de la comanda JOIN.", + "IRC_Channel_Join": "Sortida de l'ordre JOIN.", "IRC_Channel_Leave": "Resposta de la comanda PART.", "IRC_Channel_Users": "Resposta de la comanda NAMES.", - "IRC_Channel_Users_End": "Final de la resposta de la comanda NAMES.", + "IRC_Channel_Users_End": "Final de la sortida de l'ordre NAMES.", "IRC_Description": "Internet Relay Chat (IRC) és una eina de comunicació grupal basada en text. Els usuaris s'uneixen a canals o sales amb noms exclusius per a una discussió oberta. IRC també admet missatges privats entre usuaris individuals i capacitats per a compartir arxius. Aquest paquet integra aquestes capes de funcionalitat amb Rocket.Chat.", "IRC_Enabled": "Integrar suport IRC. Canviar aquest valor requereix reiniciar el Rocket.Chat.", "IRC_Enabled_Alert": "El suport d'IRC és un treball en progrés. No es recomana el seu ús en un sistema de producció en aquest moment.", @@ -2311,7 +2325,7 @@ "IRC_Federation_Disabled": "La Federació IRC està desactivada.", "IRC_Hostname": "Servidor d'IRC on connectar.", "IRC_Login_Fail": "Resposta en cas de connexió al servidor d'IRC fallida.", - "IRC_Login_Success": "Resposta en cas de connexió al servidor d'IRC reeixida.", + "IRC_Login_Success": "Sortida després d'una connexió amb èxit al servidor IRC.", "IRC_Message_Cache_Size": "Límit de memòria d'intercanvi (cache) per manegar els missatges sortints.", "IRC_Port": "Port on unir-se al servidor d'IRC.", "IRC_Private_Message": "Resposta de la comanda PRIVMSG.", @@ -2329,32 +2343,34 @@ "italics": "cursiva", "Items_per_page:": "Elements per pàgina:", "Jitsi_Application_ID": "ID d'aplicació (iss)", - "Jitsi_Application_Secret": "Secret d'aplicació", - "Jitsi_Chrome_Extension": "ID d'extensió del Chrome", + "Jitsi_Application_Secret": "Secret de l'aplicació", + "Jitsi_Chrome_Extension": "ID de l'extensió del Chrome", "Jitsi_Enable_Channels": "Activa als canals", "Jitsi_Enable_Teams": "Habilitar per equips", "Jitsi_Enabled_TokenAuth": "Activa l'autenticació JWT", - "Jitsi_Limit_Token_To_Room": "Limita el token a Jitsi Room", + "Jitsi_Limit_Token_To_Room": "Limitar token a la Room Jitsi", "Job_Title": "Títol professional", "join": "Unir-se", + "Join_call": "Unir-se a la trucada", "Join_audio_call": "Unir-se a la trucada", "Join_Chat": "Uneix-te al xat", "Join_default_channels": "Unir-se als canals predeterminats", "Join_the_Community": "Uneix-te a la comunitat", "Join_the_given_channel": "Unir-se al canal proporcionat", "Join_video_call": "Unir-se a la videotrucada", + "Join_my_room_to_start_the_video_call": "Uneix-te a la meva sala per iniciar la videotrucada", "join-without-join-code": "Unir-se sense el codi", "join-without-join-code_description": "Permís per unir-se a canals amb codi d'unió actiu sense tenir-lo", "Joined": "Unit", "Joined_at": "Inscrit a", - "Jump": "Vés", - "Jump_to_first_unread": "Vés al primer no llegit", + "Jump": "Saltar", + "Jump_to_first_unread": "Anar al primer no llegit", "Jump_to_message": "Vés al missatge", "Jump_to_recent_messages": "Vés als missatges recents", "Just_invited_people_can_access_this_channel": "Només les persones convidades poden accedir a aquest canal.", "Katex_Dollar_Syntax": "Permetre Dòlar Sintaxi", "Katex_Dollar_Syntax_Description": "Permetre l'ús de $$ bloc katex $$ $ i $ Katex línia sintaxi", - "Katex_Enabled": "Katex actiu", + "Katex_Enabled": "Katex Habilitada", "Katex_Enabled_Description": "Permetre l'ús de katex per a la composició tipogràfica de matemàtiques en els missatges", "Katex_Parenthesis_Syntax": "Permetre Parèntesi Sintaxi", "Katex_Parenthesis_Syntax_Description": "Permetre l'ús de \\ [bloc katex \\] \\ sintaxi i (en línia katex \\)", @@ -2371,7 +2387,7 @@ "Keyboard_Shortcuts_Mark_all_as_read": "Marca tots els missatges (en tots els canals) com a llegits", "Keyboard_Shortcuts_Move_To_Beginning_Of_Message": "Moure's al principi del missatge", "Keyboard_Shortcuts_Move_To_End_Of_Message": "Moure's al final del missatge", - "Keyboard_Shortcuts_New_Line_In_Message": "Nova línia al camp d'entrada de missatge", + "Keyboard_Shortcuts_New_Line_In_Message": "Nova línia a l'entrada de redacció del missatge", "Keyboard_Shortcuts_Open_Channel_Slash_User_Search": "Canal obert / Cerca d'usuari", "Keyboard_Shortcuts_Title": "Dreceres de teclat", "Knowledge_Base": "Centre de suport", @@ -2404,25 +2420,25 @@ "Language_Swedish": "Suec", "Language_Version": "Versió en català", "Last_7_days": "Els darrers 7 dies", - "Last_30_days": "Darrers 30 dies", + "Last_30_days": "Últims 30 Dies", "Last_90_days": "Darrers 90 dies", "Last_active": "Darrer actiu", "Last_Chat": "Darrer xat", "Last_login": "Darrer inici de sessió", "Last_Message": "Últim missatge", "Last_Message_At": "Últim missatge a", - "Last_seen": "Vist per darrer cop", + "Last_seen": "Última vegada vist", "Last_Status": "Darrer estat", "Last_token_part": "Darrera part del token", "Last_Updated": "Última actualització", - "Launched_successfully": "S'ha iniciat amb èxit", + "Launched_successfully": "Llançat amb èxit", "Layout": "Disseny", "Layout_Home_Body": "Cos de pàgina d'inici", "Layout_Home_Title": "Títol de pàgina d'inici", "Layout_Legal_Notice": "Avís legal", "Layout_Login_Terms": "Termes d'inici de sessió", "Layout_Privacy_Policy": "Política de privacitat", - "Layout_Show_Home_Button": "Mostra el botó inici\"", + "Layout_Show_Home_Button": "Mostra el \"botó inici\"", "Layout_Sidenav_Footer": "Peu de la barra de navegació lateral", "Layout_Sidenav_Footer_description": "La mida del peu és de 260 x 70 px", "Layout_Terms_of_Service": "Avís legal", @@ -2457,7 +2473,7 @@ "LDAP_Authentication_UserDN": "User DN", "LDAP_Authentication_UserDN_Description": "Usuari LDAP que fa cerques d'usuari per identificar altres usuaris quan inicien sessió.
      Aquest és un compte que s'acostuma a crear específicament per a fer les integracions de tercers. Utilitza un nom complet i qualificat, com `cn=Administrator,cn=Users,dc=Example,dc=com`.", "LDAP_Avatar_Field": "Camp d’avatar d’usuari", - "LDAP_Avatar_Field_Description": "Què camp s'utilitzarà com * avatar * per als usuaris. Deixi-ho en blanc per a utilitzar `thumbnailPhoto` primer i` jpegPhoto` com a alternativa.", + "LDAP_Avatar_Field_Description": "Quin camp s'utilitzarà com a *avatar* per als usuaris. Deixeu-lo en blanc per utilitzar `thumbnailPhoto` primer i `jpegPhoto` com a respatller.", "LDAP_Background_Sync": "Sincronització de fons", "LDAP_Background_Sync_Avatars": "Sincronització de fons d'avatar", "LDAP_Background_Sync_Avatars_Description": "Habiliteu un procés en segon pla separat per sincronitzar els avatars dels usuaris.", @@ -2469,26 +2485,26 @@ "LDAP_Background_Sync_Keep_Existant_Users_Updated": "Actualització de fons de sincronització dels usuaris existents", "LDAP_Background_Sync_Keep_Existant_Users_Updated_Description": "Sincronitzarà l'avatar, els camps, el nom d'usuari, etc. (Segons la seva configuració) de tots els usuaris ja importats d'LDAP en cada ** Interval de sincronització **", "LDAP_BaseDN": "Base DN", - "LDAP_BaseDN_Description": "El nom distingit (DN) completament qualificat d'un subarbre LDAP que voleu cercar usuaris i grups. Podeu afegir tants com vulgui; però, cada grup ha d'estar definit en la mateixa base de domini que els usuaris que li pertanyen. Exemple: `ou = Usuaris + ou = Projectes, dc = exemple, dc = com`. Si s'especifica grups d'usuaris restringits, només els usuaris que pertanyen a aquests grups estaran dins de l'abast. Li recomanem que especifiqui el nivell superior de la seva vista de directori LDAP com a base del seu domini i utilitzi el filtre de cerca per controlar l'accés.", + "LDAP_BaseDN_Description": "El nom distingit (DN) complet d'un subarbre LDAP on voleu cercar usuaris i grups. Podeu afegir tants com vulgueu; no obstant això, cada grup ha d'estar definit a la mateixa base de domini que els usuaris que hi pertanyen. Exemple: `ou = Usuaris + ou = Projectes, dc = Exemple, dc = com`. Si especifiqueu grups d'usuaris restringits, només els usuaris que pertanyen a aquests grups estaran dins de l'abast. Us recomanem que especifiqueu el nivell superior del vostre arbre de directoris LDAP com a base del vostre domini i utilitzeu el filtre de cerca per controlar l'accés.", "LDAP_CA_Cert": "CA Cert", "LDAP_Connect_Timeout": "Temps d'espera connexió (ms)", "LDAP_DataSync_AutoLogout": "Usuaris desactivats de tancament de sessió automàtic", "LDAP_Default_Domain": "Domini predeterminat", - "LDAP_Default_Domain_Description": "Si es proporciona el domini per defecte s'utilitzarà per crear un correu electrònic únic per als usuaris en què el correu electrònic no s'ha importat des de LDAP. El correu electrònic es muntarà com a `nomusuari@dominiperdefecte` o `nom_usuari_unic@dominiperdefecte`.
      Exemple: `rocket.chat`", - "LDAP_Enable": "Activa", + "LDAP_Default_Domain_Description": "si es proporciona, el domini per defecte s'utilitzarà per crear un correu electrònic únic per als usuaris en què el correu electrònic no s'ha importat des de LDAP. El correu electrònic es muntarà com a `username @ default_domain` o ` unique_id @ default_domain`.
      Exemple: `rocket.chat`", + "LDAP_Enable": "Habilitar", "LDAP_Enable_Description": "Intentar utilitzar LDAP com a mètode d'autenticació", "LDAP_Enable_LDAP_Groups_To_RC_Teams": "Habiliteu el mapeig de l'equip de LDAP a Rocket.Chat", "LDAP_Encryption": "Xifrat", "LDAP_Encryption_Description": "Mètode de xifrat utilitzat per a la comunicació segura cap al servidor LDAP. Alguns exemples 'sense xifrat', 'SSL / LDAPS (xifrat des de l'inici), i' StartTLS '(actualitzar a comunicacions xifrades una vegada connectat).", "LDAP_Find_User_After_Login": "Cerca l'usuari després d'iniciar sessió", "LDAP_Find_User_After_Login_Description": "Realitzarà una recerca de l'DN de l'usuari després de la vinculació per garantir que la vinculació es va realitzar correctament i evitarà l'inici de sessió amb contrasenyes buides quan ho permeti la configuració d'AD.", - "LDAP_Group_Filter_Enable": "Activa el filtre de grups d'usuaris LDAP", + "LDAP_Group_Filter_Enable": "Habilita el filtre de grup d'usuaris LDAP", "LDAP_Group_Filter_Enable_Description": "Restringir l'accés als usuaris en un grup LDAP
      Útil per permetre que els servidors OpenLDAP sense un filtre * memberOf * restringeixin l'accés per grups", - "LDAP_Group_Filter_Group_Id_Attribute": "Atribut ID de grup (Group ID)", + "LDAP_Group_Filter_Group_Id_Attribute": "Atribut ID de grup", "LDAP_Group_Filter_Group_Id_Attribute_Description": "Exemple: *OpenLDAP:*cn", "LDAP_Group_Filter_Group_Member_Attribute": "Atribut Membre de grup (Group Member)", "LDAP_Group_Filter_Group_Member_Attribute_Description": "Exemple: *OpenLDAP:*uniqueMember", - "LDAP_Group_Filter_Group_Member_Format": "Format Membre de grup (Group Member)", + "LDAP_Group_Filter_Group_Member_Format": "Format de membre del grup", "LDAP_Group_Filter_Group_Member_Format_Description": "Exemple: *OpenLDAP:*uid=#{username},ou=users,o=Company,c=com", "LDAP_Group_Filter_Group_Name": "Nom del grup", "LDAP_Group_Filter_Group_Name_Description": "Nom del grup on pertany l'usuari", @@ -2503,8 +2519,8 @@ "LDAP_Internal_Log_Level": "Nivell de log intern", "LDAP_Login_Fallback": "Inici de sessió alternativa (fallback)", "LDAP_Login_Fallback_Description": "Si l'inici de sessió LDAP no funciona, intenta iniciar-la amb el sistema de comptes per defecte/local. Útil si el servei LDAP no està disponible per algun motiu.", - "LDAP_Merge_Existing_Users": "Uneix usuaris existents", - "LDAP_Merge_Existing_Users_Description": "* Precaució! * A l'importar un usuari de LDAP i ja existeix un usuari amb el mateix nom d'usuari, la informació i la contrasenya d'LDAP s'establiran en l'usuari existent.", + "LDAP_Merge_Existing_Users": "Fusiona els usuaris existents", + "LDAP_Merge_Existing_Users_Description": "* Precaució! * Quan s'importa un usuari de LDAP i ja existeix un usuari amb el mateix nom d'usuari, la informació i la contrasenya de LDAP s'establiran a l'usuari existent.", "LDAP_Port": "Port", "LDAP_Port_Description": "Port per accedir a LDAP. Ex. `389` o `636` per LDAPS", "LDAP_Prevent_Username_Changes": "Impedir que els usuaris d'LDAP canviïn el nom d'usuari de Rocket.Chat", @@ -2512,11 +2528,11 @@ "LDAP_Reconnect": "Reconnecta", "LDAP_Reconnect_Description": "Proveu tornar a connectar-se automàticament quan la connexió s'interrompi per algun motiu mentre executa operacions", "LDAP_Reject_Unauthorized": "Rebutja no autoritzat", - "LDAP_Reject_Unauthorized_Description": "Desactiveu aquesta opció per permetre certificats que no es poden verificar. En general, els certificats autofirmados requeriran que aquesta opció estigui desactivada per funcionar", + "LDAP_Reject_Unauthorized_Description": "Desactiveu aquesta opció per permetre certificats que no es poden verificar. En general, els certificats autosignats requeriran que aquesta opció estigui desactivada per funcionar", "LDAP_Search_Page_Size": "Mida de la pàgina de cerca", "LDAP_Search_Page_Size_Description": "El nombre màxim d'entrades que cada pàgina de resultats tornarà a processar", "LDAP_Search_Size_Limit": "Límit de la mida de la cerca", - "LDAP_Search_Size_Limit_Description": "El nombre màxim d'entrades a tornar.
      **Atenció** Aquest número hauria de ser superior a **Mida de la pàgina de cerca**", + "LDAP_Search_Size_Limit_Description": "El nombre màxim d'entrades per tornar.
      ** Atenció ** Aquest número ha de ser més gran que ** Mida de la pàgina de cerca **", "LDAP_Sync_Custom_Fields": "Sincronitzar camps personalitzats", "LDAP_CustomFieldMap": "Assignació de camps personalitzats", "LDAP_Sync_AutoLogout_Enabled": "Habilitar tancament de sessió automàtic", @@ -2550,15 +2566,19 @@ "LDAP_Sync_User_Data_Roles_Filter_Description": "El filtre de cerca LDAP que s'usa per verificar si un usuari està en un grup.", "LDAP_Sync_User_Data_RolesMap": "Mapa de grup de dades d'usuari", "LDAP_Sync_User_Data_RolesMap_Description": "Mapeja els grups LDAP als rols d'usuari de Rocket.Chat
      Com a exemple, `{\"rocket-admin\":\"admin\", \"tech-support\":\"support\"}` mapejarà el grup LDAP de rocket- admin a el paper de \"admin\" de Rocket.", + "LDAP_Teams_BaseDN": "Equips LDAP BaseDN", + "LDAP_Teams_BaseDN_Description": "El LDAP BaseDN utilitza't per a cercar equips d'usuari.", + "LDAP_Teams_Name_Field": "Atribut Nom de l'equip LDAP", + "LDAP_Teams_Name_Field_Description": "L'atribut LDAP que Rocket.Chat ha d'utilitzar per carregar el nom de l'ordinador. Podeu especificar més d'un nom d'atribut possible si els separa amb una coma.", "LDAP_Timeout": "Temps d'espera (ms)", "LDAP_Timeout_Description": "Quants mil·lisegons esperen un resultat de cerca abans de tornar un error", "LDAP_Unique_Identifier_Field": "Camp d'identificador únic", - "LDAP_Unique_Identifier_Field_Description": "Aquest camp s'utilitzarà per vincular l'usuari LDAP amb l'usuari Rocket.Chat. Pot proporcionar diversos valors separats per coma per intentar obtenir el valor del registre LDAP.
      El valor per defecte és `objectGUID,ibm-entryUUID,GUID,dominoUNID,nsuniqueId,uidNumber`", + "LDAP_Unique_Identifier_Field_Description": "Quin camp s'utilitzarà per enllaçar l'usuari LDAP i l'usuari de Rocket.Chat. Podeu informar diversos valors separats per comes per intentar obtenir el valor del registre LDAP.
      El valor per defecte és `objectGUID, ibm-entryUUID, GUID, dominoUNID, nsuniqueId, uidNumber`", "LDAP_User_Found": "Usuari LDAP trobat", "LDAP_User_Search_AttributesToQuery": "Atributs per consulta", "LDAP_User_Search_AttributesToQuery_Description": "Especifiqueu quins atributs han de tornar-en les consultes LDAP, separant-los amb comes. Valors predeterminats per a tot. `*` Representa tots els atributs regulars i `+` representa tots els atributs operatius. Assegureu-vos d'incloure tots els atributs que utilitzen totes les opcions de sincronització de Rocket.Chat.", "LDAP_User_Search_Field": "Camp de cerca", - "LDAP_User_Search_Field_Description": "L'atribut LDAP que identifica l'usuari LDAP que intenta l'autenticació. Aquest camp ha de ser \"sAMAccountName\" per a la majoria de les instal·lacions d'Active Directory, però pot ser \"uid\" per a altres solucions LDAP, com OpenLDAP. Feu servir `mail` per identificar els usuaris per correu electrònic o qualsevol atribut que vulgueu.
      Podeu usar diversos valors separats per comes per permetre que els usuaris iniciïn sessió fent servir múltiples identificadors com a nom d'usuari o correu electrònic.", + "LDAP_User_Search_Field_Description": "L'atribut LDAP que identifica l'usuari LDAP que intenta autenticació. Aquest camp ha de ser \"sAMAccountName\" per a la majoria de les instal·lacions d'Active Directory, però pot ser \"uid\" per a altres solucions LDAP, com ara OpenLDAP. Podeu utilitzar `mail` per identificar els usuaris per correu electrònic o qualsevol atribut que vulgueu.
      Podeu utilitzar diversos valors separats per comes per permetre que els usuaris iniciïn sessió usant múltiples identificadors com a nom d'usuari o correu electrònic.", "LDAP_User_Search_Filter": "Filter", "LDAP_User_Search_Filter_Description": "Si s'especifica, només els usuaris que compleixin aquest filtre podran iniciar sessió. Si no s'especifica cap filtre, tots els usuaris del domini base podran fer-ho.
      Exemple per Active Directory `memberOf=cn=ROCKET_CHAT,ou=General Groups`.
      Exemple per OpenLDAP (cerca de patró extensible) `ou:dn:=ROCKET_CHAT`.", "LDAP_User_Search_Scope": "Scope", @@ -2571,7 +2591,7 @@ "Lead_capture_phone_regex": "Regex de telèfon de captura clients potencials", "Leave": "Sortir ", "Leave_a_comment": "Deixar un comentari", - "Leave_Group_Warning": "Segur que vols abandonar el grup \"%s\"?", + "Leave_Group_Warning": "Segur que vols deixar el grup \"%s\"?", "Leave_Livechat_Warning": "Segur que vols sortir de l'LiveChat amb \"% s\"?", "Leave_Private_Warning": "Segur que vols sortir de la conversa amb \"%s\"?", "Leave_room": "Sortir ", @@ -2622,15 +2642,16 @@ "Livechat_Inquiry_Already_Taken": "Sol·licitud de LiveChat ja atesa", "Livechat_Installation": "Instal·lació de Livechat", "Livechat_last_chatted_agent_routing": "Agent preferit en l'últim xat", - "Livechat_last_chatted_agent_routing_Description": "La configuració de l'últim agent amb el qual va conversar assigna xats a l'agent que va interactuar anteriorment amb el mateix visitant si l'agent està disponible quan s'inicia el xat.", + "Livechat_last_chatted_agent_routing_Description": "La configuració del darrer agent amb què va conversar assigna xats a l'agent que va interactuar anteriorment amb el mateix visitant si l'agent està disponible quan s'inicia el xat.", "Livechat_managers": "Supervisors de LiveChat", "Livechat_Managers": "Administradors", "Livechat_max_queue_wait_time_action": "Com gestionar els xats a la cua quan s'arriba al temps màxim d'espera", "Livechat_maximum_queue_wait_time": "Temps màxim d'espera en cua", + "Livechat_maximum_queue_wait_time_description": "Temps màxim (en minuts) per mantenir els xats a la cua. -1 significa il·limitat", "Livechat_message_character_limit": "Límit de caràcters de missatge de LiveChat", "Livechat_monitors": "Monitors de Livechat", "Livechat_Monitors": "Monitors", - "Livechat_offline": "LiveChat fora de línia", + "Livechat_offline": "LiveChat desconectat", "Livechat_offline_message_sent": "Missatge de LiveChat enviat sense connexió", "Livechat_OfflineMessageToChannel_enabled": "Enviar missatges sense connexió d'LiveChat a un canal", "Omnichannel_on_hold_chat_resumed": "Represa de xat en espera: __comment__", @@ -2662,6 +2683,7 @@ "Livechat_Triggers": "Activadors LiveChat", "Livechat_user_sent_chat_transcript_to_visitor": "__agent__ va enviar la transcripció de xat a __guest__", "Livechat_Users": "Usuaris de LiveChat ", + "Livechat_Calls": "Trucades Livechat", "Livechat_visitor_email_and_transcript_email_do_not_match": "El correu electrònic del visitant i el de la transcripció no coincideixen", "Livechat_visitor_transcript_request": "__guest__ ha sol·licitat la transcripció del xat", "LiveStream & Broadcasting": "Transmissió en directe i transmissió", @@ -2730,7 +2752,7 @@ "mail-messages": "Missatges via correu-e", "mail-messages_description": "Permís per utilitzar l'opció d'enviament de missatges via correu-e", "Mailer": "Missatge correu-e", - "Mailer_body_tags": "És necessari utilitzar [unsubscribe] per a l'enllaç d'anul·lació de la subscripció.
      És possible utilitzar [name], [fname], [lname] per al nom complet de l'usuari, nom o cognom, respectivament.
      També [email] per a l'adreça de correu electrònic de l'usuari.", + "Mailer_body_tags": "Vostè ha de utilitzar [unsubscribe] per a l'enllaç de cancel·lació de subscripció.
      Podeu utilitzar [name], [fname], [lname] per al nom complet, nom o cognom de l'usuari, respectivament.
      Podeu utilitzar [email] per al correu electrònic de l'usuari.", "Mailing": "Enviament", "Make_Admin": "Fes admin", "Make_sure_you_have_a_copy_of_your_codes_1": "Assegureu-vos de tenir una còpia dels codis:", @@ -2751,7 +2773,7 @@ "manage-integrations_description": "Permís per gestionar les integracions del servidor", "manage-livechat-agents": "Administrar agents de LiveChat", "manage-livechat-agents_description": "Permís per gestionar agents Livechat", - "manage-livechat-departments": "Gestioneu els departaments de LiveChat", + "manage-livechat-departments": "Administrar departaments de LiveChat", "manage-livechat-departments_description": "Permís per gestionar departaments Livechat", "manage-livechat-managers": "Administrar administradors de LiveChat", "manage-livechat-managers_description": "Permís per gestionar gestors Livechat", @@ -2759,11 +2781,11 @@ "manage-oauth-apps_description": "Permís per gestionar les apps Oauth del servidor", "manage-outgoing-integrations": "Administrar les integracions sortints", "manage-outgoing-integrations_description": "Permís per gestionar les integracions sortints del servidor", - "manage-own-incoming-integrations": "Gestionar les pròpies integracions entrants", + "manage-own-incoming-integrations": "Administrar les pròpies integracions entrants", "manage-own-incoming-integrations_description": "Permís per permetre als usuaris crear i editar les seves pròpies integracions entrants o webhooks", "manage-own-integrations": "Gestionar les pròpies integracions", "manage-own-integrations_description": "Permís per permetre als usuaris crear i editar les seves pròpies integracions o webhooks", - "manage-own-outgoing-integrations": "Gestioneu les pròpies integracions sortints", + "manage-own-outgoing-integrations": "Administrar les pròpies integracions sortints", "manage-own-outgoing-integrations_description": "Permís per permetre als usuaris crear i editar les seves pròpies integracions de sortida o webhooks", "manage-selected-settings": "Canvieu alguns paràmetres", "manage-selected-settings_description": "Permís per canviar la configuració que es concedeix explícitament per canviar-la", @@ -2796,8 +2818,9 @@ "Markdown_Marked_Smartypants": "Habilita Smartypants marcats", "Markdown_Marked_Tables": "Activa les Taules Marcades", "Markdown_Parser": "Parsejador Markdown", - "Markdown_SupportSchemesForLink": "Markdown detecta scheme:// com a enllaç", + "Markdown_SupportSchemesForLink": "Esquemes de suport de Markdown per a enllaç", "Markdown_SupportSchemesForLink_Description": "Llista dels scheme:// permesos separats per comes", + "Marketplace": "Mercat", "Marketplace_view_marketplace": "Veure Marketplace", "MAU_value": "MAU __value__", "Max_length_is": "La llargada màxima és %s", @@ -2821,17 +2844,17 @@ "Mentions_default": "Mencions (per defecte)", "Mentions_only": "Només mencions", "Merge_Channels": "Combina Channels", - "message": "Missatge", + "message": "missatge", "Message": "Missatge", "Message_AllowBadWordsFilter": "Permet el filtratge de paraulotes", "Message_AllowConvertLongMessagesToAttachment": "Permetre la conversió dels missatges llargs en arxius adjunts", "Message_AllowDeleting": "Permet l'eliminació de missatges", "Message_AllowDeleting_BlockDeleteInMinutes": "Bloqueja l'eliminació de missatges després de (n) minuts", "Message_AllowDeleting_BlockDeleteInMinutes_Description": "Introdueix 0 per desactivar el bloqueig.", - "Message_AllowDirectMessagesToYourself": "Permetre missatges directes al propi usuari", + "Message_AllowDirectMessagesToYourself": "Permetre que els usuaris s'enviïn missatges directes a vostè mateix", "Message_AllowEditing": "Permet l'edició de missatges", "Message_AllowEditing_BlockEditInMinutes": "Bloqueja l'edició de missatges després de (n) minuts", - "Message_AllowEditing_BlockEditInMinutesDescription": "Introdueix 0 per desactivar el bloqueig.", + "Message_AllowEditing_BlockEditInMinutesDescription": "Introduïu 0 per desactivar el bloqueig.", "Message_AllowPinning": "Permet que es fixin missatges", "Message_AllowPinning_Description": "Permet que els missatges es puguin fixar a qualsevol canal.", "Message_AllowSnippeting": "Permet retalls de missatges (snippeting)", @@ -2841,7 +2864,7 @@ "Message_AlwaysSearchRegExp": "Sempre cercar utilitzant RegExp", "Message_AlwaysSearchRegExp_Description": "Recomanem activar-ho si el teu idioma no està suportat per la cerca de text MongoDB.", "Message_Attachments": "Adjunts al missatge", - "Message_Attachments_GroupAttach": "Agrupa els botons d'adjuntar", + "Message_Attachments_GroupAttach": "Grup de botons de arxius adjunts", "Message_Attachments_GroupAttachDescription": "Això uneix les icones en un menú desplegable. Ocupen menys espai a la pantalla.", "Message_Attachments_Thumbnails_Enabled": "Habiliteu les miniatures d'imatges per estalviar ample de banda", "Message_Attachments_Thumbnails_Width": "Ample màxim de la miniatura (en píxels)", @@ -2899,7 +2922,7 @@ "Message_HideType_subscription_role_added": "Ocultar els missatges de \"Rol establert\"", "Message_HideType_subscription_role_removed": "Ocultar els missatges \"Rol no definit\"", "Message_HideType_uj": "Amaga missatges \"Usuari unit\"", - "Message_HideType_ul": "Amaga missatges \"Usuari surt\"", + "Message_HideType_ul": "Amagar missatges de \"Sortida d'usuari\"", "Message_HideType_ut": "Ocultar els missatges de \"L'usuari es va unir a la conversa\"", "Message_HideType_wm": "Ocultar els missatges de \"Benvinguda\"", "Message_Id": "Identificador del missatge", @@ -2907,7 +2930,7 @@ "message-impersonate": "Fer-se passar per altres usuaris", "message-impersonate_description": "Permís per fer-se passar per altres usuaris utilitzant un àlies de missatge", "Message_info": "Informació del missatge", - "Message_KeepHistory": "Mantenir l'historial per missatge", + "Message_KeepHistory": "Mantingueu l'historial d'edició per missatge", "Message_MaxAll": "Mida màxima de Channel per a TOTS els missatges", "Message_MaxAllowedSize": "Caràcters màxims permesos per missatge", "Message_pinning": "Fixació de missatges", @@ -2929,7 +2952,7 @@ "Message_TimeFormat_Description": "Veure: Moment.js", "Message_too_long": "Missatge massa llarg", "Message_UserId": "ID d'usuari", - "Message_VideoRecorderEnabled": "Gravadora de vídeo activa", + "Message_VideoRecorderEnabled": "Gravadora de vídeo habilitat", "Message_VideoRecorderEnabledDescription": "Requereix que els fitxers de tipus 'video/webm' siguin admesos a la configuració de 'Puja fitxers'.", "Message_view_mode_info": "Això canvia l'espai que ocupen els missatges en pantalla.", "MessageBox_view_mode": "Mode de visualització de el panell de missatges", @@ -2949,7 +2972,7 @@ "meteor_status_connecting": "Connectant...", "meteor_status_failed": "La connexió del servidor ha fallat", "meteor_status_offline": "Mode fora de línia", - "meteor_status_reconnect_in": "provant de nou en un segon ...", + "meteor_status_reconnect_in": "intentant de nou en un segon ...", "meteor_status_reconnect_in_plural": "provant de nou d'aquí a __count__ segons ...", "meteor_status_try_now_offline": "Connectar de nou", "meteor_status_try_now_waiting": "Prova-ho ara", @@ -2965,12 +2988,14 @@ "Mobex_sms_gateway_from_number": "De", "Mobex_sms_gateway_from_number_desc": "Adreça / número de telèfon d'origen en enviar un nou SMS al client de LiveChat", "Mobex_sms_gateway_from_numbers_list": "Llista de números des d’on enviar SMS", - "Mobex_sms_gateway_from_numbers_list_desc": "Llista de números separats per comes per utilitzar per enviar missatges nous, per exemple. 123456789, 123456788, 123456888", + "Mobex_sms_gateway_from_numbers_list_desc": "Llista de números separats per comes per utilitzar en l'enviament de missatges nous, per exemple 123456789, 123456788, 123456888", "Mobex_sms_gateway_password": "Contrasenya", - "Mobex_sms_gateway_restful_address": "Adreça Mobex SMS REST API", + "Mobex_sms_gateway_restful_address": "Adreça de l'API REST de SMS de Mobex", "Mobex_sms_gateway_restful_address_desc": "IP o Host del seu Mobex REST API. Per exemple, `http://192.168.1.1:8080` o `https://www.example.com:8080`", "Mobex_sms_gateway_username": "Nom d'usuari", "Mobile": "Mòbil", + "mobile-download-file": "Permetre la descàrrega de fitxers en dispositius mòbils", + "mobile-upload-file": "Permetre la càrrega de fitxers en dispositius mòbils", "Mobile_Push_Notifications_Default_Alert": "Alerta per defecte notificacions mòbil", "Monday": "dilluns", "Mongo_storageEngine": "Motor d'emmagatzematge Mongo", @@ -2995,12 +3020,14 @@ "Msgs": "Missatges", "multi": "multi", "multi_line": "línia múltiple", + "Mute": "Silenciar", "Mute_all_notifications": "Silencia totes les notificacions", "Mute_Focused_Conversations": "Silenci converses enfocades", "Mute_Group_Mentions": "Silenci @all i @here mencions", "Mute_someone_in_room": "Silenciar algú a la sala", "Mute_user": "Silencia l'usuari", - "mute-user": "Silenciar usuari", + "Mute_microphone": "Silenciar micròfon", + "mute-user": "Usuari silenciat", "mute-user_description": "Permís per silenciar altres usuaris del mateix canal", "Muted": "Silenciat", "My Data": "Les meves dades", @@ -3083,7 +3110,7 @@ "No_previous_chat_found": "No s'ha trobat cap xat anterior", "No_results_found": "No s'han trobat resultats", "No_results_found_for": "No s'han trobat resultats per a:", - "No_snippet_messages": "Cap retall", + "No_snippet_messages": "Sense fragment", "No_starred_messages": "Cap missatge destacat.", "No_such_command": "Comanda `/__command__` no trobada.", "No_Threads": "No s'ha trobat cap fil", @@ -3099,7 +3126,7 @@ "Not_following": "No seguir", "Not_Following": "No seguir", "Not_found_or_not_allowed": "No trobat o no permès", - "Not_Imported_Messages_Title": "Els missatges següents no s’han importat correctament", + "Not_Imported_Messages_Title": "Els missatges següents no s'han importat correctament", "Not_in_channel": "No al canal", "Not_likely": "No es probable", "Not_started": "No iniciat", @@ -3110,9 +3137,9 @@ "Notification_Desktop_Default_For": "Mostra notificacions d'escriptori per", "Notification_Push_Default_For": "Notificacions mòbils push per", "Notification_RequireInteraction": "Requerir interacció per descartar la notificació d'escriptori", - "Notification_RequireInteraction_Description": "Funciona només amb versions de el navegador Chrome> 50. Utilitza el paràmetre requireInteraction per mostrar la notificació d'escriptori de forma indefinida fins que l'usuari interactuï amb ella.", + "Notification_RequireInteraction_Description": "Només funciona amb les versions del navegador Chrome> 50. Utilitza el paràmetre requireInteraction per mostrar la notificació d'escriptori de manera indefinida fins que l'usuari hi interactuï.", "Notifications": "Notificacions", - "Notifications_Max_Room_Members": "Nombre màxim de membres de la sala abans de desactivar totes les notificacions de missatges", + "Notifications_Max_Room_Members": "Nombre màxim de membres de la Room abans de desactivar totes les notificacions de missatges", "Notifications_Max_Room_Members_Description": "Nombre màxim de membres a la sala quan es desactiven les notificacions de tots els missatges. Els usuaris encara poden canviar la configuració de cada habitació per rebre totes les notificacions de forma individual. (0 per desactivar)", "Notifications_Muted_Description": "Si esculls silenciar-ho tot, no veuràs la sala destacada a la llista quan hi hagi nous missatges, excepte si són mencions. Silenciar les notificacions sobreescriurà les opcions de notificació.", "Notifications_Preferences": "Preferències de notificacions", @@ -3134,13 +3161,13 @@ "Number_of_federated_users": "Nombre d'usuaris federats", "Number_of_messages": "Nombre de missatges", "Number_of_most_recent_chats_estimate_wait_time": "Nombre d'xats recents per calcular el temps d'espera estimat", - "Number_of_most_recent_chats_estimate_wait_time_description": "Aquest número defineix el nombre d'últimes sales reservades que s'utilitzaran per calcular els temps d'espera de la cua.", + "Number_of_most_recent_chats_estimate_wait_time_description": "Aquest número defineix el nombre de les últimes sales ateses que es faran servir per calcular els temps d'espera de la cua.", "Number_of_users_autocomplete_suggestions": "Nombre de suggeriments d'emplenament dels usuaris", "OAuth Apps": "Aplicacions OAuth", "OAuth_Application": "Aplicació OAuth", "OAuth_Applications": "Aplicacions OAuth", "Objects": "Objectes", - "Off": "Desactiva", + "Off": "Desactivar", "Off_the_record_conversation": "Conversa fora de registre", "Off_the_record_conversation_is_not_available_for_your_browser_or_device": "La conversa sense registre no està disponible per al seu navegador o dispositiu", "Office_Hours": "Horari d'obertura", @@ -3157,7 +3184,7 @@ "Offline_message": "missatge fora de línia", "Offline_Message": "Missatge fora de línia", "Offline_Message_Use_DeepLink": "Utilitzeu el format d’URL d’enllaç profund", - "Offline_messages": "Missatges fora de línia", + "Offline_messages": "Missatges sense connexió", "Offline_success_message": "Missatge fora de línia correcte", "Offline_unavailable": "Fora de línia no disponible", "Ok": "D'acord", @@ -3167,6 +3194,8 @@ "Omnichannel": "LiveChat", "Omnichannel_Directory": "Directori de LiveChat", "Omnichannel_appearance": "Aparença de LiveChat", + "Omnichannel_calculate_dispatch_service_queue_statistics": "Calcular i enviar estadístiques de la cua d'espera Livechat", + "Omnichannel_calculate_dispatch_service_queue_statistics_Description": "Processar i enviar estadístiques de la cua despera, com la posició i el temps despera esperat. Si el * canal de xat en viu * no està en ús, es recomana desactivar aquesta configuració i evitar que el servidor realitzi processos innecessaris.", "Omnichannel_Contact_Center": "Centre de contacte LiveChat", "Omnichannel_contact_manager_routing": "Assignar nous converses a l'administrador de contactes", "Omnichannel_contact_manager_routing_Description": "Aquesta configuració assigna un xat a l'Administrador de contactes assignat, sempre que l'Administrador de contactes estigui en línia quan s'inicia el xat", @@ -3177,20 +3206,21 @@ "Omnichannel_External_Frame_URL": "URL de marc extern", "On": "Activa", "On_Hold_Chats": "En espera", + "On_Hold_conversations": "Converses en espera", "online": "en línia", "Online": "Connectat", "Only_authorized_users_can_write_new_messages": "Només els usuaris autoritzats poden escriure missatges nous", "Only_authorized_users_can_react_to_messages": "Només els usuaris autoritzats poden reaccionar als missatges", "Only_from_users": "Només elimini el contingut d'aquests usuaris (deixeu en blanc per eliminar el contingut de tots)", "Only_Members_Selected_Department_Can_View_Channel": "Només els membres de l'departament seleccionat poden veure els xats en aquest canal", - "Only_On_Desktop": "Mode ordinador d'escriptori (només envia amb Enter en ordinadors)", + "Only_On_Desktop": "Mode d'escriptori (només envia amb Enter a l'escriptori)", "Only_works_with_chrome_version_greater_50": "Funciona només amb versions de Google Chrome> 50", "Only_you_can_see_this_message": "Només tu pots veure aquest missatge", "Only_invited_users_can_acess_this_channel": "Només els usuaris convidats poden accedir a aquest Channel", "Oops_page_not_found": "Vaja, pàgina no trobada", "Oops!": "Ui!", "Open": "Obre", - "Open_channel_user_search": "`%s` - Obre canal / Cerca usuari", + "Open_channel_user_search": "`%s` - Obre Channell / Cerca usuari", "Open_conversations": "Converses obertes", "Open_Days": "Díes oberts", "Open_days_of_the_week": "Dies d'obertura", @@ -3211,10 +3241,10 @@ "Organization_Name": "Nom de l'Organització", "Organization_Type": "Tipus d'Organització", "Original": "Original", - "OS_Arch": "Arquitectura del sistema", + "OS_Arch": "Arquitectura del SO", "OS_Cpus": "Recompte de CPU", "OS_Freemem": "Memòria RAM lliure", - "OS_Loadavg": "Mitjanes de càrrega", + "OS_Loadavg": "Mitjana de Càrrega del SO", "OS_Platform": "Plataforma del SO", "OS_Release": "Versió del SO", "OS_Totalmem": "Memòria RAM total", @@ -3229,7 +3259,7 @@ "Outgoing_WebHook": "WebHook sortint", "Outgoing_WebHook_Description": "Extreu dades de Rocket.Chat en temps real.", "Output_format": "Format de sortida", - "Override_URL_to_which_files_are_uploaded_This_url_also_used_for_downloads_unless_a_CDN_is_given": "Sobreescriu l'adreça URL a la qual es pugen els arxius. Aquesta adreça també s'utilitza per a les descàrregues a menys que s'especifiqui un CDN", + "Override_URL_to_which_files_are_uploaded_This_url_also_used_for_downloads_unless_a_CDN_is_given": "Reemplaça la URL a la qual es carreguen els fitxers. Aquest URL també es fa servir per a baixades a no ser que es proporcioni un CDN", "Page_title": "Titol de la pàgina", "Page_URL": "Adreça URL de la pàgina", "Pages": "Pàgines", @@ -3273,18 +3303,18 @@ "PiwikAdditionalTrackers_Description": "Introduïu les URL addicionals de la pàgina web de Piwik i els SiteID en el següent format, si desitja rastrejar les mateixes dades en diferents llocs web: [ { \"trackerURL\" : \"https://my.piwik.domain2/\", \"siteId\" : 42 }, { \"trackerURL\" : \"https://my.piwik.domain3/\", \"siteId\" : 15 } ]", "PiwikAnalytics_cookieDomain": "Tots els subdominis", "PiwikAnalytics_cookieDomain_Description": "Segueix visitants per tots els subdominis", - "PiwikAnalytics_domains": "Amaga enllaços de sortida", + "PiwikAnalytics_domains": "Oculta els enllaços sortints", "PiwikAnalytics_domains_Description": "A l'informe \"Enllaços externs\", oculti els clics a URL d'àlies conegudes. Inseriu un domini per línia i no utilitzeu separadors.", "PiwikAnalytics_prependDomain": "Prefixa domini", - "PiwikAnalytics_prependDomain_Description": "Prefixa el domini del lloc al títol de la pàgina", - "PiwikAnalytics_siteId_Description": "L'ID de lloc a utilitzar per a la identificació d'aquest lloc. Exemple: 17", + "PiwikAnalytics_prependDomain_Description": "Anteposi el domini del lloc al títol de la pàgina quan faci el seguiment", + "PiwikAnalytics_siteId_Description": "La Identificació del lloc a utilitzar per a la identificació daquest lloc. Exemple: 17", "PiwikAnalytics_url_Description": "L'adreça URL on es troba el Piwik, assegureu-vos d'incloure la barra del final. Exemple: //piwik.rocket.chat/", "Placeholder_for_email_or_username_login_field": "Marcador de posició per al camp d'inici de sessió de correu electrònic o nom d'usuari", "Placeholder_for_password_login_confirm_field": "Confirma marcador de posició per al camp d'inici de sessió amb contrasenya", "Placeholder_for_password_login_field": "Marcador de posició per al camp d'inici de sessió amb contrasenya", "Please_add_a_comment": "Si us plau, afegeix un comentari", "Please_add_a_comment_to_close_the_room": "Si us plau, afegeix un comentari per tancar la sala", - "Please_answer_survey": "Si us plau, permeti'ns un moment per una breu enquesta sobre aquest xat", + "Please_answer_survey": "Si us plau preneu-vos un moment per respondre una breu enquesta sobre aquest xat", "Please_enter_usernames": "Sisplau, entra noms d'usuari...", "please_enter_valid_domain": "Si us plau introduiu un domini vàlid", "Please_enter_value_for_url": "Si us plau introdueix l'adreça URL del teu avatar.", @@ -3323,7 +3353,7 @@ "Presence": "Presència", "Preview": "Vista prèvia", "preview-c-room": "Previsualitzar canal públic", - "preview-c-room_description": "Permís per veure els continguts d'un canal públic abans d'unir-s'hi", + "preview-c-room_description": "PPermís per veure els continguts d´un canal públic abans d´unir-se", "Previous_month": "Mes anterior", "Previous_week": "Setmana anterior", "Priorities": "Prioritats", @@ -3372,10 +3402,10 @@ "Push_Notifications": "Notificaciones push", "Push_apn_cert": "Certificat APN", "Push_apn_dev_cert": "Certificat APN de desenvolupador (Dev)", - "Push_apn_dev_key": "Clau APN Dev (Key)", + "Push_apn_dev_key": "Clau de desenvolupament d'APN", "Push_apn_dev_passphrase": "Contrasenya APN Dev (Passphrase)", "Push_apn_key": "Clau APN (Key)", - "Push_apn_passphrase": "Contrasenya APN (Passphrase)", + "Push_apn_passphrase": "Frase de contrasenya d'APN", "Push_enable": "Activa", "Push_enable_gateway": "Activa porta d'enllaç", "Push_enable_gateway_Description": " Són els Ha d'acceptar per registrar el seu servidor (Assistent de configuració> Informació de l'organització> Registrar servidor) i els nostres termes de privacitat (Assistent de configuració> Informació del núvol> Acord de termes de privacitat de el servei en el núvol) per habilitar aquesta configuració i usar la nostra porta d'entrada. Fins i tot si aquesta configuració està activada, no funcionarà si el servidor no està registrat.", @@ -3384,7 +3414,7 @@ "Push_gcm_api_key": "Clau API GCM (Key)", "Push_gcm_project_number": "GCM Project Number", "Push_production": "Producció", - "Push_request_content_from_server": "Obtenir el contingut complet d'el missatge de servidor a l'rebre'l", + "Push_request_content_from_server": "Obtenir el contingut complet del missatge del servidor en rebre'l", "Push_Setting_Requires_Restart_Alert": "Canviar aquest valor requereix reiniciar Rocket.Chat.", "Push_show_message": "Mostra el missatge a la notificació", "Push_show_username_room": "Mostra Channel / grup / nom d'usuari en la notificació", @@ -3393,6 +3423,7 @@ "Query_description": "Condicions addicionals per a determinar a quins usuaris s'enviarà el missatge de correu-e. Els usuaris des-subscrits s'eliminen automàticament de la consulta. Ha de ser un objecte JSON vàlid. Exemple: \"{\"createdAt\":{\"$gt\":{\"$date\": \"2015-01-01T00:00:00.000Z\"}}}\"", "Query_is_not_valid_JSON": "La consulta no és JSON vàlid", "Queue": "Cua", + "Queue_delay_timeout": "Temps despera despera de processament de cua", "Queue_Time": "Temps de cua", "Queue_management": "Gestió de cues", "quote": "cita", @@ -3408,22 +3439,22 @@ "Read_by": "Llegit per", "Read_only": "Només lectura", "Read_only_changed_successfully": "Només lectura canviat correctament", - "Read_only_channel": "Canal de només lectura", + "Read_only_channel": "Channel Només lectura", "Read_only_group": "Grup de només lectura", "Real_Estate": "Béns arrels", "Real_Time_Monitoring": "Monitorització en temps real", "RealName_Change_Disabled": "El seu administrador de Rocket.Chat ha desactivat el canvi de noms", - "Reason_To_Join": "Raó per unir-se", + "Reason_To_Join": "Motiu per unir-se", "Receive_alerts": "Rebre alertes", "Receive_Group_Mentions": "Rebi mencions @all i @here", "Recent_Import_History": "Històric recent d'importació", "Record": "Gravar", "recording": "grabació", - "Redirect_URI": "URI de redireccionament (Redirect URI)", + "Redirect_URI": "URI de Redireccionament ", "Refresh": "Actualització", "Refresh_keys": "Refresca les claus", "Refresh_oauth_services": "Refresca serveis OAuth", - "Refresh_your_page_after_install_to_enable_screen_sharing": "Per poder compartir la pantalla refresqui la pàgina després de la instal·lació ", + "Refresh_your_page_after_install_to_enable_screen_sharing": "Actualitzar la pantalla després de la instal·lació per permetre compartir la pantalla", "Regenerate_codes": "Regenera codis", "Regexp_validation": "Validació per expressió regular", "Register": "Crea un compte nou", @@ -3453,7 +3484,7 @@ "Remove_Admin": "Treu admin", "Remove_as_leader": "Treure de líder", "Remove_as_moderator": "Treu de moderador", - "Remove_as_owner": "Treu de propietari", + "Remove_as_owner": "Eliminar com a propietari", "Remove_Channel_Links": "Eliminar enllaços de canals", "Remove_custom_oauth": "Esborra OAuth personalitzat", "Remove_from_room": "Treu-lo de la sala", @@ -3479,7 +3510,7 @@ "Reply_via_Email": "Respondre per correu electrònic", "ReplyTo": "Respondre a", "Report": "Reportar", - "Report_Abuse": "Informar d'un abús", + "Report_Abuse": "Reportar abús", "Report_exclamation_mark": "Informa!", "Report_sent": "Informe enviat", "Report_this_message_question_mark": "Informar d'aquest missatge?", @@ -3490,7 +3521,7 @@ "Request_more_seats_out_of_seats": "No podeu afegir membres perquè aquest espai de treball no té lloc, demani més llocs.", "Request_more_seats_sales_team": "Una vegada que enviï la seva sol·licitud, el nostre equip de vendes l'analitzarà i es comunicarà amb vostè en els pròxims dies.", "Request_more_seats_title": "Sol·licitar més llocs", - "Request_comment_when_closing_conversation": "Sol·licitar un comentari a l'tancar la conversa", + "Request_comment_when_closing_conversation": "Sol·licitar un comentari al tancar la conversa", "Request_comment_when_closing_conversation_description": "Si està activat, l'agent haurà de fer un comentari abans que es tanqui la conversa.", "Request_tag_before_closing_chat": "Sol·licitar etiqueta(es) abans de tancar la conversa", "Requested_At": "Sol·licitat en", @@ -3512,7 +3543,7 @@ "Responding": "Responent", "Response_description_post": "Els cossos buits o els cossos amb una propietat de text buida simplement seran ignorats. Les respostes que no siguin 200 es tornaran a intentar una quantitat raonable de vegades. Es publicarà una resposta amb l'àlies i l'avatar especificats anteriorment. Pot anul·lar aquesta informació com en l'exemple anterior.", "Response_description_pre": "Si el controlador desitja tornar a publicar una resposta al canal, el següent JSON s'ha de retornar com el cos de la resposta:", - "Restart": "Reinicia (restart)", + "Restart": "Reiniciar", "Restart_the_server": "Reinicia el servidor", "Retail": "Venda al detall", "Retention_setting_changed_successfully": "La configuració de la política de retenció s'ha canviat correctament", @@ -3539,15 +3570,15 @@ "RetentionPolicy_Precision": "Precisió del temporitzador", "RetentionPolicy_Precision_Description": "Amb quina freqüència ha de funcionar el comptador de poda. Establir això en un valor més precís fa que els canals amb temporitzadors de retenció ràpids funcionin millor, però podria costar potència de processament addicional en comunitats grans.", "RetentionPolicy_RoomWarning": "Els missatges anteriors a __time__ s'eliminen automàticament aquí", - "RetentionPolicy_RoomWarning_FilesOnly": "Els arxius anteriors a __time__ s'eliminaran automàticament aquí (els missatges romanen intactes)", + "RetentionPolicy_RoomWarning_FilesOnly": "Els fitxers anteriors a __time__ s'eliminen automàticament aquí (els missatges romanen intactes)", "RetentionPolicy_RoomWarning_Unpinned": "Els missatges no fixats anteriors a __time__ s'eliminaran automàticament aquí", - "RetentionPolicy_RoomWarning_UnpinnedFilesOnly": "Els arxius no fixats anteriors a __time__ s'eliminaran automàticament aquí (els missatges romanen intactes)", + "RetentionPolicy_RoomWarning_UnpinnedFilesOnly": "Els fitxers no fixats anteriors a __time__ s'eliminen automàticament aquí (els missatges romanen intactes)", "RetentionPolicyRoom_Enabled": "Esborrar missatges antics automàticament", "RetentionPolicyRoom_ExcludePinned": "Exclou els missatges fixats", "RetentionPolicyRoom_FilesOnly": "Esborri només arxius, mantingui missatges", "RetentionPolicyRoom_MaxAge": "Antiguitat màxima de l'missatge en dies (per defecte: __max__)", "RetentionPolicyRoom_OverrideGlobal": "Anul·lar la política de retenció global", - "RetentionPolicyRoom_ReadTheDocs": "Compte! Ajustar aquestes configuracions sense la major cura pot destruir tot l'historial de missatges. Llegiu la documentació abans d'activar la funció aquí .", + "RetentionPolicyRoom_ReadTheDocs": "Compte! Ajustar aquestes configuracions sense tenir més cura pot destruir tot l'historial de missatges. Llegiu la documentació abans d'activar la funció aquí .", "Retry": "processar de nou", "Retry_Count": "Comptador de reintents", "Return_to_home": "Tornar a inici", @@ -3577,9 +3608,9 @@ "Room_archivation_state_true": "Arxivada", "Room_archived": "Sala arxivada", "room_changed_announcement": "L'anunci de la sala s'ha canviat a: __room_announcement__ per __user_by__", - "room_changed_avatar": "Avatar de la sala canviat per __user_by__ ", + "room_changed_avatar": "Avatar de Room canviat per __user_by__ ", "room_changed_description": "Descripció de la sala canviada a: __room_description__ per __user_by__.", - "room_changed_privacy": "Tipus de sala canviat a: __room_type__ per __user_by__.", + "room_changed_privacy": "Tipus de Room canviat a: __room_type__ per __user_by__.", "room_changed_topic": "Tema de la sala canviat a: __room_topic__ per __user_by__.", "Room_default_change_to_private_will_be_default_no_more": "Aquest és un canal per defecte i canviar-lo a grup privat farà que deixi de ser-ho. Voleu continuar?", "Room_description_changed_successfully": "Descripció de la sala canviada correctament", @@ -3588,7 +3619,7 @@ "Room_has_been_archived": "La sala s'ha arxivat", "Room_has_been_deleted": "La sala s'ha eliminat", "Room_has_been_removed": "Room ha estat eliminat", - "Room_has_been_unarchived": "La sala s'ha desarxivat", + "Room_has_been_unarchived": "La Room s'ha desarxivat", "Room_Info": "Informació de la Room", "room_is_blocked": "Aquesta sala està bloquejada", "room_account_deactivated": "Aquest compte està desactivat", @@ -3603,7 +3634,7 @@ "Room_tokenpass_config_changed_successfully": "La configuració tokenpass de la sala canviada amb èxit", "Room_topic_changed_successfully": "El tema de la sala s'ha canviat correctament", "Room_type_changed_successfully": "El tipus de sala s'ha canviat correctament", - "Room_type_of_default_rooms_cant_be_changed": "Aquesta és una sala per defecte i no es pot canviar el tipus, si us plau consulta-ho amb l'administrador.", + "Room_type_of_default_rooms_cant_be_changed": "Aquesta és una sala per defecte i el tipus no es pot canviar, consulteu amb el vostre administrador.", "Room_unarchived": "La sala s'ha desarxivat", "Room_updated_successfully": "Rooms'ha actualitzat correctament.", "Room_uploaded_file_list": "Llista d'arxius pujats", @@ -3634,7 +3665,7 @@ "SAML_Custom_Authn_Context_Comparison": "Comparació del context d’Authn", "SAML_Custom_Authn_Context_description": "Deixi això buit per ometre el context d'autenticació de la sol·licitud. \n\nPer afegir múltiples contextos d'autenticació, afegiu els addicionals directament a la configuració __AuthnContext Template__.", "SAML_Custom_Cert": "Certificat personalitzat", - "SAML_Custom_Debug": "Activa la depuració", + "SAML_Custom_Debug": "Activar la depuració", "SAML_Custom_EMail_Field": "Nom del camp de correu electrònic", "SAML_Custom_Entry_point": "Punt d'entrada (Entry Point) personalitzat", "SAML_Custom_Generate_Username": "Generar nom d'usuari", @@ -3653,7 +3684,7 @@ "SAML_Custom_Public_Cert": "Contingut del certificat públic", "SAML_Custom_signature_validation_all": "Valida totes les signatures", "SAML_Custom_signature_validation_assertion": "Validar la signatura d'asserció", - "SAML_Custom_signature_validation_either": "Validar qualsevol signatura", + "SAML_Custom_signature_validation_either": "Validar qualsevol de les firmes", "SAML_Custom_signature_validation_response": "Validar la signatura de resposta", "SAML_Custom_signature_validation_type": "Tipus de validació de signatura", "SAML_Custom_signature_validation_type_description": "Aquesta configuració s'ignorarà si no es proporciona un certificat personalitzat.", @@ -3678,9 +3709,9 @@ "SAML_Metadata_Template_Description": "Les següents variables estan disponibles:\n - ** \\ _ \\ _ sloLocation \\ _ \\ _ **: L'URL de tancament de sessió simple de Rocket.Chat\n- ** \\ _ \\ _ issuer \\ _ \\ _ **: The value of the __Custom Issuer__ setting.\n- ** \\ _ \\ _ identifierFormat \\ _ \\ _ **: el valor de l'opció __Identifier Format __\n- ** \\ _ \\ _ certificateTag \\ _ \\ _ **: Si un certificat privat és configurat, això inclourà el __Metadata Certificate Template__, en cas contrari serà ignorado.\n- ** \\ _ \\ _ callbackUrl \\ _ \\ _ **: L'URL de crida de Rocket.Chat", "SAML_MetadataCertificate_Template": "Plantilla de certificat de metadades", "SAML_NameIdPolicy_Template": "Plantilla de política NameID", - "SAML_NameIdPolicy_Template_Description": "Podeu utilitzar qualsevol variable de la plantilla de sol·licitud d'autorització aquí.", + "SAML_NameIdPolicy_Template_Description": "Podeu utilitzar qualsevol variable de la Plantilla de sol·licitud d'autorització aquí.", "SAML_Role_Attribute_Name": "Nom de l'atribut de rol", - "SAML_Role_Attribute_Name_Description": "Si aquest atribut es troba a la resposta SAML, els seus valors s'utilitzaran com a noms de rol per als usuaris nous.", + "SAML_Role_Attribute_Name_Description": "Si aquest atribut es troba a la resposta SAML, els vostres valors es faran servir com a noms de rols per a nous usuaris.", "SAML_Role_Attribute_Sync": "Sincronitza els rols de l'usuari", "SAML_Role_Attribute_Sync_Description": "Sincronitzeu els rols d'usuari de SAML a l'iniciar sessió (sobreescriu els rols d'usuari local).", "SAML_Section_1_User_Interface": "Interfície d'usuari", @@ -3764,6 +3795,7 @@ "Send_invitation_email_error": "No s'ha proporcionat cap adreça de correu electrònic vàlida.", "Send_invitation_email_info": "Es poden enviar múltiples invitacions per correu electrònic a la vegada.", "Send_invitation_email_success": "S'han enviat amb èxit invitacions per correu-e a les següents adreces:", + "Send_it_as_attachment_instead_question": "Envieu-lo com a fitxer adjunt?", "Send_me_the_code_again": "Enviem el codi de nou", "Send_request_on": "Envia la sol·licitud en", "Send_request_on_agent_message": "Enviar sol·licitud en missatges d'agent", @@ -3803,9 +3835,9 @@ "Set_as_favorite": "Establir com a favorit", "Set_as_leader": "Posar com a líder", "Set_as_moderator": "Fes-lo moderador", - "Set_as_owner": "Fes-lo propietari", + "Set_as_owner": "Establir com a propietari", "Set_random_password_and_send_by_email": "Establir una contrasenya aleatòria i envieu-la per correu electrònic", - "set-leader": "Líder d'establiment", + "set-leader": "Establir com a líder", "set-leader_description": "Permís per establir a altres usuaris com a líders d'un canal", "set-moderator": "Assignar moderador", "set-moderator_description": "Permís per assignar altres usuaris com a moderadors d'un canal", @@ -3817,9 +3849,10 @@ "set-readonly_description": "Permís per fer un canal de només lectura", "Settings": "Configuració", "Settings_updated": "S'ha actualitzat la configuració", - "Setup_Wizard": "Ajudant de configuració", + "Setup_Wizard": "Assistent de configuració", "Setup_Wizard_Info": "El guiarem per configurar el seu primer usuari administrador, configurar la seva organització i registrar el seu servidor per rebre notificacions push gratuïtes i més.", "Share_Location_Title": "Compartir localització?", + "Share_screen": "Compartir pantalla", "New_CannedResponse": "Nova resposta preparada", "Edit_CannedResponse": "Edita la resposta predefinida", "Sharing": "Intercanvi", @@ -3830,13 +3863,13 @@ "Should_be_a_URL_of_an_image": "Ha de ser l'adreça URL d'una imatge.", "Should_exists_a_user_with_this_username": "Aquest usuari ja deu existir.", "Show_agent_email": "Mostra el correu electrònic de l'agent", - "Show_agent_info": "Mostra la informació de l'agent", + "Show_agent_info": "Mostra informació de l'agent", "Show_all": "Veure tots", "Show_Avatars": "Mostra Avatars", "Show_counter": "Mostra comptador", "Show_email_field": "Mostra el camp de correu electrònic", "Show_Message_In_Main_Thread": "Mostra els missatges del fil al fil principal", - "Show_more": "Veure més", + "Show_more": "Mostrar més", "Show_name_field": "Mostra el camp del nom", "show_offline_users": "Mostra els usuaris desconnectats", "Show_on_offline_page": "Mostra a la pàgina fora de línia", @@ -3847,6 +3880,7 @@ "Show_room_counter_on_sidebar": "Mostra un comptador de sales a la barra lateral", "Show_Setup_Wizard": "Mostra l'assistent de configuració", "Show_the_keyboard_shortcut_list": "Mostra la llista de dreceres de teclat", + "Show_video": "Veure vídeo", "Showing_archived_results": "

      Mostrant %s resultats arxivats

      ", "Showing_online_users": "Mostrant-ne __total_showing__, En línia: __online__, Total: __total__ usuaris", "Showing_results": "

      Mostrant %s resultats

      ", @@ -3862,9 +3896,9 @@ "Skip": "Salta", "Slack_Users": "CSV d'usuaris de Slack", "SlackBridge_APIToken": "API Tokens", - "SlackBridge_APIToken_Description": "Podeu configurar diversos servidors slack afegint un símbol API per línia.", + "SlackBridge_APIToken_Description": "Podeu configurar diversos servidors slack afegint un token d'API per línia.", "Slackbridge_channel_links_removed_successfully": "Els enllaços de canal de Slackbridge s'han eliminat correctament.", - "SlackBridge_error": "Hi ha hagut un error a SlackBridge mentre importava els missatges a %s: %s", + "SlackBridge_error": "SlackBridge va rebre un error en importar els seus missatges a %s:%s", "SlackBridge_finish": "SlackBridge ha finalitat la importació a %s. Si us plau, refresqueu per veure tots els missatges.", "SlackBridge_Out_All": "SlackBridge Out de tot", "SlackBridge_Out_All_Description": "Envia els missatges de tots els canals que existeixen a Slack i en els quals el bot s'ha unit", @@ -3886,16 +3920,16 @@ "Smarsh_Email": "Correu-e Smarsh", "Smarsh_Email_Description": "Adreça de correu-e Smarsh on enviar l'arxiu .eml.", "Smarsh_Enabled": "Smarsh actiu", - "Smarsh_Enabled_Description": "Activa o no el connector Smarsh eml (requereix el camp 'De' ple a Correu-e -> SMTP).", + "Smarsh_Enabled_Description": "Si el connector eml de Smarsh està habilitat o no (cal completar 'Des de correu electrònic' a Correu electrònic -> SMTP).", "Smarsh_Interval": "Interval Smarsh", - "Smarsh_Interval_Description": "Temps a esperar abans d'enviar els xats (requereix el camp 'De' ple a Correu-e -> SMTP).", - "Smarsh_MissingEmail_Email": "Adreça de correu-e desconeguda", - "Smarsh_MissingEmail_Email_Description": "Adreça de correu-e a mostrar per a un usuari quan no té cap adreça establerta. Normalment passa en els comptes de bots.", + "Smarsh_Interval_Description": "La quantitat de temps d'espera abans d'enviar els xats (cal completar 'Des de correu electrònic' a Correu electrònic -> SMTP).", + "Smarsh_MissingEmail_Email": "Falta correu electrònic", + "Smarsh_MissingEmail_Email_Description": "El correu electrònic que es mostra per a un compte d'usuari quan falta la vostra adreça de correu electrònic, generalment passa amb els comptes de bot.", "Smarsh_Timezone": "Zona horària Smarsh", "Smileys_and_People": "Emoticones i persones", "SMS": "SMS", "SMS_Default_Omnichannel_Department": "Departament de LiveChat (per defecte)", - "SMS_Default_Omnichannel_Department_Description": "Si s'estableix, tots els nous xats entrants iniciats per aquesta integració es redirigiran a aquest departament.", + "SMS_Default_Omnichannel_Department_Description": "Si s'estableix, tots els nous xats entrants iniciats per aquesta integració s'encaminaran a aquest departament. \nAquesta configuració es pot sobreescriure passant el paràmetre de consulta del departament a la sol·licitud.\nEx. https: // / api / v1 / livechat / sms-entrante / twilio? department = .\nNota: si utilitzeu el nom del departament, aleshores hauria de ser URL segur.", "SMS_Enabled": "Activa SMS", "SMTP": "SMTP", "SMTP_Host": "Host SMTP", @@ -3920,7 +3954,7 @@ "Star": "Iniciar", "Star_Message": "Destacar un missatge", "Starred_Messages": "Missatges destacats", - "Start": "Inicia", + "Start": "Iniciar", "Start_audio_call": "Inicia trucada", "Start_Chat": "Inicia el xat", "Start_of_conversation": "Inici de la conversa", @@ -3931,7 +3965,7 @@ "start-discussion": "Iniciar discussió", "start-discussion_description": "Permís per iniciar una discussió", "start-discussion-other-user": "Inicia la discussió (Un altre usuari)", - "start-discussion-other-user_description": "Permís per iniciar una discussió, que dóna permís a l'usuari per crear una discussió a partir d'un missatge enviat també per un altre usuari", + "start-discussion-other-user_description": "Permís per iniciar una discussió, que us dóna permís a l'usuari per crear una discussió a partir d'un missatge enviat per un altre usuari també.", "Started": "Començat", "Started_a_video_call": "Inicia una videoconferència", "Started_At": "Va començar a les", @@ -3961,7 +3995,7 @@ "Stats_Total_Livechat_Rooms": "Total de Rooms LiveChat", "Stats_Total_Messages": "Total de missatges", "Stats_Total_Messages_Channel": "Total de missatges a canals", - "Stats_Total_Messages_Direct": "Total de missatges a missatges directes", + "Stats_Total_Messages_Direct": "Total de missatges en missatges directes", "Stats_Total_Messages_Livechat": "Total de missatges a LiveChat", "Stats_Total_Messages_PrivateGroup": "Total de missatges a grups privats", "Stats_Total_Outgoing_Integrations": "Integracions sortints totals", @@ -3972,14 +4006,15 @@ "Stats_Total_Users": "Total d'usuaris", "Status": "Estat", "StatusMessage": "Missatge d'estat", - "StatusMessage_Change_Disabled": "El seu administrador de Rocket.Chat ha desactivat el canvi de missatges d'estat", + "StatusMessage_Change_Disabled": "El vostre administrador de Rocket.Chat ha desactivat el canvi de missatges d'estat", "StatusMessage_Changed_Successfully": "El missatge d'estat va canviar correctament.", "StatusMessage_Placeholder": "Què estàs fent en aquest moment?", "StatusMessage_Too_Long": "El missatge d'estat ha de tenir menys de 120 caràcters.", "Step": "Pas", + "Stop_call": "Aturar trucada", "Stop_Recording": "Atura gravació", "Store_Last_Message": "Desar l'últim missatge", - "Store_Last_Message_Sent_per_Room": "Desar l'últim missatge enviat a cada sala.", + "Store_Last_Message_Sent_per_Room": "Emmagatzemar el darrer missatge enviat a cada sala.", "Stream_Cast": "Stream Cast", "Stream_Cast_Address": "Adreça Stream Cast", "Stream_Cast_Address_Description": "IP o host del Stream Cast del teu Rocket.Chat central. Exemple: `192.168.1.1:3000` o `localhost:4000`.", @@ -3991,7 +4026,7 @@ "Success_message": "Missatge correcte", "Successfully_downloaded_file_from_external_URL_should_start_preparing_soon": "L'arxiu descarregat correctament des d'una URL externa hauria de començar a preparar-aviat", "Suggestion_from_recent_messages": "Suggeriment de missatges recents", - "Sunday": "diumenge", + "Sunday": "Diumenge", "Support": "Suport", "Survey": "Enquesta", "Survey_instructions": "Valoreu cada pregunta d'acord al nivell de satisfacció, sent 1 completament insatisfet i 5 completament satisfet.", @@ -4001,17 +4036,17 @@ "Sync_in_progress": "Sincronització en progrés", "Sync_Interval": "Interval de sincronització", "Sync_success": "Sincronització correcta", - "Sync_Users": "Sincronitza usuaris", + "Sync_Users": "Sincronitzar usuaris", "System_messages": "Missatges del sistema", "Tag": "Etiqueta", "Tags": "Etiquetes", - "Tag_removed": "Etiqueta suprimida", + "Tag_removed": "Etiqueta eliminada", "Tag_already_exists": "La etiqueta ya existe", "Take_it": "Agafa'l!", "Taken_at": "Pres en", "Target user not allowed to receive messages": "L'usuari objectiu no té permís per rebre missatges", "TargetRoom": "Sala de destí", - "TargetRoom_Description": "Sala que rebrà els missatges resultants de les execucions d'aquest esdeveniment. Només es permet una sala de destí i aquesta ha d'existir.", + "TargetRoom_Description": "La sala on s'enviaran els missatges que són el resultat de l'activació d'aquest esdeveniment. Només es permet una sala de destinació i hi ha d'haver.", "Team_Add_existing_channels": "Afegeix Channels existents", "Team_Add_existing": "Afegeix existents", "Team_Auto-join": "Unir-se automàticament", @@ -4081,8 +4116,8 @@ "Test_LDAP_Search": "Provar de cerca LDAP", "Texts": "Textos", "Thank_you_exclamation_mark": "Gràcies!", - "Thank_you_for_your_feedback": "Gràcies per la seva col·laboració", - "The_application_name_is_required": "Es requereix el nom de l'aplicació", + "Thank_you_for_your_feedback": "Gràcies pels seus comentaris", + "The_application_name_is_required": "El nom de laplicació és obligatori.", "The_channel_name_is_required": "Es requereix el nom del canal", "The_emails_are_being_sent": "Els missatges de correu-e s'estan enviant.", "The_empty_room__roomName__will_be_removed_automatically": "La sala buida __roomName__ s'eliminarà automàticament.", @@ -4100,7 +4135,7 @@ "The_user_s_will_be_removed_from_role_s": "L'usuari% s serà eliminat de el rol% s", "The_user_will_be_removed_from_s": "L'usuari s'eliminarà de %s", "The_user_wont_be_able_to_type_in_s": "L'usuari no podrà escriure a %s", - "Theme": "Aparença", + "Theme": "Tema", "theme-color-attention-color": "Color d'atenció", "theme-color-component-color": "Color de component", "theme-color-content-background-color": "Color del fons del contingut", @@ -4147,7 +4182,7 @@ "theme-color-status-busy": "Color de l'estat ocupat", "theme-color-status-offline": "Color de l'estat desconnectat", "theme-color-status-online": "Color de l'estat connectat", - "theme-color-success-color": "Color de 'Correcte'", + "theme-color-success-color": "Color d'èxit", "theme-color-tertiary-font-color": "Color terciari del text", "theme-color-transparent-dark": "Transparent fosc", "theme-color-transparent-darker": "Transparent més fosc", @@ -4185,7 +4220,7 @@ "thread": "fil", "Thread_message": "Comentat al missatge de *__username__'s* missatge: _ __msg__ _", "Threads": "Fils", - "Thursday": "dijous", + "Thursday": "Dijous", "Time_in_minutes": "Temps en minuts", "Time_in_seconds": "Temps en segons", "Timeout": "Temps d'espera", @@ -4201,11 +4236,11 @@ "to_see_more_details_on_how_to_integrate": "per a veure més detalls sobre com fer la integració.", "To_users": "Per als usuaris", "Today": "Avui", - "Toggle_original_translated": "Canvia original/traducció", + "Toggle_original_translated": "Alternar original / traduït", "toggle-room-e2e-encryption": "Alternar xifrat Room E2E", "toggle-room-e2e-encryption_description": "Permís per alternar la sala de xifrat e2e", "Token": "Token", - "Token_Access": "Accés de token", + "Token_Access": "Token d'accés", "Token_Controlled_Access": "Accés controlat per tokens", "Token_required": "El token és obligatori", "Tokenpass_Channel_Label": "Canal Tokenpass", @@ -4221,7 +4256,7 @@ "Total": "Total", "Total_abandoned_chats": "Total de xats abandonats", "Total_conversations": "Total devconverses", - "Total_Discussions": "Total dicussions", + "Total_Discussions": "Discussions totals", "Total_messages": "Total de missatges", "Total_Threads": "Totals de Fils", "Total_visitors": "Total de visitants", @@ -4233,7 +4268,7 @@ "totp-required": "Es requereix TOTP", "Transcript": "Transcripció", "Transcript_Enabled": "Pregunti a l'visitant si els agradaria una transcripció després de xat tancat", - "Transcript_message": "Missatge per mostrar a l'preguntar sobre la transcripció", + "Transcript_message": "Missatge per mostrar en preguntar sobre la transcripció", "Transcript_of_your_livechat_conversation": "Transcripció de la seva conversa de LiveChat.", "Transcript_Request": "Sol·licitud de transcripció", "transfer-livechat-guest": "Transferir convidats de Livechat", @@ -4251,16 +4286,16 @@ "Troubleshoot_Disable_Data_Exporter_Processor": "Desactiva el processador de l'exportador de dades", "Troubleshoot_Disable_Data_Exporter_Processor_Alert": "Aquesta configuració deté el processament de totes les sol·licituds d'exportació dels usuaris, de manera que no rebran l'enllaç per descarregar les seves dades.", "Troubleshoot_Disable_Instance_Broadcast": "Desactiva la retransmissió d'instàncies", - "Troubleshoot_Disable_Instance_Broadcast_Alert": "Aquesta configuració evita que les instàncies de Rocket.Chat enviïn esdeveniments a les altres instàncies, 'pot causar problemes de sincronització i mal comportament!", + "Troubleshoot_Disable_Instance_Broadcast_Alert": "Aquesta configuració evita que les instàncies de Rocket.Chat enviïn esdeveniments a les altres instàncies, pot causar problemes de sincronització i mal comportament!", "Troubleshoot_Disable_Livechat_Activity_Monitor": "Desactiva el monitor d'activitats de Livechat", "Troubleshoot_Disable_Livechat_Activity_Monitor_Alert": "Aquesta configuració deté el processament de les sessions de visita de l'LiveChat causant que les estadístiques deixin de funcionar!", "Troubleshoot_Disable_Notifications": "Desactiva notificacions", "Troubleshoot_Disable_Notifications_Alert": "Aquesta configuració desactiva per complet el sistema de notificacions; 'Els sons, les notificacions d'escriptori, les notificacions mòbils i els correus electrònics s'aturaran!", "Troubleshoot_Disable_Presence_Broadcast": "Desactiva la transmissió de presència", - "Troubleshoot_Disable_Presence_Broadcast_Alert": "Aquesta configuració evita que totes les instàncies enviïn els canvis d'estat dels usuaris als seus clients, mantenint a tots els usuaris amb el seu estat de presència des de la primera càrrega!", + "Troubleshoot_Disable_Presence_Broadcast_Alert": "Aquesta configuració evita que totes les instàncies enviïn els canvis d'estat dels usuaris als clients, mantenint tots els usuaris amb el seu estat de presència des de la primera càrrega!", "Troubleshoot_Disable_Sessions_Monitor": "Desactiva el monitor de sessions", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Aquesta configuració deté el processament de les sessions de visita de l'LiveChat causant que les estadístiques deixin de funcionar!", - "Troubleshoot_Disable_Statistics_Generator": "Desactiva el generador d'estadístiques", + "Troubleshoot_Disable_Statistics_Generator": "Desactivar el generador d'estadístiques", "Troubleshoot_Disable_Statistics_Generator_Alert": "Aquest ajust deté el processament de totes les estadístiques fent que la pàgina d'informació quedi desactualitzada fins que algú faci clic al botó d'actualització i pot causar que falti altra informació en el sistema!", "Troubleshoot_Disable_Workspace_Sync": "Desactiva la sincronització de l'espai de treball", "Troubleshoot_Disable_Workspace_Sync_Alert": "¡Este ajuste detiene la sincronización de este servidor con la nube de Rocket.Chat y puede causar problemas con el mercado y las licencias de las empresas!", @@ -4268,8 +4303,10 @@ "Tuesday": "dimarts", "Turn_OFF": "Apagar", "Turn_ON": "ACTIVA", + "Turn_on_video": "Activar el vídeo", + "Turn_off_video": "Desactivar el vídeo", "Two Factor Authentication": "Autenticació de dos factors", - "Two-factor_authentication": "Autenticació de dos factors mitjançant TOTP", + "Two-factor_authentication": "Autenticació de dos factors a través de TOTP", "Two-factor_authentication_disabled": "Autenticació de dos factors desactivada", "Two-factor_authentication_email": "Autenticació de dos factors via correu electrònic", "Two-factor_authentication_email_is_currently_disabled": "L'autenticació en 2 passos via correu electrònic està inhabilitada", @@ -4280,21 +4317,21 @@ "typing": "escrivint", "Types": "Tipus", "Types_and_Distribution": "Tipus i distribució", - "Type_your_email": "Escrigui el seu correu electrònic", - "Type_your_job_title": "Escriu el títol del lloc de treball", + "Type_your_email": "Escriviu el vostre correu electrònic", + "Type_your_job_title": "Escriviu el vostre títol de treball", "Type_your_message": "Introduïu el missatge", "Type_your_name": "Escriu el teu nom", - "Type_your_new_password": "Escriu la nova contrasenya", + "Type_your_new_password": "Escriviu la nova contrasenya", "Type_your_password": "Escriviu la vostra contrasenya", "Type_your_username": "Escriviu el vostre nom d'usuari", "UI_Allow_room_names_with_special_chars": "Permetre caràcters especials en noms de sales", - "UI_Click_Direct_Message": "Clica per crear un missatge directe", + "UI_Click_Direct_Message": "Feu clic per crear un missatge directe", "UI_Click_Direct_Message_Description": "Evita obrir la pestanya del perfil, vés directe a la conversa", "UI_DisplayRoles": "Mostra rols", "UI_Group_Channels_By_Type": "Agrupar canals per tipus", - "UI_Merge_Channels_Groups": "Uneix grups privats amb canals", + "UI_Merge_Channels_Groups": "Uneix grups privats amb Channels", "UI_Show_top_navbar_embedded_layout": "Mostra la barra de navegació superior al disseny incrustat", - "UI_Unread_Counter_Style": "Estil de comptador de no-llegits", + "UI_Unread_Counter_Style": "Estil de comptador no llegit", "UI_Use_Name_Avatar": "Utilitzeu les inicials del nom complet per generar un avatar predeterminat", "UI_Use_Real_Name": "Utilitza el nom real", "unable-to-get-file": "No es pot obtenir l'arxiu", @@ -4302,7 +4339,7 @@ "unarchive-room": "Desarxivar sala", "unarchive-room_description": "Permís per desarxivar canals", "Unavailable": "No disponible", - "Unblock_User": "Desbloqueja usuari", + "Unblock_User": "Desbloquejar usuari", "Uncheck_All": "Desmarcar tot", "Uncollapse": "Desplegar", "Undefined": "No definit", @@ -4313,21 +4350,23 @@ "Unit_removed": "Unitat eliminada", "Unknown_Import_State": "Estat d'importació desconegut", "Unlimited": "Il·limitat", + "Unmute": "Activar so", "Unmute_someone_in_room": "Torna a donar veu a algú de la sala", "Unmute_user": "Dóna veu a l'usuari", "Unnamed": "Sense nom", "Unpin": "Treure els fixats", "Unpin_Message": "Desfixa el missatge", "unpinning-not-allowed": "No permet treure els fixats", - "Unread": "No llegits", - "Unread_Count": "Comptador de no llegits", - "Unread_Count_DM": "Comptador de no-llegits per als missatges directes", + "Unread": "No llegit", + "Unread_Count": "Recompte de no llegits", + "Unread_Count_DM": "Recompte de missatges no llegits per a missatges directes", "Unread_Messages": "Missatges no llegits", "Unread_on_top": "No s'ha llegit a la part superior", "Unread_Rooms": "Sales no llegides", "Unread_Rooms_Mode": "Mode de sales no llegides", "Unread_Tray_Icon_Alert": "Icona d'alerta de no llegits a la safata", "Unstar_Message": "Esborra el destacat", + "Unmute_microphone": "Activar so del micròfon", "Update": "Actualització", "Update_EnableChecker": "Habilitar el Update Checker", "Update_EnableChecker_Description": "Comprova automàticament si hi ha noves actualitzacions / missatges importants dels desenvolupadors de Rocket.Chat i rep notificacions quan estan disponibles. La notificació apareix una vegada per nova versió com un banner en què es pot fer clic i com un missatge de el bot Rocket.Cat, tots dos visibles només per als administradors.", @@ -4335,7 +4374,7 @@ "Update_LatestAvailableVersion": "Actualitza la darrera versió disponible", "Update_to_version": "Actualitzar a __version__", "Update_your_RocketChat": "Actualitza el teu Rocket.Chat", - "Updated_at": "Actualitzat el", + "Updated_at": "Actualitzat a", "Upload": "Pujar", "Uploads": "Càrregues", "Upload_app": "Pujar l'Aplicació", @@ -4362,11 +4401,11 @@ "Use_Room_configuration": "Sobreescriu la configuració del servidor i utilitza la configuració de sala", "Use_Server_configuration": "Utilitzeu la configuració del servidor", "Use_service_avatar": "Utilitza l'avatar de %s", - "Use_this_response": "Utilitzeu aquesta resposta", + "Use_this_response": "Fes servir aquesta resposta", "Use_response": "Utilitzeu la resposta", "Use_this_username": "Utilitza aquest nom d'usuari", "Use_uploaded_avatar": "Utilitza l'avatar pujat", - "Use_url_for_avatar": "Utilitzeu l'URL per a l'avatar", + "Use_url_for_avatar": "Usar URL per a avatar", "Use_User_Preferences_or_Global_Settings": "Usa les preferències d'usuari o la configuració global", "User": "Usuari", "User Search": "Cerca d'usuaris", @@ -4375,7 +4414,7 @@ "User__username__is_now_a_moderator_of__room_name_": "L'usuari __username__ ara és moderador de la sala __room_name__", "User__username__is_now_a_owner_of__room_name_": "L'usuari __username__ ara és un propietari de __room_name__", "User__username__muted_in_room__roomName__": "Usuari __username__ silenciat a la sala __roomName__", - "User__username__removed_from__room_name__leaders": "L'usuari __username__ ja no és líder de __room_name__", + "User__username__removed_from__room_name__leaders": "L'usuari __username__ va ser remogut dels líders de __room_name__", "User__username__removed_from__room_name__moderators": "L'usuari __username__ ja no és moderador de la sala __room_name__", "User__username__removed_from__room_name__owners": "L'usuari __username__ ja no és propietari de __room_name__", "User__username__unmuted_in_room__roomName__": "Usuari __username__ sense silenciar a la sala __roomName__", @@ -4438,7 +4477,7 @@ "User_uploaded_a_file_to_you": "__username__ us ha enviat un fitxer", "User_uploaded_file": "Ha pujat un arxiu", "User_uploaded_image": "Ha pujat una imatge", - "user-generate-access-token": "Usuaris generen Access Tokens", + "user-generate-access-token": "Token d'accés generat per l'usuari", "user-generate-access-token_description": "Permís perquè els usuaris puguin generar access tokens", "UserData_EnableDownload": "Habilitar la descàrrega de dades d'usuari", "UserData_FileSystemPath": "Ruta del sistema (archivos exportados)", @@ -4471,9 +4510,9 @@ "Username_title": "Tria un nom d'usuari", "Username_wants_to_start_otr_Do_you_want_to_accept": "L'usuari __username__ vol iniciar una conversa OTR. L'acceptes?", "Users": "Usuaris", - "Users must use Two Factor Authentication": "Els usuaris han d’utilitzar l’autenticació de dos factors", + "Users must use Two Factor Authentication": "Els usuaris han de fer servir l'autenticació de dos factors", "Users_added": "Els usuaris s'han afegit", - "Users_and_rooms": "Usuaris i Room s", + "Users_and_rooms": "Usuaris i Rooms", "Users_by_time_of_day": "Usuaris per hora del dia", "Users_in_role": "Usuaris al rol", "Users_key_has_been_reset": "Es va restablir la clau de l'usuari", @@ -4488,6 +4527,7 @@ "UTF8_User_Names_Validation_Description": "RegExp que s'utilitzarà per validar noms d'usuari", "UTF8_Channel_Names_Validation": "Validació de noms de channel UTF8", "UTF8_Channel_Names_Validation_Description": "RegExp que s'utilitzarà per validar els noms dels canals", + "Videocall_enabled": "Vídeo trucada activa", "Validate_email_address": "Validar l'adreça de correu electrònic", "Validation": "Validació", "Value_messages": "__value__ messages", @@ -4509,12 +4549,14 @@ "Video_Conference": "Videoconferència", "Video_message": "Missatge de vídeo", "Videocall_declined": "Vídeo trucada rebutjada.", - "Videocall_enabled": "Vídeo trucada activa", + "Video_and_Audio_Call": "Trucada d'àudio i vídeo", "Videos": "Vídeos", "View_All": "Veure tots els membres", "View_channels": "Veure Channel s", + "view-omnichannel-contact-center": "Veure centre de contacte Livechat", + "view-omnichannel-contact-center_description": "Permís per veure i interactuar amb el centre de contacte Livechat", "View_Logs": "Veure registre log", - "View_mode": "Mode de visualització", + "View_mode": "Mode de vista", "View_original": "Veure original", "View_the_Logs_for": "Veure els registres de: \"__name__\"", "view-broadcast-member-list": "Veure llista de membres a la sala de transmissió", @@ -4531,9 +4573,9 @@ "view-history_description": "Permís per veure l'historial del canal", "view-join-code": "Veure el codi per unir-se", "view-join-code_description": "Permís per veure el codi per unir-se al canal", - "view-joined-room": "Veure sales on unit", + "view-joined-room": "Veure Room unida", "view-joined-room_description": "Permís per veure els canals on actualment s'està unit", - "view-l-room": "Veure sales de LiveChat", + "view-l-room": "Veure Rooms de LiveChat", "view-l-room_description": "Permís per veure els canals de LiveChat", "view-livechat-analytics": "Veure analítiques de LiveChat", "view-livechat-analytics_description": "Permís per veure anàlisi de livechat", @@ -4559,18 +4601,18 @@ "view-livechat-triggers_description": "Permís per veure els activadors de livechat", "view-livechat-webhooks": "Veure webhooks Livechat", "view-livechat-webhooks_description": "Permís per veure webhooks Livechat", - "view-livechat-unit": "Veure les unitats de livechat", + "view-livechat-unit": "Veure les unitats de LiveChat", "view-logs": "Veure registres", "view-logs_description": "Permís per veure els registres del servidor", - "view-other-user-channels": "Veure canals d'altres usuaris", + "view-other-user-channels": "Veure d'altres usuaris Channels ", "view-other-user-channels_description": "Permís per veure canals que pertanyen a altres usuaris", "view-outside-room": "Vista exterior de Room", "view-outside-room_description": "Permís per veure usuaris fora de la sala actual", "view-p-room": "Veure sala privada", "view-p-room_description": "Permís per veure canals privats", - "view-privileged-setting": "Veure opcions privilegiades", + "view-privileged-setting": "Veure configuració privilegiada", "view-privileged-setting_description": "Permís per a veure la configuració", - "view-room-administration": "Veure administració de sala", + "view-room-administration": "Veure administració de Room", "view-room-administration_description": "Permís per veure estadístiques de missatges públics, privats i directes. No inclou veure converses o arxius", "view-statistics": "Veure estadístiques", "view-statistics_description": "Permís per veure estadístiques de el sistema, com el nombre d'usuaris connectats, el nombre d'habitacions, la informació de sistema operatiu", @@ -4586,6 +4628,7 @@ "Visitor_message": "Missatges dels visitants", "Visitor_Name": "Nom del visitant", "Visitor_Name_Placeholder": "Si us plau, introduïu el nom de l'visitant ...", + "Visitor_does_not_exist": "El visitant no existeix!", "Visitor_Navigation": "Navegació del visitant", "Visitor_page_URL": "URL de la pàgina del visitant", "Visitor_time_on_site": "Temps de visita", @@ -4598,7 +4641,7 @@ "Warnings": "Avisos", "WAU_value": "WAU __value__", "We_appreciate_your_feedback": "Agraïm els seus comentaris", - "We_are_offline_Sorry_for_the_inconvenience": "Estem fora de línia. Disculpi les molèsties.", + "We_are_offline_Sorry_for_the_inconvenience": "Estem fora de línia. Disculpeu les molèsties.", "We_have_sent_password_email": "T'hem enviat un missatge de correu electrònic amb les instruccions per reinicialitzar la contrasenya. Si no reps el missatge en breu, si us plau mira al correu brossa i/o torna i reintenta-ho.", "We_have_sent_registration_email": "T'hem enviat un missatge de correu electrònic per confirmar el registre. Si no reps el missatge en breu, si us plau mira al correu brossa i/o torna i reintenta-ho.", "Webdav Integration": "Integració de Webdav", @@ -4611,8 +4654,9 @@ "webdav-account-saved": "Compte WebDAV guardada", "webdav-account-updated": "Compte WebDAV actualitzada", "Webhook_Details": "Detalls de WebHook", - "Webhook_URL": "Adreça URL WebHook", + "Webhook_URL": "URL del webhook", "Webhooks": "Webhooks", + "WebRTC_Call": "Trucada WebRTC", "WebRTC_direct_audio_call_from_%s": "Trucada d'àudio directa de %s", "WebRTC_direct_video_call_from_%s": "Videotrucada directa de %s", "WebRTC_Enable_Channel": "Activa per a canals públics", @@ -4623,6 +4667,8 @@ "WebRTC_monitor_call_from_%s": "Superviseu la trucada de %s", "WebRTC_Servers": "Servidors STUN/TURN", "WebRTC_Servers_Description": "Llista de servidors STUN i TURN separats per comes.
      Noms d'usuari, contrasenya i port són permesos en el format `username:password@stun:host:port` o bé `username:password@turn:host:port`.", + "WebRTC_call_ended_message": " La trucada va finalitzar a les __endTime__ - Va durar __callDuration__", + "WebRTC_call_declined_message": "Trucada rebutjada per contacte.", "Website": "lloc web", "Wednesday": "dimecres", "Weekly_Active_Users": "Usuaris actius setmanals", @@ -4645,7 +4691,7 @@ "Would_you_like_to_place_chat_on_hold": "Li agradaria posar aquest xat en espera?", "Yes": "Sí", "Yes_archive_it": "Sí, arxiva'l!", - "Yes_clear_all": "Sí, esborra!", + "Yes_clear_all": "Sí, esborrar-ho tot.", "Yes_deactivate_it": "Sí, desactiveu-lo.", "Yes_delete_it": "Sí, elimina!", "Yes_hide_it": "Sí, oculta!", @@ -4655,14 +4701,14 @@ "Yes_remove_user": "Sí, elimina l'usuari!", "Yes_unarchive_it": "Sí, desarxiva'l!", "yesterday": "ahir", - "Yesterday": "ahir", + "Yesterday": "Ahir", "You": "Vostè", "You_are_converting_team_to_channel": "Ets convertint aquest equip en un canal.", "you_are_in_preview_mode_of": "Estàs en mode vista prèvia del canal #__room_name__", "you_are_in_preview_mode_of_incoming_livechat": "Esteu en mode de previsualització d'aquest xat", "You_are_logged_in_as": "Sessió iniciada com", "You_are_not_authorized_to_view_this_page": "No està autoritzat a veure aquesta pàgina.", - "You_can_change_a_different_avatar_too": "Es pot ignorar l'avatar d'aquesta integració.", + "You_can_change_a_different_avatar_too": "Podeu anul·lar l'avatar utilitzat per publicar des d'aquesta integració.", "You_can_close_this_window_now": "Ja pots tancar aquesta finestra", "You_can_search_using_RegExp_eg": "Podeu fer cerques mitjançant Expressió regular . per exemple. / ^ text $ / i ", "You_can_use_an_emoji_as_avatar": "També es pot utilitzar un emoji com a avatar.", @@ -4671,7 +4717,7 @@ "You_followed_this_message": "Vas seguir aquest missatge.", "You_have_a_new_message": "Teniu un missatge nou", "You_have_been_muted": "Has estat silenciat i no podràs dir res en aquesta sala", - "You_have_n_codes_remaining": "Encara et queden __number__ codis.", + "You_have_n_codes_remaining": "Et queden __number__ codis.", "You_have_not_verified_your_email": "Encara no has verificat la teva adreça de correu electrònic.", "You_have_successfully_unsubscribed": "T'has donat de baixa correctament de la nostra llista de distribució de correu.", "You_have_to_set_an_API_token_first_in_order_to_use_the_integration": "Primer ha de configurar un símbol d'API per utilitzar la integració.", @@ -4680,35 +4726,35 @@ "You_need_install_an_extension_to_allow_screen_sharing": "Necessita instal·lar una extensió per poder compartir la pantalla", "You_need_to_change_your_password": "Cal canvïs la contrasenya", "You_need_to_type_in_your_password_in_order_to_do_this": "Cal que escriguis la contrasenya per fer això.", - "You_need_to_type_in_your_username_in_order_to_do_this": "Cal que escriguis el nom d'usuari per fer això.", + "You_need_to_type_in_your_username_in_order_to_do_this": "Necessiteu escriure el vostre nom d'usuari per fer-ho!", "You_need_to_verifiy_your_email_address_to_get_notications": "Cal tenir verificada l'adreça de correu electrònic per poder rebre notificacions", "You_need_to_write_something": "Cal escriure alguna cosa!", "You_reached_the_maximum_number_of_guest_users_allowed_by_your_license": "Heu assolit el nombre màxim d’usuaris convidats que permet la vostra llicència.", "You_should_inform_one_url_at_least": "Heu de definir almenys una URL.", - "You_should_name_it_to_easily_manage_your_integrations": "Caldria posar-li un nom per poder administrar fàcilment les integracions.", + "You_should_name_it_to_easily_manage_your_integrations": "Ho hauria de nomenar per administrar fàcilment les seves integracions.", "You_unfollowed_this_message": "Vas deixar de seguir aquest missatge.", - "You_will_be_asked_for_permissions": "Se li demanaran permisos", + "You_will_be_asked_for_permissions": "Se us demanaran permisos", "You_will_not_be_able_to_recover": "No podràs recuperar aquest missatge!", "You_will_not_be_able_to_recover_email_inbox": "No podrà recuperar aquesta safata d'entrada de correu electrònic", - "You_will_not_be_able_to_recover_file": "No podràs recuperar aquest arxiu!", - "You_wont_receive_email_notifications_because_you_have_not_verified_your_email": "No rebràs notificacions per correu electrònic, ja que no s'ha verificat l'adreça.", + "You_will_not_be_able_to_recover_file": "No podreu recuperar aquest fitxer!", + "You_wont_receive_email_notifications_because_you_have_not_verified_your_email": "No rebreu notificacions per correu electrònic perquè no heu verificat el vostre correu electrònic.", "Your_e2e_key_has_been_reset": "La vostra clau e2e ha estat restablerta.", "Your_email_address_has_changed": "La seva adreça de correu electrònic ha estat modificada.", - "Your_email_has_been_queued_for_sending": "El teu correu electrònic s'ha posat a la cua d'enviament", + "Your_email_has_been_queued_for_sending": "El vostre correu electrònic s'ha posat en cua per enviar-lo.", "Your_entry_has_been_deleted": "L'entrada s'ha eliminat.", "Your_file_has_been_deleted": "L'arxiu s'ha eliminat.", "Your_invite_link_will_expire_after__usesLeft__uses": "El seu enllaç d'invitació expirarà després d'__usesLeft__ usos.", "Your_invite_link_will_expire_on__date__": "El seu enllaç d'invitació expirarà el dia __date__.", "Your_invite_link_will_expire_on__date__or_after__usesLeft__uses": "El seu enllaç d'invitació expirarà en __date__ o després de __usesLeft__ usos.", - "Your_invite_link_will_never_expire": "L'enllaç d'invitació no caducarà mai.", + "Your_invite_link_will_never_expire": "El vostre enllaç d'invitació mai no caducarà.", "Your_mail_was_sent_to_s": "S'ha enviat el missatge a %s", - "your_message": "el teu missatge", - "your_message_optional": "el teu missatge (opcional)", + "your_message": "El seu missatge", + "your_message_optional": "el seu missatge (opcional)", "Your_new_email_is_email": "La vostra nova adreça electrònica és [email] .", "Your_password_is_wrong": "La contrasenya és incorrecta!", "Your_password_was_changed_by_an_admin": "Un administrador ha canviat la vostra contrasenya.", "Your_push_was_sent_to_s_devices": "La notificació push s'ha enviat a %s dispositius", - "Your_question": "La teva pregunta", + "Your_question": "La seva pregunta", "Your_server_link": "El vostre enllaç del servidor", "Your_temporary_password_is_password": "La vostra contrasenya temporal és [contrasenya] .", "Your_TOTP_has_been_reset": "El vostre TOTP de dos factors s'ha restablert.", diff --git a/packages/rocketchat-i18n/i18n/cs.i18n.json b/packages/rocketchat-i18n/i18n/cs.i18n.json index 1be411e31d21..b8e39f7a2af1 100644 --- a/packages/rocketchat-i18n/i18n/cs.i18n.json +++ b/packages/rocketchat-i18n/i18n/cs.i18n.json @@ -5,6 +5,7 @@ "__count__empty_rooms_will_be_removed_automatically__rooms__": "__count__ prázdných místností bude automaticky odstraněno:
      __rooms__.", "__username__is_no_longer__role__defined_by__user_by_": "__username__ již není __role__ (odebral/a __user_by__ )", "__username__was_set__role__by__user_by_": "__username__ je nyní __role__ (nastavil/a __user_by__)", + "This_room_encryption_has_been_enabled_by__username_": "Tato místnost byla archivována uživatelem __username__", "@username": "@uživatel", "@username_message": "@uživatel ", "#channel": "#místnost", @@ -371,6 +372,7 @@ "API_Upper_Count_Limit": "Maximální počet", "API_Upper_Count_Limit_Description": "Kolik nejvíce záznamů smí REST API vrátit (pokud není limitovaná)", "API_Use_REST_For_DDP_Calls": "Použít místo websocketů REST", + "API_User_Limit": "Maximální počet uživatelů přidaných do místnosti", "API_Wordpress_URL": "WordPress URL", "api-bypass-rate-limit": "Obejít rychlostní limit pro REST API", "Apiai_Key": "Api.ai Klíč", @@ -2841,7 +2843,7 @@ "Placeholder_for_password_login_field": "Zástupný text pro pole hesla v přihlášení", "Please_add_a_comment": "Prosím, přidejte komentář", "Please_add_a_comment_to_close_the_room": "Pro uzavření místnosti prosím přidejte komentář", - "Please_answer_survey": "Věnujte prosím chvilku času ohodnocení chatu.", + "Please_answer_survey": "Věnujte nám prosím chvilku svého času na ohodnocení chatu.", "Please_enter_usernames": "Zadejte uživatelská jména...", "please_enter_valid_domain": "Prosím zadejte platnou doménu", "Please_enter_value_for_url": "Prosím, zadejte URL Vašeho avataru.", @@ -3332,6 +3334,7 @@ "Show_Setup_Wizard": "Zobrazit průvodce nastavením", "Show_the_keyboard_shortcut_list": "Zobrazit klávesové zkratky", "Showing_archived_results": "

      Zobrazeno %s archivovaných výsledků

      ", + "Showing_online_users": null, "Showing_results": "

      Zobrazeno %s výsledků

      ", "Sidebar": "Postranní panel", "Sign_in_to_start_talking": "Pro konverzaci se přihlašte", @@ -3479,7 +3482,7 @@ "Sync_success": "Synchronizace úspěšná", "Sync_Users": "Synchronizace uživatelů", "System_messages": "Systémové zprávy", - "Tag": "Štítek", + "Tag": "Tag", "Tag_removed": "Štítek odstraněn", "Take_it": "Převzít", "Target user not allowed to receive messages": "Cílový uživatel nemá povoleno přijímat zprávy", @@ -3862,6 +3865,7 @@ "Uses": "Použití", "Uses_left": "Zbývající počet použití", "UTF8_Names_Slugify": "Url podoba UTF8 jmen", + "Videocall_enabled": "Videohovor povolen", "Validate_email_address": "Validovat email", "Validation": "Validace", "Value_messages": "__value__ zpráv", @@ -3883,7 +3887,6 @@ "Video_Conference": "Video konference", "Video_message": "Video zpráva", "Videocall_declined": "Videohovor odmítnut", - "Videocall_enabled": "Videohovor povolen", "Videos": "Videa", "View_All": "Zobrazit všechny členy", "View_Logs": "Zobrazit logy", @@ -4057,4 +4060,4 @@ "Your_server_link": "Odkaz na Váš server", "Your_temporary_password_is_password": "Vaše dočasné heslo je [password].", "Your_workspace_is_ready": "Váš prostředí je připraveno k použití 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/cy.i18n.json b/packages/rocketchat-i18n/i18n/cy.i18n.json index 328c1a06530d..15af907b0976 100644 --- a/packages/rocketchat-i18n/i18n/cy.i18n.json +++ b/packages/rocketchat-i18n/i18n/cy.i18n.json @@ -2678,6 +2678,7 @@ "Users_added": "Mae'r defnyddwyr wedi cael eu hychwanegu", "Users_in_role": "Defnyddwyr mewn rôl", "UTF8_Names_Slugify": "Enwau UTF8 Slugify", + "Videocall_enabled": "Galwad Fideo Galluogi", "Validate_email_address": "Dilyswch yr E-bost", "Verification": "Gwirio", "Verification_Description": "Fe allech chi ddefnyddio'r llefydd canlynol:
      • [Verification_Url] ar gyfer yr URL dilysu.
      • [name], [fname], [lname] ar gyfer enw llawn, enw cyntaf neu enw olaf y defnyddiwr, yn y drefn honno.
      • [e-bost] ar gyfer e-bost y defnyddiwr.
      • [Site_Name] a [Site_URL] ar gyfer yr Enw Cais a'r URL yn y drefn honno.
      ", @@ -2692,7 +2693,6 @@ "Video_Conference": "Cynhadledd Fideo", "Video_message": "Neges fideo", "Videocall_declined": "Gwrthodwyd Galwad Fideo.", - "Videocall_enabled": "Galwad Fideo Galluogi", "View_All": "Gweld yr holl Aelodau", "View_Logs": "Gweld Logiau", "View_mode": "Modd Gweld", diff --git a/packages/rocketchat-i18n/i18n/da.i18n.json b/packages/rocketchat-i18n/i18n/da.i18n.json index 95768715eb56..92937eda8db3 100644 --- a/packages/rocketchat-i18n/i18n/da.i18n.json +++ b/packages/rocketchat-i18n/i18n/da.i18n.json @@ -2857,7 +2857,7 @@ "Placeholder_for_password_login_field": "Stedholder for Password Login Field", "Please_add_a_comment": "Tilføj venligst en kommentar", "Please_add_a_comment_to_close_the_room": "Vær venlig at tilføje en kommentar for at lukke værelset", - "Please_answer_survey": "Tag et øjeblik til at besvare en hurtig undersøgelse om denne chat", + "Please_answer_survey": "Brug et øjeblik på at besvare et spørgeskema om denne chat", "Please_enter_usernames": "Indtast venligst brugernavne ...", "please_enter_valid_domain": "Angiv et gyldigt domænenavn", "Please_enter_value_for_url": "Indtast venligst en værdi for din avatarens url.", @@ -2869,7 +2869,7 @@ "Please_fill_a_username": "Venligst udfyld et brugernavn", "Please_fill_all_the_information": "Udfyld venligst alle oplysninger", "Please_fill_an_email": "Udlfyld med e-mail", - "Please_fill_name_and_email": "Venligst udfyld navn og email", + "Please_fill_name_and_email": "Udfyld venligst navn og e-mail", "Please_go_to_the_Administration_page_then_Livechat_Facebook": "Gå til Administrationssiden -> Omnikanal -> Facebook", "Please_select_an_user": "Vælg venligst en bruger", "Please_select_enabled_yes_or_no": "Vælg venligst en indstilling for Aktiveret", @@ -3363,7 +3363,7 @@ "Site_Url": "Webstedets webadresse", "Site_Url_Description": "Eksempel: https://chat.domain.com/", "Size": "Størrelse", - "Skip": "Springe", + "Skip": "Spring over", "Slack_Users": "Slack's Users CSV", "SlackBridge_APIToken": "API Tokens", "SlackBridge_APIToken_Description": "Du kan konfigurere flere slack-servere ved at tilføje en API-token pr. linje.", @@ -3492,8 +3492,8 @@ "Suggestion_from_recent_messages": "Forslag fra nylige meddelelser", "Sunday": "Søndag", "Support": "Support", - "Survey": "Undersøgelse", - "Survey_instructions": "Vurder hvert spørgsmål efter din tilfredshed, 1 hvilket betyder at du er helt utilfreds og 5 betyder, at du er helt tilfreds.", + "Survey": "Spørgeskema", + "Survey_instructions": "Vurder hvert spørgsmål efter din tilfredshed: 1 betyder, at du er helt utilfreds, og 5 betyder, at du er helt tilfreds.", "Symbols": "Symboler", "Sync": "Synkronisér", "Sync / Import": "Synkronisér / Importér", @@ -3886,6 +3886,7 @@ "Uses": "Brugere", "Uses_left": "Tilbageværende brugere", "UTF8_Names_Slugify": "UTF8 Navne Slugify", + "Videocall_enabled": "Videoopkald aktiveret", "Validate_email_address": "Valider e-mail-adresse", "Validation": "Validering", "Value_messages": "__value__ meddelelser", @@ -3907,7 +3908,6 @@ "Video_Conference": "Video konference", "Video_message": "Video besked", "Videocall_declined": "Videoopkald nægtet.", - "Videocall_enabled": "Videoopkald aktiveret", "Videos": "Videoer", "View_All": "Se alle medlemmer", "View_Logs": "Se logfiler", diff --git a/packages/rocketchat-i18n/i18n/de-AT.i18n.json b/packages/rocketchat-i18n/i18n/de-AT.i18n.json index 1c636fa1ec7c..39085f874fec 100644 --- a/packages/rocketchat-i18n/i18n/de-AT.i18n.json +++ b/packages/rocketchat-i18n/i18n/de-AT.i18n.json @@ -556,6 +556,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Kontinuierliche Soundbenachrichtigungen für den neuen Livechat-Raum", "Conversation": "Chat", "Conversation_closed": "Gespräch geschlossen: __comment__.", + "Conversation_finished": "Gespräch beendet", "Conversation_finished_message": "Konversation beendete Nachricht", "conversation_with_s": "die Konversation mit %s", "Convert_Ascii_Emojis": "ASCII zu Emoji konvertieren", @@ -1275,7 +1276,7 @@ "hours": "Stunden", "Hours": "Std", "How_friendly_was_the_chat_agent": "Wie freundlich war der Chat-Agent?", - "How_knowledgeable_was_the_chat_agent": "Wie sachkundig war der Chat-Agent?", + "How_knowledgeable_was_the_chat_agent": "Wie sachkundig war der Chat-Berater?", "How_long_to_wait_after_agent_goes_offline": "Wie lange wird gewartet, bis der Agent offline geht?", "How_responsive_was_the_chat_agent": "Wie reaktionsschnell war der Chat-Agent?", "How_satisfied_were_you_with_this_chat": "Wie zufrieden waren Sie mit diesem Chat?", @@ -2304,6 +2305,7 @@ "Show_Setup_Wizard": "Setup-Assistent anzeigen", "Show_the_keyboard_shortcut_list": "Zeigen Sie die Tastenkombination an", "Showing_archived_results": "

      Anzeigen von %s archivierten Ergebnissen

      ", + "Showing_online_users": null, "Showing_results": "

      %s Ergebnisse

      ", "Sidebar": "Seitenleiste", "Sidebar_list_mode": "Sidebar-Kanallistenmodus", @@ -2684,6 +2686,7 @@ "Users_added": "Die Benutzer wurden hinzugefügt", "Users_in_role": "Zugeordnete Nutzer", "UTF8_Names_Slugify": "UTF8-Namen-Slugify", + "Videocall_enabled": "Videoanruf aktiviert", "Validate_email_address": "E-mail Adresse bestätigen", "Verification": "Überprüfung", "Verification_Description": "Sie können die folgenden Platzhalter verwenden:
      • [Verification_Url] für die Bestätigungs-URL.
      • [Name], [Name], [Name] für den vollständigen Namen, den Vornamen bzw. den Nachnamen des Benutzers.
      • [E-Mail] für die E-Mail des Nutzers.
      • [Site_Name] und [Site_URL] für den Anwendungsnamen bzw. die URL.
      ", @@ -2698,7 +2701,6 @@ "Video_Conference": "Videokonferenz", "Video_message": "Videonachricht", "Videocall_declined": "Videoanruf abgelehnt.", - "Videocall_enabled": "Videoanruf aktiviert", "View_All": "Alle ansehen", "View_Logs": "Logs anzeigen", "View_mode": "Ansichts-Modus", @@ -2820,4 +2822,4 @@ "Your_push_was_sent_to_s_devices": "Die Push-Nachricht wurde an %s Geräte gesendet.", "Your_server_link": "Ihre Serververbindung", "Your_workspace_is_ready": "Ihr Arbeitsbereich ist einsatzbereit 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/de.i18n.json b/packages/rocketchat-i18n/i18n/de.i18n.json index 764e5befd9f5..1712ba826a80 100644 --- a/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/packages/rocketchat-i18n/i18n/de.i18n.json @@ -1968,7 +1968,7 @@ "hours": "Stunden", "Hours": "Stunden", "How_friendly_was_the_chat_agent": "Wie freundlich war der Chat-Agent?", - "How_knowledgeable_was_the_chat_agent": "Wie sachkundig war der Chat-Agent?", + "How_knowledgeable_was_the_chat_agent": "Wie sachkundig war der Chat-Berater?", "How_long_to_wait_after_agent_goes_offline": "Wartedauer, bevor ein Agent in den Offline-Modus übergeht", "How_long_to_wait_to_consider_visitor_abandonment": "Wie lange warten, um die Abwesenheit von Besuchern aufzugeben?", "How_long_to_wait_to_consider_visitor_abandonment_in_seconds": "Wie lange warten, um die Abwesenheit von Besuchern aufzugeben?", @@ -3449,6 +3449,7 @@ "Show_Setup_Wizard": "Setup-Assistent anzeigen", "Show_the_keyboard_shortcut_list": "Zeige die Liste der Keyboard-Shortcuts", "Showing_archived_results": "

      Aneigen von %s archivierte Räume

      ", + "Showing_online_users": null, "Showing_results": "

      %s Ergebnisse

      ", "Sidebar": "Seitenleiste", "Sidebar_list_mode": "Seitenleiste Kanallisten-Modus", @@ -3973,6 +3974,7 @@ "Uses": "Verwendet", "Uses_left": "Verbleibende Verwendungen", "UTF8_Names_Slugify": "UTF8-Namen-Slugify", + "Videocall_enabled": "Videoanruf aktiviert", "Validate_email_address": "E-Mail-Adresse bestätigen", "Verification": "Überprüfung ", "Verification_Description": "Sie können die folgenden Platzhalter verwenden:
      • [Verification_Url] für die Verifikations-URL
      • [name], [fname], [lname] für den vollständigen Namen, Vornamen oder Nachnamen des Benutzers
      • [email] für die E-Mail-Adresse des Benutzers.
      • [Site_Name] und [Site_URL] für den Anwendungsnamen und die URL der Anwendung
      ", @@ -3991,7 +3993,6 @@ "Video_Conference": "Video-Konferenz", "Video_message": "Videonachricht", "Videocall_declined": "Videoanruf abgelehnt", - "Videocall_enabled": "Videoanruf aktiviert", "Videos": "Videos", "View_All": "Alle ansehen", "View_Logs": "Logs anzeigen", @@ -4163,4 +4164,4 @@ "Your_temporary_password_is_password": "Ihr temporäres Passwort lautet [password].", "Your_TOTP_has_been_reset": "Dein Zwei-Faktor-TOTP wurde zurückgesetzt.", "Your_workspace_is_ready": "Ihr Arbeitsbereich ist einsatzbereit 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/el.i18n.json b/packages/rocketchat-i18n/i18n/el.i18n.json index 368800e9cea5..6181f923c0b9 100644 --- a/packages/rocketchat-i18n/i18n/el.i18n.json +++ b/packages/rocketchat-i18n/i18n/el.i18n.json @@ -561,6 +561,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Συνεχείς ειδοποιήσεις ήχου για νέα αίθουσα livechat", "Conversation": "Συνομιλία", "Conversation_closed": "Η συνομιλία έκλεισε: __comment__.", + "Conversation_finished": "Η συνομιλία τελείωσε", "Conversation_finished_message": "Συνομιλία Ολοκληρώθηκε μήνυμα", "conversation_with_s": "τη συνομιλία με το %s", "Convert_Ascii_Emojis": "Μετατροπή ASCII σε Emoji", @@ -1693,6 +1694,7 @@ "Max_length_is": "Το μέγιστο μήκος είναι%s", "Media": "Μεσο ΜΑΖΙΚΗΣ ΕΝΗΜΕΡΩΣΗΣ", "Medium": "Μεσαίο", + "Members": "Μέλη", "Members_List": "Λίστα μελών", "mention-all": "Αναφέρετε όλα", "mention-all_description": "Άδεια χρήσης της παραπομπής @all", @@ -2308,6 +2310,7 @@ "Show_Setup_Wizard": "Εμφάνιση του οδηγού εγκατάστασης", "Show_the_keyboard_shortcut_list": "Εμφάνιση της λίστας συντομεύσεων πληκτρολογίου", "Showing_archived_results": "

      Εμφάνιση αρχειοθετημένα αποτελέσματα %s

      ", + "Showing_online_users": null, "Showing_results": "

      Εμφανιζονται %s αποτελεσματα

      ", "Sidebar": "Πλευρική γραμμή", "Sidebar_list_mode": "Λειτουργία λίστας καναλιών πλευρικής γραμμής", @@ -2414,7 +2417,7 @@ "Sunday": "Κυριακή", "Support": "Υποστήριξη", "Survey": "Έρευνα", - "Survey_instructions": "Βαθμολογήστε κάθε ερώτηση, σύμφωνα με την ικανοποίησή σας, 1 που σημαίνει ότι θα είναι εντελώς ανικανοποίητοι και 5 σημαίνει ότι είστε απόλυτα ικανοποιημένοι.", + "Survey_instructions": "Βαθμολογήστε κάθε ερώτηση, σύμφωνα με την ικανοποίησή σας, 1 που σημαίνει ότι μείνατε δυσαρεστημένοι και 5 σημαίνει ότι είστε απόλυτα ικανοποιημένοι.", "Symbols": "σύμβολα", "Sync_in_progress": "Ο συγχρονισμός βρίσκεται σε εξέλιξη", "Sync_success": "Συγχρονισμός επιτυχία", @@ -2435,7 +2438,7 @@ "Test_Connection": "δοκιμή σύνδεσης", "Test_Desktop_Notifications": "Δοκιμή Desktop Ειδοποιήσεις", "Thank_you_exclamation_mark": "Ευχαριστώ!", - "Thank_you_for_your_feedback": "Ευχαριστούμε για την ανταπόκρισή σας", + "Thank_you_for_your_feedback": "Ευχαριστούμε για τα σχόλιά σας", "The_application_name_is_required": "Το όνομα της εφαρμογής απαιτείται", "The_channel_name_is_required": "Το όνομα του καναλιού απαιτείται", "The_emails_are_being_sent": "Τα μηνύματα ηλεκτρονικού ταχυδρομείου που αποστέλλονται.", @@ -2687,6 +2690,7 @@ "Users_added": "Οι χρήστες έχουν προστεθεί", "Users_in_role": "Χρήστες σε ρόλο", "UTF8_Names_Slugify": "UTF8 Ονόματα Slugify", + "Videocall_enabled": "Κλήση βίντεο ενεργοποιημένη", "Validate_email_address": "Επαλήθευση διεύθυνσης ηλεκτρονικού ταχυδρομείου", "Verification": "Επαλήθευση", "Verification_Description": "Μπορείτε να χρησιμοποιήσετε τις ακόλουθες αντικαταστάσεις:
      • [Verification_Url] για τη διεύθυνση URL επαλήθευσης.
      • [name], [fname], [lname] για το πλήρες όνομα, το όνομα ή το επώνυμο του χρήστη, αντίστοιχα.
      • [email] για τη διεύθυνση ηλεκτρονικής αλληλογραφίας του χρήστη.
      • [Site_Name] και [Site_URL] για το Όνομα της Εφαρμογής και τη διεύθυνση URL αντίστοιχα.
      ", @@ -2701,7 +2705,6 @@ "Video_Conference": "Τηλεδιάσκεψη", "Video_message": "Βίντεο μηνύματος", "Videocall_declined": "Η κλήση βίντεο απορρίφθηκε.", - "Videocall_enabled": "Κλήση βίντεο ενεργοποιημένη", "View_All": "Εμφάνιση όλων", "View_Logs": "Δείτε τα αρχεία καταγραφών", "View_mode": "λειτουργία προβολής", @@ -2823,4 +2826,4 @@ "Your_push_was_sent_to_s_devices": "ώθηση σας στάλθηκε σε συσκευές %s", "Your_server_link": "Σύνδεσμος διακομιστή σας", "Your_workspace_is_ready": "Ο χώρος εργασίας σας είναι έτοιμος για χρήση 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index c6e6db53394e..ce54f0dcc6ca 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -114,6 +114,8 @@ "Accounts_OAuth_Custom_Merge_Users": "Merge users", "Accounts_OAuth_Custom_Name_Field": "Name field", "Accounts_OAuth_Custom_Roles_Claim": "Roles/Groups field name", + "Accounts_OAuth_Custom_Roles_To_Sync": "Roles to Sync", + "Accounts_OAuth_Custom_Roles_To_Sync_Description": "OAuth Roles to sync on user login and creation (comma-separated).", "Accounts_OAuth_Custom_Scope": "Scope", "Accounts_OAuth_Custom_Secret": "Secret", "Accounts_OAuth_Custom_Show_Button_On_Login_Page": "Show Button on Login Page", @@ -370,6 +372,8 @@ "API_Enable_Rate_Limiter_Dev": "Enable Rate Limiter in development", "API_Enable_Rate_Limiter_Dev_Description": "Should limit the amount of calls to the endpoints in the development environment?", "API_Enable_Rate_Limiter_Limit_Calls_Default": "Default number calls to the rate limiter", + "Rate_Limiter_Limit_RegisterUser": "Default number calls to the rate limiter for registering a user", + "Rate_Limiter_Limit_RegisterUser_Description": "Number of default calls for user registering endpoints(REST and real-time API's), allowed within the time range defined in the API Rate Limiter section.", "API_Enable_Rate_Limiter_Limit_Calls_Default_Description": "Number of default calls for each endpoint of the REST API, allowed within the time range defined below", "API_Enable_Rate_Limiter_Limit_Time_Default": "Default time limit for the rate limiter (in ms)", "API_Enable_Rate_Limiter_Limit_Time_Default_Description": "Default timeout to limit the number of calls at each endpoint of the REST API(in ms)", @@ -385,6 +389,7 @@ "API_Personal_Access_Tokens_Regenerate_Modal": "If you lost or forgot your token, you can regenerate it, but remember that all applications that use this token should be updated", "API_Personal_Access_Tokens_Remove_Modal": "Are you sure you wish to remove this personal access token?", "API_Personal_Access_Tokens_To_REST_API": "Personal access tokens to REST API", + "API_Rate_Limiter": "API Rate Limiter", "API_Shield_Types": "Shield Types", "API_Shield_Types_Description": "Types of shields to enable as a comma separated list, choose from `online`, `channel` or `*` for all", "API_Shield_user_require_auth": "Require authentication for users shields", @@ -701,6 +706,9 @@ "By_author": "By __author__", "cache_cleared": "Cache cleared", "Call": "Call", + "Call_declined": "Call Declined!", + "Call_provider": "Call Provider", + "Call_Already_Ended": "Call Already Ended", "call-management": "Call Management", "call-management_description": "Permission to start a meeting", "Caller": "Caller", @@ -1340,6 +1348,7 @@ "Days": "Days", "DB_Migration": "Database Migration", "DB_Migration_Date": "Database Migration Date", + "DDP_Rate_Limit": "DDP Rate Limit", "DDP_Rate_Limit_Connection_By_Method_Enabled": "Limit by Connection per Method: enabled", "DDP_Rate_Limit_Connection_By_Method_Interval_Time": "Limit by Connection per Method: interval time", "DDP_Rate_Limit_Connection_By_Method_Requests_Allowed": "Limit by Connection per Method: requests allowed", @@ -1609,6 +1618,8 @@ "Encryption_key_saved_successfully": "Your encryption key was saved successfully.", "EncryptionKey_Change_Disabled": "You can't set a password for your encryption key because your private key is not present on this client. In order to set a new password you need load your private key using your existing password or use a client where the key is already loaded.", "End": "End", + "End_call": "End call", + "Expand_view": "Expand view", "End_OTR": "End OTR", "Engagement_Dashboard": "Engagement Dashboard", "Enter": "Enter", @@ -1836,7 +1847,9 @@ "Favorite": "Favorite", "Favorite_Rooms": "Enable Favorite Rooms", "Favorites": "Favorites", + "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "This feature depends on the above selected call provider to be enabled from the administration settings.
      For **Jitsi**, please make sure you have Jitsi Enabled under Admin -> Video Conference -> Jitsi -> Enabled.
      For **WebRTC**, please make sure you have WebRTC enabled under Admin -> WebRTC -> Enabled.", "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "This feature depends on \"Send Visitor Navigation History as a Message\" to be enabled.", + "Feature_Limiting": "Feature Limiting", "Features": "Features", "Features_Enabled": "Features Enabled", "Feature_Disabled": "Feature Disabled", @@ -2339,12 +2352,14 @@ "Jitsi_Limit_Token_To_Room": "Limit token to Jitsi Room", "Job_Title": "Job Title", "join": "Join", + "Join_call": "Join Call", "Join_audio_call": "Join audio call", "Join_Chat": "Join Chat", "Join_default_channels": "Join default channels", "Join_the_Community": "Join the Community", "Join_the_given_channel": "Join the given channel", "Join_video_call": "Join video call", + "Join_my_room_to_start_the_video_call": "Join my room to start the video call", "join-without-join-code": "Join Without Join Code", "join-without-join-code_description": "Permission to bypass the join code in channels with join code enabled", "Joined": "Joined", @@ -2669,6 +2684,7 @@ "Livechat_Triggers": "Livechat Triggers", "Livechat_user_sent_chat_transcript_to_visitor": "__agent__ sent the chat transcript to __guest__", "Livechat_Users": "Omnichannel Users", + "Livechat_Calls": "Livechat Calls", "Livechat_visitor_email_and_transcript_email_do_not_match": "Visitor's email and transcript's email do not match", "Livechat_visitor_transcript_request": "__guest__ requested the chat transcript", "LiveStream & Broadcasting": "LiveStream & Broadcasting", @@ -2805,6 +2821,7 @@ "Markdown_Parser": "Markdown Parser", "Markdown_SupportSchemesForLink": "Markdown Support Schemes for Link", "Markdown_SupportSchemesForLink_Description": "Comma-separated list of allowed schemes", + "Marketplace": "Marketplace", "Marketplace_view_marketplace": "View Marketplace", "MAU_value": "MAU __value__", "Max_length_is": "Max length is %s", @@ -2978,6 +2995,8 @@ "Mobex_sms_gateway_restful_address_desc": "IP or Host of your Mobex REST API. E.g. `http://192.168.1.1:8080` or `https://www.example.com:8080`", "Mobex_sms_gateway_username": "Username", "Mobile": "Mobile", + "mobile-download-file": "Allow file download on mobile devices", + "mobile-upload-file": "Allow file upload on mobile devices", "Mobile_Push_Notifications_Default_Alert": "Push Notifications Default Alert", "Monday": "Monday", "Mongo_storageEngine": "Mongo Storage Engine", @@ -3008,6 +3027,7 @@ "Mute_Group_Mentions": "Mute @all and @here mentions", "Mute_someone_in_room": "Mute someone in the room", "Mute_user": "Mute user", + "Mute_microphone": "Mute Microphone", "mute-user": "Mute User", "mute-user_description": "Permission to mute other users in the same channel", "Muted": "Muted", @@ -3187,6 +3207,7 @@ "Omnichannel_External_Frame_URL": "External frame URL", "On": "On", "On_Hold_Chats": "On Hold", + "On_Hold_conversations": "On hold conversations", "online": "online", "Online": "Online", "Only_authorized_users_can_write_new_messages": "Only authorized users can write new messages", @@ -3909,7 +3930,7 @@ "Smileys_and_People": "Smileys & People", "SMS": "SMS", "SMS_Default_Omnichannel_Department": "Omnichannel Department (Default)", - "SMS_Default_Omnichannel_Department_Description": "If set, all new incoming chats initiated by this integration will be routed to this department.", + "SMS_Default_Omnichannel_Department_Description": "If set, all new incoming chats initiated by this integration will be routed to this department.\nThis setting can be overwritten by passing department query param in the request.\ne.g. https:///api/v1/livechat/sms-incoming/twilio?department=.\nNote: if you're using Department Name, then it should be URL safe.", "SMS_Enabled": "SMS Enabled", "SMTP": "SMTP", "SMTP_Host": "SMTP Host", @@ -4283,6 +4304,8 @@ "Tuesday": "Tuesday", "Turn_OFF": "Turn OFF", "Turn_ON": "Turn ON", + "Turn_on_video": "Turn on video", + "Turn_off_video": "Turn off video", "Two Factor Authentication": "Two Factor Authentication", "Two-factor_authentication": "Two-factor authentication via TOTP", "Two-factor_authentication_disabled": "Two-factor authentication disabled", @@ -4344,6 +4367,7 @@ "Unread_Rooms_Mode": "Unread Rooms Mode", "Unread_Tray_Icon_Alert": "Unread Tray Icon Alert", "Unstar_Message": "Remove Star", + "Unmute_microphone": "Unmute Microphone", "Update": "Update", "Update_EnableChecker": "Enable the Update Checker", "Update_EnableChecker_Description": "Checks automatically for new updates / important messages from the Rocket.Chat developers and receives notifications when available. The notification appears once per new version as a clickable banner and as a message from the Rocket.Cat bot, both visible only for administrators.", @@ -4504,6 +4528,7 @@ "UTF8_User_Names_Validation_Description": "RegExp that will be used to validate usernames", "UTF8_Channel_Names_Validation": "UTF8 Channel Names Validation", "UTF8_Channel_Names_Validation_Description": "RegExp that will be used to validate channel names", + "Videocall_enabled": "Video Call Enabled", "Validate_email_address": "Validate Email Address", "Validation": "Validation", "Value_messages": "__value__ messages", @@ -4525,10 +4550,12 @@ "Video_Conference": "Video Conference", "Video_message": "Video message", "Videocall_declined": "Video Call Declined.", - "Videocall_enabled": "Video Call Enabled", + "Video_and_Audio_Call": "Video and Audio Call", "Videos": "Videos", "View_All": "View All Members", "View_channels": "View Channels", + "view-omnichannel-contact-center": "View Omnichannel Contact Center", + "view-omnichannel-contact-center_description": "Permission to view and interact with the Omnichannel Contact Center", "View_Logs": "View Logs", "View_mode": "View Mode", "View_original": "View Original", @@ -4602,6 +4629,7 @@ "Visitor_message": "Visitor Messages", "Visitor_Name": "Visitor Name", "Visitor_Name_Placeholder": "Please enter a visitor name...", + "Visitor_does_not_exist": "Visitor does not exist!", "Visitor_Navigation": "Visitor Navigation", "Visitor_page_URL": "Visitor page URL", "Visitor_time_on_site": "Visitor time on site", @@ -4629,6 +4657,7 @@ "Webhook_Details": "WebHook Details", "Webhook_URL": "Webhook URL", "Webhooks": "Webhooks", + "WebRTC_Call": "WebRTC Call", "WebRTC_direct_audio_call_from_%s": "Direct audio call from %s", "WebRTC_direct_video_call_from_%s": "Direct video call from %s", "WebRTC_Enable_Channel": "Enable for Public Channels", @@ -4639,6 +4668,8 @@ "WebRTC_monitor_call_from_%s": "Monitor call from %s", "WebRTC_Servers": "STUN/TURN Servers", "WebRTC_Servers_Description": "A list of STUN and TURN servers separated by comma.
      Username, password and port are allowed in the format `username:password@stun:host:port` or `username:password@turn:host:port`.", + "WebRTC_call_ended_message": " Call ended at __endTime__ - Lasted __callDuration__", + "WebRTC_call_declined_message": " Call Declined by Contact.", "Website": "Website", "Wednesday": "Wednesday", "Weekly_Active_Users": "Weekly Active Users", @@ -4729,4 +4760,4 @@ "Your_temporary_password_is_password": "Your temporary password is [password].", "Your_TOTP_has_been_reset": "Your Two Factor TOTP has been reset.", "Your_workspace_is_ready": "Your workspace is ready to use 🎉" -} +} \ No newline at end of file diff --git a/packages/rocketchat-i18n/i18n/eo.i18n.json b/packages/rocketchat-i18n/i18n/eo.i18n.json index 54fff096f16a..866905fb6fc2 100644 --- a/packages/rocketchat-i18n/i18n/eo.i18n.json +++ b/packages/rocketchat-i18n/i18n/eo.i18n.json @@ -555,6 +555,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Kontinua sono-sciigoj por nova viva ĉambro", "Conversation": "Konversacio", "Conversation_closed": "Konversacio fermita: __comment__.", + "Conversation_finished": "Konversacio finis", "Conversation_finished_message": "Konversacio Finita Mesaĝo", "conversation_with_s": "la konversacio kun %s", "Convert_Ascii_Emojis": "Konvertu ASCII al Emoji", @@ -2682,6 +2683,7 @@ "Users_added": "La uzantoj estis aldonitaj", "Users_in_role": "Uzantoj en rolo", "UTF8_Names_Slugify": "UTF8 Nomoj Slugify", + "Videocall_enabled": "Video Vokis Enabled", "Validate_email_address": "Validigi retpoŝtadreson", "Verification": "Verkcio", "Verification_Description": "Vi povas uzi la jenajn anstataŭilojn:
      • [Verification_Url] por la verificación URL.
      • [nomo], [fname], [lname] por la plena nomo, unua nomo aŭ familinomo de la uzanto, respektive.
      • [retpoŝto] por la retpoŝto de la uzanto.
      • [Site_Name] kaj [Site_URL] por la Aplika nomo kaj URL respektive.
      ", @@ -2696,7 +2698,6 @@ "Video_Conference": "Video Konferenco", "Video_message": "Video-mesaĝo", "Videocall_declined": "Video Vokis Malakceptita.", - "Videocall_enabled": "Video Vokis Enabled", "View_All": "Rigardi ĉiujn membrojn", "View_Logs": "Vidi Registrojn", "View_mode": "Rigardi Modo", diff --git a/packages/rocketchat-i18n/i18n/es.i18n.json b/packages/rocketchat-i18n/i18n/es.i18n.json index 92cf8484b32e..9627c0b0d6e2 100644 --- a/packages/rocketchat-i18n/i18n/es.i18n.json +++ b/packages/rocketchat-i18n/i18n/es.i18n.json @@ -4,7 +4,7 @@ "__count__empty_rooms_will_be_removed_automatically": "__count__ salas vacías serán eliminadas automáticamente.", "__count__empty_rooms_will_be_removed_automatically__rooms__": "__count__ salas vacías serán eliminadas automáticamente.
      __rooms__.", "__username__is_no_longer__role__defined_by__user_by_": "__username__ ya no es __role__ (por __user_by__)", - "__username__was_set__role__by__user_by_": "__username__ fue establecido __role__ (por __user_by__)", + "__username__was_set__role__by__user_by_": "__username__ fue establecido __role__ por __user_by__", "This_room_encryption_has_been_enabled_by__username_": "El cifrado de esta sala ha sido habilitado por __username__", "This_room_encryption_has_been_disabled_by__username_": "El cifrado de esta sala ha sido deshabilitado por __username__", "@username": "@usuario", @@ -21,7 +21,7 @@ "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Un nuevo propietario será asignado automáticamente a estas __count__ salas.
      __rooms__.", "Accept": "Aceptar", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Aceptar solicitudes entrantes de Omnichannel aunque no haya agentes en línea", - "Accept_new_livechats_when_agent_is_idle": "Aceptar nuevas solicitudes de Omnichannel cuando el agente esté inactivo", + "Accept_new_livechats_when_agent_is_idle": "Aceptar nuevas solicitudes de omnichannel cuando el agente esté inactivo", "Accept_with_no_online_agents": "Aceptar sin agentes en línea", "Access_not_authorized": "Acceso no autorizado", "Access_Token_URL": "URL de Token de Acceso", @@ -83,7 +83,7 @@ "Accounts_EmailVerification_Description": "Asegúrese de que tiene la configuración SMTP correcta para usar esta característica", "Accounts_Enrollment_Email": "Correo Electrónico de Inscripción ", "Accounts_Enrollment_Email_Default": "

      Bienvenido a [Site_Name]

      Ve a [Site_URL] y pruebe la mejor solución de chat de código abierto disponible en la actualidad!

      ", - "Accounts_Enrollment_Email_Description": "Puedes utilizar los siguientes marcadores:
      • [name], [fname], [lname] para el nombre completo, nombre o apellidos, respectivamente.
      • [email] para el correo electrónico del usuario.
      • [Site_Name] y [Site_URL] para el nombre del sitio web y la URL, respectivamente.
      ", + "Accounts_Enrollment_Email_Description": "Puedes utilizar los siguientes marcadores:
      • [name], [fname], [lname] para el nombre completo de usuario, nombre o apellidos, respectivamente.
      • [email] para el correo electrónico del usuario.
      • [Site_Name] y [Site_URL] para el nombre del sitio web y la URL, respectivamente.
      ", "Accounts_Enrollment_Email_Subject_Default": "Bienvenido a [Site_Name]", "Accounts_ForgetUserSessionOnWindowClose": "Olvidar la sesión de usuario al cerrar la ventana", "Accounts_Iframe_api_method": "Método API", @@ -114,6 +114,8 @@ "Accounts_OAuth_Custom_Merge_Users": "Fusionar usuarios", "Accounts_OAuth_Custom_Name_Field": "Campo de nombre", "Accounts_OAuth_Custom_Roles_Claim": "Nombre del campo roles/grupos", + "Accounts_OAuth_Custom_Roles_To_Sync": "Roles para sincronizar", + "Accounts_OAuth_Custom_Roles_To_Sync_Description": "Roles de OAuth para sincronizar en el inicio de sesión y la creación del usuario (separados por comas).", "Accounts_OAuth_Custom_Scope": "Ámbito (scope)", "Accounts_OAuth_Custom_Secret": "Secreto", "Accounts_OAuth_Custom_Show_Button_On_Login_Page": "Mostrar botón en la página de inicio de sesión", @@ -128,7 +130,7 @@ "Accounts_OAuth_Facebook_callback_url": "URL de retorno (callback) de Facebook", "Accounts_OAuth_Facebook_id": "App Id de Facebook", "Accounts_OAuth_Facebook_secret": "Secreto de Facebook", - "Accounts_OAuth_Github": "Habilitar OAuth", + "Accounts_OAuth_Github": "OAuth habilitado", "Accounts_OAuth_Github_callback_url": "URL de retorno (callback) de Github", "Accounts_OAuth_GitHub_Enterprise": "OAuth Habilitado", "Accounts_OAuth_GitHub_Enterprise_callback_url": "URL de retorno (callback) de GitHub Enterprise", @@ -217,17 +219,17 @@ "Accounts_RegistrationForm_SecretURL": "URL Secreto del Fomulario de Registro", "Accounts_RegistrationForm_SecretURL_Description": "Debe proporcionar una cadena de texto aleatorio que se añadirá a la URL de registro. Ejemplo: https://open.rocket.chat/register/[secret_hash]", "Accounts_RequireNameForSignUp": "Requerir un Nombre para el Registro", - "Accounts_RequirePasswordConfirmation": "Solicitar Confirmación de Contraseña", + "Accounts_RequirePasswordConfirmation": "Requiere Confirmación de Contraseña", "Accounts_RoomAvatarExternalProviderUrl": "Room URL del proveedor externo de Avatar", "Accounts_RoomAvatarExternalProviderUrl_Description": "Ejemplo: `https://acme.com/api/v1/{roomId}`", "Accounts_SearchFields": "Campos a Considerar en la Búsqueda", - "Accounts_Send_Email_When_Activating": "Enviar un correo electrónico al usuario cuando esté activado", + "Accounts_Send_Email_When_Activating": "Enviar correo electrónico al usuario cuando el usuario está activado", "Accounts_Send_Email_When_Deactivating": "Enviar un correo electrónico al usuario cuando esté desactivado", "Accounts_Set_Email_Of_External_Accounts_as_Verified": "Establecer el correo electrónico de las cuentas externas como verificado", "Accounts_Set_Email_Of_External_Accounts_as_Verified_Description": "Los correos electrónicos de cuentas creadas desde servicios externos, como LDAP, OAuth, etc., se marcarán como verificados automáticamente. ", "Accounts_SetDefaultAvatar": "Establecer avatar predeterminado", "Accounts_SetDefaultAvatar_Description": "Tratar de determinar el avatar predeterminado basado en la cuenta OAuth o Gravatar", - "Accounts_ShowFormLogin": "Mostrar el formulario de inicio de sesión predeterminado", + "Accounts_ShowFormLogin": "Mostrar formulario de inicio de sesión predeterminado", "Accounts_TwoFactorAuthentication_By_TOTP_Enabled": "Habilitar la autenticación de dos factores a través de TOTP", "Accounts_TwoFactorAuthentication_By_TOTP_Enabled_Description": "Los usuarios pueden configurar su autenticación de dos factores utilizando cualquier aplicación TOTP, como Google Authenticator o Authy.", "Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In": "Establecer segundo factor de autenticación vía email por defecto para nuevos usuarios ", @@ -270,7 +272,7 @@ "Add_User": "Añadir usuario", "Add_users": "Añadir usuarios", "Add_members": "Añadir miembros", - "add-livechat-department-agents": "Añadir agentes de Omnichannel a departamentos", + "add-livechat-department-agents": "Añadir agentes de Omnichannel a los departamentos", "add-livechat-department-agents_description": "Permiso para agregar agentes Omnichannel a los departamentos", "add-oauth-service": "Agregar Servicio Oauth", "add-oauth-service_description": "Permiso para agregar un nuevo servicio OAuth", @@ -287,14 +289,14 @@ "Adding_user": "Añadiendo usuario", "Additional_emails": "Correos electrónicos adicionales", "Additional_Feedback": "Retroalimentación adicional", - "additional_integrations_Bots": "Si está buscando cómo integrar su propio bot, entonces no busque más, nuestro adaptador Hubot. https://github.com/RocketChat/hubot-rocketchat", + "additional_integrations_Bots": "Si está buscando cómo integrar su propio bot, no busque más que nuestro adaptador Hubot. https://github.com/RocketChat/hubot-rocketchat", "additional_integrations_Zapier": "¿Está buscando integrar otro software y aplicaciones con Rocket.Chat pero no tiene tiempo para hacerlo manualmente? Entonces, sugerimos usar Zapier, que es totalmente compatible. Lea más sobre esto en nuestra documentación. https://rocket.chat/docs/administrator-guides/integrations/zapier/using-zaps/", "Admin_disabled_encryption": "Su administrador no habilitó encriptación E2E", "Admin_Info": "Información de administración", "Administration": "Administración", "Adult_images_are_not_allowed": "No se permiten imágenes para adultos", "Aerospace_and_Defense": "Aeroespacial y Defensa", - "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "Después de la autenticación OAuth2, los usuarios serán redirigidos a esta URL. Puedes añadir una URL por línea.", + "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "Después de la autenticación OAuth2, los usuarios serán redirigidos a una URL en esta lista. Puede agregar una URL por línea.", "Agent": "Agente", "Agent_added": "Agente agregado", "Agent_Info": "Información del agente", @@ -352,7 +354,7 @@ "API_Drupal_URL": "URL del servidor Drupal", "API_Drupal_URL_Description": "Ejemplo: https://domain.com (sin incluir la barra diagonal)", "API_Embed": "Incrustar (embed)", - "API_Embed_Description": "Ya sea que las previsualizaciones de enlace integrados estén activas o no cuando un usuario publica un enlace a un sitio web.", + "API_Embed_Description": "Si las vistas previas de enlaces incrustados están habilitadas o no cuando un usuario publica un enlace a un sitio web.", "API_Embed_UserAgent": "Incrusta agente de usuario en la solicitud", "API_EmbedCacheExpirationDays": "Incrusta días de vencimiento de caché", "API_EmbedDisabledFor": "Deshabilitar el insertar vinculos para los Usuarios", @@ -370,8 +372,10 @@ "API_Enable_Rate_Limiter_Dev": "Habilitar límite de frecuencia en desarrollo", "API_Enable_Rate_Limiter_Dev_Description": "¿Debería limitar la cantidad de llamadas a los puntos finales en el entorno de desarrollo?", "API_Enable_Rate_Limiter_Limit_Calls_Default": "Número predeterminado de llamadas al límite de velocidad", + "Rate_Limiter_Limit_RegisterUser": "Llamadas de números predeterminados al limitador de velocidad para registrar un usuario", + "Rate_Limiter_Limit_RegisterUser_Description": "Número de llamadas predeterminadas para usuarios que registran puntos finales (REST y API en tiempo real), permitidas dentro del rango de tiempo definido en la sección API Rate Limiter.", "API_Enable_Rate_Limiter_Limit_Calls_Default_Description": "Número de llamadas predeterminadas para cada punto final de la API REST, permitidas dentro del rango de tiempo definido a continuación", - "API_Enable_Rate_Limiter_Limit_Time_Default": "Límite de tiempo predeterminado para el límite de frecuencia (en ms)", + "API_Enable_Rate_Limiter_Limit_Time_Default": "Límite de tiempo predeterminado para el limitador de frecuencia (en ms)", "API_Enable_Rate_Limiter_Limit_Time_Default_Description": "Tiempo de espera predeterminado para limitar el número de llamadas en cada punto final de la API REST (en ms)", "API_Enable_Shields": "Activar escudos", "API_Enable_Shields_Description": "Activar los escudos disponibles en `/api/v1/shields. svg`", @@ -385,6 +389,7 @@ "API_Personal_Access_Tokens_Regenerate_Modal": "Si perdiste tu token, puedes volver a generarlo, pero recuerda que las aplicaciones que lo usen tendrán que actualizarlo", "API_Personal_Access_Tokens_Remove_Modal": "¿Estás seguro que quieres eliminar este token de acceso personal?", "API_Personal_Access_Tokens_To_REST_API": "Token de acceso personal a API REST", + "API_Rate_Limiter": "Limitador de tasa API", "API_Shield_Types": "Tipos de escudo", "API_Shield_Types_Description": "Tipos de escudos para activar como una lista separada por comas, elija entre `online`,` channel` o `*` para todos", "API_Shield_user_require_auth": "Requerir autenticación para escudos de los usuarios", @@ -394,7 +399,7 @@ "API_Upper_Count_Limit": "Cantidad máxima de registros", "API_Upper_Count_Limit_Description": "¿Cuál es el número máximo de registros que debe devolver la API REST (cuando no es ilimitado)?", "API_Use_REST_For_DDP_Calls": "Usar REST en lugar de WebSocket para las llamadas de Meteor", - "API_User_Limit": "Límite de usuarios para añadir todos los usuarios a un canal", + "API_User_Limit": "Límite de usuario para agregar todos los usuarios a un canal", "API_Wordpress_URL": "URL de Wordpress", "api-bypass-rate-limit": "Límite de velocidad de omisión para la API REST", "api-bypass-rate-limit_description": "Permiso para llamar a la API sin limitación de tarifas", @@ -452,16 +457,16 @@ "Apps_Interface_IPostRoomDeleted": "Evento que ocurre después de que una sala es eliminada", "Apps_Interface_IPostRoomUserJoined": "Evento que ocurre después de que un usuario se una a una sala (pública, privada)", "Apps_Interface_IPreMessageDeletePrevent": "Evento que ocurre después de que un mensaje es eliminado", - "Apps_Interface_IPreMessageSentExtend": "Evento que ocurre antes de que un mensaje es enviado", + "Apps_Interface_IPreMessageSentExtend": "Evento que ocurre antes de que se envíe un mensaje", "Apps_Interface_IPreMessageSentModify": "Evento que ocurre antes de que un mensaje es enviado", - "Apps_Interface_IPreMessageSentPrevent": "Evento que ocurre antes de que un mensaje es enviado", + "Apps_Interface_IPreMessageSentPrevent": "Evento que ocurre antes de que se envíe un mensaje", "Apps_Interface_IPreMessageUpdatedExtend": "Evento que ocurre antes de que un mensaje es actualizado", - "Apps_Interface_IPreMessageUpdatedModify": "Evento que ocurre antes de que un mensaje es actualizado", + "Apps_Interface_IPreMessageUpdatedModify": "Evento que ocurre antes de que se actualice un mensaje", "Apps_Interface_IPreMessageUpdatedPrevent": "Evento que ocurre antes de que un mensaje es actualizado", "Apps_Interface_IPreRoomCreateExtend": "Evento que ocurre antes de que una sala es creada", "Apps_Interface_IPreRoomCreateModify": "Evento que ocurre antes de que una sala es creada", - "Apps_Interface_IPreRoomCreatePrevent": "Evento que ocurre antes de que una sala es creada", - "Apps_Interface_IPreRoomDeletePrevent": "Evento que ocurre antes de que una sala es eliminada", + "Apps_Interface_IPreRoomCreatePrevent": "Evento que ocurre antes de que se cree una sala", + "Apps_Interface_IPreRoomDeletePrevent": "Evento que ocurre antes de que se elimine una sala", "Apps_Interface_IPreRoomUserJoined": "Evento que ocurre antes de que un usuario se una a una sala (pública, privada)", "Apps_License_Message_appId": "No se ha emitido la licencia para esta aplicación.", "Apps_License_Message_bundle": "Licencia emitida para un paquete que no contiene la aplicación", @@ -476,8 +481,8 @@ "Apps_Logs_TTL_30days": "30 díes", "Apps_Logs_TTL_Alert": "Dependiendo del tamaño de la colección de registros, cambiar esta configuración puede causar lentitud por algunos momentos", "Apps_Marketplace_Deactivate_App_Prompt": "¿Realmente quieres deshabilitar esta aplicación?", - "Apps_Marketplace_Login_Required_Description": "Comprar aplicaciones del Marketplace Rocket.Chat requiere registrar tu entorno de trabajo y logarse.", - "Apps_Marketplace_Login_Required_Title": "Login requerido para Marketplace", + "Apps_Marketplace_Login_Required_Description": "Comprar aplicaciones de Rocket.Chat Marketplace requiere registrar su espacio de trabajo e iniciar sesión.", + "Apps_Marketplace_Login_Required_Title": "Se requiere inicio de sesión en Marketplace", "Apps_Marketplace_Modify_App_Subscription": "Modificar suscripción", "Apps_Marketplace_pricingPlan_monthly": "__price__ / mes", "Apps_Marketplace_pricingPlan_monthly_perUser": "__price__ / mes por usuario", @@ -550,10 +555,10 @@ "assign-roles": "Asignar roles", "assign-roles_description": "Permiso para asignar roles a otros usuarios", "at": "en", - "At_least_one_added_token_is_required_by_the_user": "Al menos uno de los tokens añadidos es requerido por el usuario", + "At_least_one_added_token_is_required_by_the_user": "El usuario requiere al menos un token agregado", "AtlassianCrowd": "Atlassian Crowd", "Attachment_File_Uploaded": "Archivo subido", - "Attribute_handling": "Manejo de atributos", + "Attribute_handling": "Tratamiento de atributos", "Audio": "Audio", "Audio_message": "Mensaje de audio", "Audio_Notification_Value_Description": "Puede ser cualquier sonido personalizado o los predeterminados: bip, chelle, ding, droplet, highbell, seasons", @@ -585,7 +590,7 @@ "Automatic_Translation": "Traducción automática", "AutoTranslate": " Traducción Automática", "AutoTranslate_APIKey": "Clave API", - "AutoTranslate_Change_Language_Description": "Cambiar el idioma de la traducción automática no traduce los mensajes anteriores.", + "AutoTranslate_Change_Language_Description": "Cambiar el idioma de traducción automática no traduce los mensajes anteriores.", "AutoTranslate_DeepL": "DeepL", "AutoTranslate_Enabled": "Habilitar Traducción Automática", "AutoTranslate_Enabled_Description": "Habilitar la traducción automática, permite a las personas con la licencia de auto-traducción que todos los mensajes se traduzcan automáticamente a su idioma seleccionado. Se pueden aplicar cargos.", @@ -603,7 +608,7 @@ "Avg_chat_duration": "Promedio de duración del chat", "Avg_first_response_time": "Promedio del tiempo de la primera respuesta", "Avg_of_abandoned_chats": "Promedio de conversaciones abandonadas", - "Avg_of_available_service_time": "Promedio del tiempo de servicio disponible", + "Avg_of_available_service_time": "Promedio del tiempo disponible del servicio", "Avg_of_chat_duration_time": "Promedio del tiempo de duración del chat", "Avg_of_service_time": "Promedio del tiempo de servicio", "Avg_of_waiting_time": "Promedio del tiempo de espera", @@ -646,10 +651,10 @@ "Block_Multiple_Failed_Logins_By_Ip": "Bloquear los intentos fallidos de acceso por IP", "Block_Multiple_Failed_Logins_By_User": "Bloquear los intentos fallidos de acceso por nombre de usuario", "Block_Multiple_Failed_Logins_Enable_Collect_Login_data_Description": "Almacenar la IP y el nombre de usuario de los intentos de acceso en una colección en la base de datos", - "Block_Multiple_Failed_Logins_Enabled": "Habilitar la recopilación de datos de inicio de sesión", + "Block_Multiple_Failed_Logins_Enabled": "Habilitar recopilar datos de inicio de sesión", "Block_Multiple_Failed_Logins_Ip_Whitelist": "Lista blanca de IP", "Block_Multiple_Failed_Logins_Ip_Whitelist_Description": "Lista separada por comas de IP permitidas", - "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes": "Tiempo para desbloquear la IP (En minutos)", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes": "Tiempo para desbloquear IP (en minutos)", "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes": "Tiempo para desbloquear al usuario (en minutos)", "Block_Multiple_Failed_Logins_Notify_Failed": "Notificación de intentos fallidos de inicio de sesión", "Block_Multiple_Failed_Logins_Notify_Failed_Channel": "Channel para enviar las notificaciones", @@ -676,8 +681,8 @@ "Broadcasting_enabled": "Habilitar transmisión", "Broadcasting_media_server_url": "URL del servidor de medios de transmisión", "Browse_Files": "Búsqueda de archivos", - "Browser_does_not_support_audio_element": "Su navegador no soporta el elemento de audio.", - "Browser_does_not_support_video_element": "Su navegador no soporta el elemento de vídeo.", + "Browser_does_not_support_audio_element": "Su navegador no es compatible con el elemento de audio.", + "Browser_does_not_support_video_element": "Su navegador no es compatible con el elemento de vídeo.", "Bugsnag_api_key": "Clave API de Bugsnag", "Build_Environment": "Entorno de construcción", "bulk-register-user": "Creación masiva de usuarios", @@ -701,6 +706,9 @@ "By_author": "Por __author__", "cache_cleared": "Caché borrada", "Call": "Llamada", + "Call_declined": "¡Llamada rechazada!", + "Call_provider": "Proveedor de llamadas", + "Call_Already_Ended": "Llamada ya finalizada", "call-management": "Gestión de llamadas", "call-management_description": "Permiso para inciar una reunión", "Caller": "Emisor", @@ -712,7 +720,7 @@ "Canned_Response_Delete_Warning": "La eliminación de una respuesta automática no se puede deshacer.", "Canned_Response_Removed": "Respuesta predefinida eliminada", "Canned_Response_Sharing_Department_Description": "Cualquier persona del departamento seleccionado puede acceder a esta respuesta predefinida", - "Canned_Response_Sharing_Private_Description": "Solo usted y los administradores de LiveChat pueden acceder a esta respuesta predefinida", + "Canned_Response_Sharing_Private_Description": "Solo usted y los administradores de Omnichannel pueden acceder a esta respuesta predefinida", "Canned_Response_Sharing_Public_Description": "Cualquiera puede acceder a esta respuesta predefinida", "Canned_Responses": "Respuestas predefinidas", "Canned_Responses_Enable": "Habilitar respuesta predefinida", @@ -734,12 +742,12 @@ "CAS_login_url_Description": "La URL de inicio de sesión de su servicio de SSO externo, por ejemplo: https: //sso.example.undef/sso/login", "CAS_popup_height": "Altura de la ventana emergente de inicio de sesión", "CAS_popup_width": "Ancho de la ventana emergente de inicio de sesión", - "CAS_Sync_User_Data_Enabled": "Sincronizar siempre los datos de usuario", + "CAS_Sync_User_Data_Enabled": "Sincronizar siempre los datos del usuario", "CAS_Sync_User_Data_Enabled_Description": "Sincronizar siempre los datos de usuario de CAS externos en los atributos disponibles al iniciar sesión. Nota: los atributos siempre se sincronizan al crear la cuenta de todos modos.", "CAS_Sync_User_Data_FieldMap": "Mapa de atributos", "CAS_Sync_User_Data_FieldMap_Description": "Utilice esta entrada JSON para construir atributos internos (clave) a partir de atributos externos (valor). Los nombres de los atributos externos encerrados con '%' se interpolarán en cadenas de valores.
      Ejemplo, `{\"email\":\"%email%\", \"name\":\"%firstname%, %lastname%\"}`

      El mapa de atributos siempre está interpolado. En CAS 1.0 sólo está disponible el atributo \"nombre de usuario\". Los atributos internos disponibles son: nombre de usuario, nombre, correo electrónico, habitaciones; habitaciones es una lista separada por comas de habitaciones a las que unirse al crear un usuario, por ejemplo: {\"rooms\": \"%team%,%department%\"} se uniría a los usuarios de CAS en la creación a su equipo y canal de departamento.", "CAS_trust_username": "Confiar en el nombre de usuario de CAS", - "CAS_trust_username_description": "Cuando está habilitado, Rocket.Chat confiará en que cualquier nombre de usuario de CAS pertenece al mismo usuario en Rocket.Chat.
      Esto puede ser necesario si se cambia el nombre de un usuario en CAS, pero también puede permitir que las personas tomen el control de Rocket. Chatee las cuentas cambiando el nombre de sus propios usuarios CAS", + "CAS_trust_username_description": "Cuando está habilitado, Rocket.Chat confiará en que cualquier nombre de usuario de CAS pertenece al mismo usuario en Rocket.Chat.
      Esto puede ser necesario si se cambia el nombre de un usuario en CAS, pero también puede permitir que las personas tomen el control de Rocket. Chatee las cuentas cambiando el nombre de sus propios usuarios de CAS.", "CAS_version": "Versión CAS", "CAS_version_Description": "Use sólo una versión compatible con CAS admitida por su servicio CAS SSO.", "Categories": "Categorías", @@ -747,7 +755,7 @@ "CDN_PREFIX": "Prefijo de CDN", "CDN_PREFIX_ALL": "Utilizar prefijo CDN para todos los activos", "Certificates_and_Keys": "Certificados y Claves", - "change-livechat-room-visitor": "Cambiar visitante de sala Omnichannel", + "change-livechat-room-visitor": "Cambiar visitante de sala Omnichannel Room", "change-livechat-room-visitor_description": "Permiso para agregar información adicional al visitante de Omnichannel", "Change_Room_Type": "Cambiar el Tipo de Sala", "Changing_email": "Cambiando correo electrónico", @@ -755,7 +763,7 @@ "Channel": "Canal", "Channel_already_exist": "El canal '#%s' ya existe.", "Channel_already_exist_static": "El canal ya existe.", - "Channel_already_Unarchived": "El canal con nombre `#%s` ya está en estado Desarchivado", + "Channel_already_Unarchived": "El Channel con nombre `#%s` ya está en estado Desarchivado", "Channel_Archived": "El canal con nombre `#%s` ha sido archivado con éxito", "Channel_created": "Canal `#%s` creado.", "Channel_doesnt_exist": "El canal `#%s` no existe", @@ -844,7 +852,7 @@ "Chatpal_Window_Size_Description": "El tamaño de las ventanas de índice en horas (en arranque)", "Chats_removed": "Chats eliminados", "Check_All": "Seleccionar todo", - "Check_Progress": "Comprobar progres", + "Check_Progress": "Comprobar progreso", "Choose_a_room": "Elija una sala", "Choose_messages": "Elija mensajes", "Choose_the_alias_that_will_appear_before_the_username_in_messages": "Elige el alias que aparecerá antes del nombre de usuario en los mensajes.", @@ -856,15 +864,15 @@ "clear": "Borrar", "Clear_all_unreads_question": "Borrar todos los mensajes no leídos?", "clear_cache_now": "Borrar caché ahora", - "Clear_filters": "Limpiar filtros", + "Clear_filters": "Borrar los filtros", "clear_history": "Borrar historial", "Click_here": "Haga clic aquí", - "Click_here_for_more_details_or_contact_sales_for_a_new_license": "Click aquí para más detalles o contacta con __email__ para una nueva licencia.", + "Click_here_for_more_details_or_contact_sales_for_a_new_license": "Haga clic aquí para obtener más detalles o comuníquese con __email__ para obtener una nueva licencia.", "Click_here_for_more_info": "Haga click aquí para más información", "Click_here_to_enter_your_encryption_password": "Haga clic aquí para ingresar su contraseña de cifrado", "Click_here_to_view_and_copy_your_password": "Haga click aquí para ver y copiar su contraseña.", "Click_the_messages_you_would_like_to_send_by_email": "Haga click en los mensajes que desee enviar por correo electrónico", - "Click_to_join": "¡Click para unirse!", + "Click_to_join": "¡Haga clic para unirse!", "Click_to_load": "Haga click para cargar", "Client_ID": "Cliente ID", "Client_Secret": "Cliente Secreto", @@ -873,12 +881,12 @@ "Close": "Cerrar", "Close_chat": "Cerrar chat", "Close_room_description": "Estás a punto de cerrar este chat. ¿Estás seguro de que quieres continuar?", - "Close_to_seat_limit_banner_warning": "* Te quedan [__seats__] asientos * \nEste espacio de trabajo se acerca a su límite de asientos. Una vez que se alcanza el límite, no se pueden agregar nuevos miembros. * [Solicitar más asientos] (__url__) *", + "Close_to_seat_limit_banner_warning": "* Te quedan [__seats__] sitiios * \nEste espacio de trabajo se acerca a su límite de sitios. Una vez que se alcanza el límite, no se pueden agregar nuevos miembros. * [Solicitar más sitios] (__url__) *", "Close_to_seat_limit_warning": "No se pueden crear nuevos miembros una vez que se alcanza el límite de asientos.", "close-livechat-room": "Cerrar la Room de Omnichannel", "close-livechat-room_description": "Permiso para cerrar la sala Omnichannel actual", "Close_menu": "Cerrar menú", - "close-others-livechat-room": "Cerrar otras salas de Omnichannel", + "close-others-livechat-room": "Cerrar otras Room de Omnichannel", "close-others-livechat-room_description": "Permiso para cerrar otras salas de Omnichannel", "Closed": "Cerrado", "Closed_At": "Cerrado en", @@ -923,10 +931,10 @@ "Cloud_update_email": "Actualizar correo electrónico", "Cloud_what_is_it": "¿Que es esto?", "Cloud_what_is_it_additional": "Además, podrá administrar licencias, facturación y soporte desde la consola de la nube Rocket.Chat.", - "Cloud_what_is_it_description": "Rocket.Chat Cloud Connect le permite conectar su espacio de trabajo Rocket.Chat con los servicios que ofrecemos en nuestra nube.", + "Cloud_what_is_it_description": "Rocket.Chat Cloud Connect le permite conectar su espacio de trabajo Rocket.Chat autohospedado a los servicios que brindamos en nuestra nube.", "Cloud_what_is_it_services_like": "Servicios como:", "Cloud_workspace_connected": "Su espacio de trabajo está conectado a Rocket.Chat Cloud. Iniciar sesión en su cuenta de Rocket.Chat Cloud aquí le permitirá interactuar con algunos servicios como el marketplace.", - "Cloud_workspace_connected_plus_account": "Su área de trabajo ahora está conectada a Rocket.Chat Cloud y una cuenta está asociada.", + "Cloud_workspace_connected_plus_account": "Su espacio de trabajo ahora está conectado a Rocket.Chat Cloud y una cuenta está asociada.", "Cloud_workspace_connected_without_account": "Su espacio de trabajo ahora está conectado a Rocket.Chat Cloud. Si lo desea, puede iniciar sesión en Rocket.Chat Cloud y asociar su espacio de trabajo con su cuenta en la nube.", "Cloud_workspace_disconnect": "Si ya no desea utilizar los servicios en la nube, puede desconectar su espacio de trabajo de Rocket.Chat Cloud.", "Cloud_workspace_support": "Si tiene problemas con un servicio en la nube, intente sincronizar primero. Si el problema persiste, abra un ticket de soporte en Cloud Console.", @@ -937,7 +945,7 @@ "Color": "Color", "Colors": "Colores", "Commands": "Comandos", - "Comment_to_leave_on_closing_session": "Comentario para salir en la sesión de clausura", + "Comment_to_leave_on_closing_session": "Comentario para dejar en la sesión de cierre", "Comment": "Comentario", "Common_Access": "Acceso común", "Community": "Comunidad", @@ -979,7 +987,7 @@ "Contact_Info": "Información de contacto", "Content": "Contenido", "Continue": "Continuar", - "Continuous_sound_notifications_for_new_livechat_room": "Notificaciones continuas de sonido para nuevas salas", + "Continuous_sound_notifications_for_new_livechat_room": "Notificaciones de sonido continuas para una nueva sala omnichannel", "Conversation": "Conversación", "Conversation_closed": "Conversación cerrada: __comment__.", "Conversation_closing_tags": "Etiquetas de cierre de la conversación", @@ -1114,7 +1122,7 @@ "Country_Kenya": "Kenia", "Country_Kiribati": "Kiribati", "Country_Korea_Democratic_Peoples_Republic_of": "República Popular Democrática de Corea", - "Country_Korea_Republic_of": "Corea, República de", + "Country_Korea_Republic_of": "República de Corea", "Country_Kuwait": "Kuwait", "Country_Kyrgyzstan": "Kirguistán", "Country_Lao_Peoples_Democratic_Republic": "República Democrática Popular Lao", @@ -1340,9 +1348,10 @@ "Days": "Días", "DB_Migration": "Migración de base de datos", "DB_Migration_Date": "Fecha de migración de base de datos", + "DDP_Rate_Limit": "Límite de tasa de DDP", "DDP_Rate_Limit_Connection_By_Method_Enabled": "Límite de Conexión por Método: habilitado", - "DDP_Rate_Limit_Connection_By_Method_Interval_Time": "Límite de Conexión por Método: intervalo de tiempo", - "DDP_Rate_Limit_Connection_By_Method_Requests_Allowed": "Límite de Conexión por Método: peticiones permitidas", + "DDP_Rate_Limit_Connection_By_Method_Interval_Time": "Límite por conexión por método: tiempo de intervalo", + "DDP_Rate_Limit_Connection_By_Method_Requests_Allowed": "Límite por conexión por método: solicitudes permitidas", "DDP_Rate_Limit_Connection_Enabled": "Límite por Conexión: habilitado", "DDP_Rate_Limit_Connection_Interval_Time": "Límite por Conexión: intervalo de tiempo", "DDP_Rate_Limit_Connection_Requests_Allowed": "Límite por conexión: solicitudes permitidas", @@ -1399,7 +1408,7 @@ "Desktop_Notifications_Default_Alert": "Alerta predeterminada de notificaciones de escritorio", "Desktop_Notifications_Disabled": "Las Notificaciones de Escritorio has sido Deshabilitadas. Cambia las preferencias en tu navegador si necesitas habilitar las Notificaciones.", "Desktop_Notifications_Duration": "Duración de las notificaciones del escritorio", - "Desktop_Notifications_Duration_Description": "Segundos para mostrar notificación de escritorio. Esto puede afectar el centro de notificaciones de OS X. Introduzca 0 para utilizar la configuración del navegador por defecto y no afectar Centro de Notificación X OS.", + "Desktop_Notifications_Duration_Description": "Segundos para mostrar la notificación de escritorio. Esto puede afectar al Centro de notificaciones de OS X. Ingrese 0 para usar la configuración predeterminada del navegador y no afectar al Centro de notificaciones de OS X.", "Desktop_Notifications_Enabled": "Las Notificaciones de Escritorio están Habilitadas", "Desktop_Notifications_Not_Enabled": "Las notificaciones de escritorio no están habilitadas", "Details": "Detalles", @@ -1414,9 +1423,9 @@ "Direct_Reply_Debug": "Respuesta directa a la depuración", "Direct_Reply_Debug_Description": "[Cuidado] Habilitar el modo de depuración mostraría su 'Contraseña de texto sin formato' en la consola de administración.", "Direct_Reply_Delete": "Eliminar correos electrónicos", - "Direct_Reply_Delete_Description": "[¡Atención!] Si se activa esta opción, todos los mensajes no leídos serán eliminados irrevocablemente, incluso los que no son respuestas directas. El buzón de correo electrónico configurado está entonces siempre vacío y no puede ser procesado en \"paralelo\" por humanos.", + "Direct_Reply_Delete_Description": "[¡Atención!] Si esta opción está activada, todos los mensajes no leídos se eliminan irrevocablemente, incluso aquellos que no son respuestas directas. El buzón de correo electrónico configurado está siempre vacío y no puede ser procesado en \"paralelo\" por humanos.", "Direct_Reply_Enable": "Habilitar respuesta directa", - "Direct_Reply_Enable_Description": "[¡Atención!] Si \"Respuesta Directa\" está habilitado, Rocket.Chat controlará el buzón de correo electrónico configurado. Todos los correos electrónicos no leídos son recuperados, marcados como leídos y procesados. \"Respuesta directa\" sólo debe ser activada si el buzón utilizado está destinado exclusivamente para el acceso de Rocket.Chat y no es leído/procesado \"en paralelo\" por humanos.", + "Direct_Reply_Enable_Description": "[¡Atención!] Si la \"Respuesta directa\" está habilitada, Rocket.Chat controlará el buzón de correo electrónico configurado. Todos los correos electrónicos no leídos se recuperan, se marcan como leídos y se procesan. La \"Respuesta directa\" solo debe activarse si el buzón de correo utilizado está destinado exclusivamente para el acceso de Rocket.Chat y no es leído / procesado \"en paralelo\" por humanos.", "Direct_Reply_Frequency": "Frecuencia de verificación de correo electrónico", "Direct_Reply_Frequency_Description": "(en minutos, por defecto / mínimo 2)", "Direct_Reply_Host": "Host de respuesta directa", @@ -1471,7 +1480,7 @@ "Domains": "Dominios", "Domains_allowed_to_embed_the_livechat_widget": "Lista de dominios separados por comas que permite incrustar el widget LiveChat. Déjelo en blanco para permitir todos los dominios.", "Dont_ask_me_again": "¡No me preguntes otra vez!", - "Dont_ask_me_again_list": "No me preguntes de nuevo", + "Dont_ask_me_again_list": "Lista de 'No me preguntes de nuevo'", "Download": "Descargar", "Download_Info": "Descargar información", "Download_My_Data": "Descargar mis datos (HTML)", @@ -1484,7 +1493,7 @@ "Dry_run_description": "Se enviara únicamente un correo electrónico, a la misma dirección establecida en el campo De. El correo electrónico debe pertenecer a un usuario valido.", "Duplicate_archived_channel_name": "Ya existe un canal archivado con el nombre ' %s' ", "Duplicate_archived_private_group_name": "Ya existe un grupo privado archivado con el nombre ' %s' ", - "Duplicate_channel_name": "Ya existe un canal con el nombre '%s' ", + "Duplicate_channel_name": "Ya existe un Channel con el nombre '%s' ", "Duplicate_file_name_found": "Se encontró un nombre de archivo duplicado.", "Duplicate_private_group_name": "Ya existe un Grupo Privado con el nombre '%s' ", "Duplicated_Email_address_will_be_ignored": "Se ignorará la dirección de correo electrónico duplicada.", @@ -1498,12 +1507,12 @@ "E2E_Enabled_Default_DirectRooms": "Habilitar la encriptación para las Rooms directas por defecto", "E2E_Enabled_Default_PrivateRooms": "Habilitar la encriptación para las Rooms privadas por defecto", "E2E_Encryption_Password_Change": "Cambiar la contraseña de cifrado", - "E2E_Encryption_Password_Explanation": "Ahora puede crear grupos privados codificados y mensajes directos. También puede cambiar los grupos privados o mensajes directos existentes a cifrados.

      Esto es cifrado de extremo a extremo, así que la clave para cifrar/descifrar tus mensajes no se guardará en el servidor. Por esa razón necesitas guardar tu contraseña en un lugar seguro. Se te pedirá que la introduzcas en otros dispositivos en los que desees utilizar la encriptación de E2E.", + "E2E_Encryption_Password_Explanation": "Ahora puede crear grupos privados cifrados y mensajes directos. También puede cambiar los grupos privados o DM existentes a cifrados.

      Este es un cifrado de extremo a extremo, por lo que la clave para codificar / decodificar sus mensajes no se guardará en el servidor. Por esa razón, debe guardar su contraseña en un lugar seguro. Se le pedirá que lo ingrese en otros dispositivos en los que desee usar el cifrado e2e.", "E2E_key_reset_email": "Notificación de reseteo de clave E2E", "E2E_password_request_text": "Para acceder a sus grupos privados cifrados y a los mensajes directos, introduzca su contraseña de cifrado.
      Necesitas introducir esta contraseña para cifrar/descifrar tus mensajes en cada cliente que utilices, ya que la clave no se almacena en el servidor.", - "E2E_password_reveal_text": "Ahora puede crear grupos privados codificados y mensajes directos. También puedes cambiar los grupos privados o mensajes directos existentes a cifrados.

      Esto es cifrado de extremo a extremo, así que la clave para cifrar/descifrar tus mensajes no se guardará en el servidor. Por esa razón necesitas almacenar esta contraseña en un lugar seguro. Se te pedirá que la introduzcas en otros dispositivos en los que desees utilizar cifrado E2E. ¡Aprende más aquí!

      Tu contraseña es: %s

      Esta es una contraseña autogenerada, puedes configurar una nueva contraseña para tu clave de cifrado en cualquier momento desde cualquier navegador en el que hayas introducido la contraseña existente.
      Esta contraseña sólo se almacena en este navegador hasta que guardes la contraseña y desestimes este mensaje.", + "E2E_password_reveal_text": "Ahora puede crear grupos privados cifrados y mensajes directos. También puede cambiar los grupos privados o DM existentes a cifrados.

      Este es un cifrado de extremo a extremo, por lo que la clave para codificar / decodificar sus mensajes no se guardará en el servidor. Por esa razón, debe guardar esta contraseña en un lugar seguro. Se le pedirá que lo ingrese en otros dispositivos en los que desee usar el cifrado e2e. ¡Obtenga más información aquí!

      Su contraseña es: % s

      Esta es una contraseña generada automáticamente, puede configurar una nueva contraseña para su clave de cifrado en cualquier momento desde cualquier navegador que haya ingresado la contraseña existente.
      Esta contraseña solo se almacena en este navegador hasta que la guarde y descarte este mensaje.", "E2E_Reset_Email_Content": "Se ha desconectado automáticamente. Cuando vuelva a iniciar sesión, Rocket.Chat generará una nueva clave y restaurará su acceso a cualquier sala cifrada que tenga uno o más miembros en línea. Debido a la naturaleza del cifrado E2E, Rocket.Chat no podrá restaurar el acceso a ninguna sala cifrada que no tenga miembros en línea.", - "E2E_Reset_Key_Explanation": "Esta opción eliminará su clave E2E actual y cerrará la sesión.
      Cuando vuelva a iniciar sesión, Rocket.Chat le generará una nueva clave y restablecerá su acceso a cualquier sala cifrada que tenga uno o más miembros en línea.
      Debido a la naturaleza del cifrado E2E, Rocket.Chat no podrá restaurar el acceso a ninguna sala cifrada que no tenga miembros en línea", + "E2E_Reset_Key_Explanation": "Esta opción eliminará su clave E2E actual y cerrará la sesión.
      Cuando vuelva a iniciar sesión, Rocket.Chat le generará una nueva clave y restaurará su acceso a cualquier sala cifrada que tenga uno o más miembros en línea.
      Debido a la naturaleza del cifrado E2E, Rocket.Chat no podrá restaurar el acceso a ninguna sala cifrada que no tenga miembros en línea", "E2E_Reset_Other_Key_Warning": "Restablecer la clave E2E actual cerrará la sesión del usuario. Cuando el usuario vuelve a iniciar sesión, Rocket.Chat generará una nueva clave y restaurará el acceso del usuario a cualquier sala cifrada que tenga uno o más miembros en línea. Debido a la naturaleza del cifrado E2E, Rocket.Chat no podrá restaurar el acceso a ninguna sala cifrada que no tenga miembros en línea.", "ECDH_Enabled": "Habilite el cifrado de segunda capa para el transporte de datos", "Edit": "Editar", @@ -1517,10 +1526,10 @@ "Edit_Priority": "Editar prioridad", "Edit_Status": "Editar estado", "Edit_Tag": "Editar etiqueta", - "Edit_Trigger": "Editar disparador", + "Edit_Trigger": "Editar activador", "Edit_Unit": "Editar unidad", "Edit_User": "Editar usuario", - "edit-livechat-room-customfields": "Editar campos personalizados de sala Omnichannel", + "edit-livechat-room-customfields": "Editar campos personalizados de Omnichannel Room", "edit-livechat-room-customfields_description": "Permiso para editar los campos personalizados de la sala Omnichannel", "edit-message": "Editar Mensaje", "edit-message_description": "Permiso para editar un mensaje dentro de una sala", @@ -1531,7 +1540,7 @@ "edit-other-user-e2ee": "Editar el cifrado E2E de otro usuario", "edit-other-user-e2ee_description": "Permiso para editar el cifrado E2E de otro usuario", "edit-other-user-info": "Editar la información de otro usuario", - "edit-other-user-info_description": "Permiso para cambiar el nombre, nombre de usuario o dirección de correo electrónico de otro usuario.", + "edit-other-user-info_description": "Permiso para cambiar el nombre, el nombre de usuario o la dirección de correo electrónico de otro usuario.", "edit-other-user-password": "Editar la contraseña de otro usuario", "edit-other-user-password_description": "Permiso para modificar las contraseñas de otros usuarios. Requiere permiso edit-other-user-info.", "edit-other-user-totp": "Editar el doble factor TOTP de otro usuario", @@ -1556,11 +1565,11 @@ "Email_address_to_send_offline_messages": "Dirección de correo electrónico para enviar mensajes fuera de línea", "Email_already_exists": "El correo electrónico ya existe", "Email_body": "Cuerpo del Correo electrónico", - "Email_Change_Disabled": "Su administrador ha deshabilitado el cambio de correo electrónico", - "Email_Changed_Description": "Puede utilizar los siguientes marcadores de posición:
      • [email] para el correo electrónico del usuario.
      • [Site_Name] y [Site_URL] para el nombre de la aplicación y la URL respectivamente.
      ", + "Email_Change_Disabled": "Su administrador de Rocket.Chat ha desactivado el cambio de correo electrónico", + "Email_Changed_Description": "Puede utilizar los siguientes marcadores de posición:
      • [email] para el correo electrónico del usuario.
      • [Site_Name] y [Site_URL] para el nombre de la aplicación y la URL respectivamente.
      ", "Email_Changed_Email_Subject": "[Site_Name] - La dirección de correo electrónico ha sido modificada", "Email_changed_section": "Dirección de correo electrónico modificada", - "Email_Footer_Description": "Es posible utilizar los siguientes marcadores:
      • [Site_Name] y [Site_URL] para el nombre de la aplicación y la URL, respectivamente.
      ", + "Email_Footer_Description": "Puede utilizar los siguientes marcadores de posición:
      • [Site_Name] y [Site_URL] para el nombre de la aplicación y la URL respectivamente.
      ", "Email_from": "De", "Email_Header_Description": "Es posible utilizar los siguientes marcadores:
      • [Site_Name] y [Site_URL] para el nombre de la aplicación y la URL, respectivamente.
      ", "Email_Inbox": "Bandeja de entrada de correo electrónico", @@ -1603,11 +1612,13 @@ "Encrypted": "Cifrado", "Encrypted_channel_Description": "Canal cifrado de extremo a extremo. La búsqueda no funcionará con canales cifrados y es posible que las notificaciones no muestren el contenido de los mensajes.", "Encrypted_message": "Mensaje cifrado", - "Encrypted_setting_changed_successfully": "La configuración encriptada se modificó correctamente", + "Encrypted_setting_changed_successfully": "La configuración encriptada se cambió correctamente", "Encrypted_not_available": "No disponible para Channels Públicos", - "Encryption_key_saved_successfully": "Su clave de encriptación se guardó correctamente", + "Encryption_key_saved_successfully": "su clave de cifrado se guardó correctamente.", "EncryptionKey_Change_Disabled": "No puede establecer una contraseña para su clave de cifrado porque su clave privada no está presente en este cliente. Para establecer una nueva contraseña, necesita cargar su clave privada usando su contraseña existente o usar un cliente donde la clave ya esté cargada.", "End": "Fin", + "End_call": "Finalizar llamada", + "Expand_view": "Expandir vista", "End_OTR": "Finalizar OTR", "Engagement_Dashboard": "Panel de participación", "Enter": "Entrar", @@ -1629,7 +1640,7 @@ "Enter_your_E2E_password": "Introduzca su contraseña E2E", "Enterprise": "Empresa", "Enterprise_License": "Licencia de empresa", - "Enterprise_License_Description": "Si tu espacio de trabajo está registrado y dispone de una licencia proporcionada por Rocket.Chat Cloud no es necesario actualizar esta licencia.", + "Enterprise_License_Description": "Si su espacio de trabajo está registrado y la licencia la proporciona Rocket.Chat Cloud, no es necesario que actualice manualmente la licencia aquí.", "Entertainment": "Entretenimiento", "Error": "Error", "Error_404": "Error 404", @@ -1671,13 +1682,13 @@ "error-field-unavailable": "__field__ ya está en uso :(", "error-file-too-large": "El archivo es demasiado grande", "error-forwarding-chat": "Se produjo un error al reenviar el chat. Vuelve a intentarlo más tarde.", - "error-forwarding-chat-same-department": "El departamento seleccionado y el actual departamento de la sala son los mismos", + "error-forwarding-chat-same-department": "El departamento seleccionado y el departamento de sala actual son el mismo", "error-forwarding-department-target-not-allowed": "No se permite el reenvío al departamento de destino.", "error-guests-cant-have-other-roles": "Los usuarios invitados no pueden tener ningún otro rol.", "error-import-file-extract-error": "No se pudo extraer el archivo de importación.", "error-import-file-is-empty": "El archivo importado parece estar vacío.", "error-import-file-missing": "No se encontró el archivo a importar en la ruta especificada.", - "error-importer-not-defined": "El importador no se definió correctamente, no se encuentra la Clase de Importación.", + "error-importer-not-defined": "El importador no se definió correctamente, falta la clase Import.", "error-input-is-not-a-valid-field": "__input__ no es un __field__ válido", "error-invalid-account": "Cuenta no válida", "error-invalid-actionlink": "Enlace de acción inválido", @@ -1709,7 +1720,7 @@ "error-invalid-permission": "Permiso no válido", "error-invalid-port-number": "Número de puerto no válido", "error-invalid-priority": "Prioridad inválida", - "error-invalid-redirectUri": "redirectUri no válida", + "error-invalid-redirectUri": "RedirectUri no válido", "error-invalid-role": "Rol no válido", "error-invalid-room": "Sala no válida", "error-invalid-room-name": "__room_name__ no es un nombre válido de sala, utilice sólo letras, números, guiones y guiones bajos", @@ -1745,7 +1756,7 @@ "error-password-policy-not-met-oneNumber": "La contraseña no cumple con la política del servidor de al menos un carácter numérico", "error-password-policy-not-met-oneSpecial": "La contraseña no cumple con la política del servidor de al menos un carácter especial", "error-password-policy-not-met-oneUppercase": "La contraseña no cumple con la política del servidor de al menos un carácter en mayúscula", - "error-password-policy-not-met-repeatingCharacters": "La contraseña no cumple con la política del servidor de caracteres repetitivos prohibidos (tiene demasiados caracteres iguales uno al lado del otro)", + "error-password-policy-not-met-repeatingCharacters": "La contraseña no cumple con la política del servidor de caracteres repetidos prohibidos (tiene demasiados caracteres iguales uno al lado del otro)", "error-password-same-as-current": "Ingresó la misma contraseña que la contraseña actual", "error-personal-access-tokens-are-current-disabled": "Las claves de acceso personales están actualmente desactivadas", "error-pinning-message": "No se pudo fijar el mensaje", @@ -1753,18 +1764,18 @@ "error-remove-last-owner": "Este es el último propietario. Por favor, establece un nuevo propietario antes de eliminarlo.", "error-returning-inquiry": "Error al devolver la consulta a la cola", "error-role-in-use": "No puede eliminar el rol porque esta en uso", - "error-role-name-required": "El nombre de rol es requerido", + "error-role-name-required": "Se requiere nombre de Rol", "error-role-already-present": "Ya existe un rol con este nombre", "error-room-is-not-closed": "La sala no está cerrada", "error-room-onHold": "¡Error! Room está en espera", "error-selected-agent-room-agent-are-same": "El agente seleccionado y el agente de la sala son los mismos", "error-starring-message": "No se pudo mirar el mensaje", "error-tags-must-be-assigned-before-closing-chat": "Se deben asignar etiquetas antes de cerrar el chat", - "error-the-field-is-required": " E campo __field__. es requerido", + "error-the-field-is-required": "El campo __field__ es obligatorio.", "error-this-is-not-a-livechat-room": "Esta no es una sala de Omnichannel", "error-token-already-exists": "Ya existe un token con este nombre", "error-token-does-not-exists": "El token no existe", - "error-too-many-requests": "Error, demasiadas peticiones. Por favor más despacio. Debe esperar __seconds__ segundos antes de volver a intentarlo.", + "error-too-many-requests": "Error, demasiadas solicitudes. Por favor más despacio. Debe esperar __seconds__ segundos antes de volver a intentarlo.", "error-transcript-already-requested": "Transcripción ya solicitada", "error-unpinning-message": "No se pudo desanclar el mensaje", "error-user-has-no-roles": "El usuario no tiene roles", @@ -1779,7 +1790,7 @@ "error-validating-department-chat-closing-tags": "Se requiere al menos una etiqueta de cierre cuando el departamento requiere etiqueta(s) en las conversaciones de cierre.", "error-no-permission-team-channel": "No tienes permiso para agregar este canal al equipo.", "error-no-owner-channel": "Solo los propietarios pueden agregar este canal al equipo", - "error-you-are-last-owner": "Usted es el último propietario. Por favor, establezca un nuevo propietario antes de salir de la Sala.", + "error-you-are-last-owner": "Eres el último dueño. Establezca un nuevo propietario antes de salir de la Sala.", "Errors_and_Warnings": "Errores y advertencias", "Esc_to": "Esc a", "Estimated_due_time": "Tiempo estimado de espera (tiempo en minutos)", @@ -1800,14 +1811,14 @@ "Example_s": "Ejemplo: %s", "except_pinned": "(excepto aquellos que están fijados)", "Exclude_Botnames": "Excluir Bots", - "Exclude_Botnames_Description": "No propagar los mensajes de bots cuyos nombres coincidan con la expresión regular. Se se deja en blanco, todos los mensajes de los bots se propagarán.", + "Exclude_Botnames_Description": "No propague mensajes de bots cuyo nombre coincida con la expresión regular anterior. Si se deja en blanco, se propagarán todos los mensajes de los bots.", "Exclude_pinned": "Excluir mensajes fijados", "Execute_Synchronization_Now": "Ejecutar sincronización ahora", "Exit_Full_Screen": "Salir de pantalla completa", "Expand": "Expandir", "Experimental_Feature_Alert": "¡Esta es una función experimental! Tenga en cuenta que puede cambiar, romperse o incluso eliminarse en el futuro sin previo aviso.", "Expiration": "Expiración", - "Expiration_(Days)": "Expiración (días)", + "Expiration_(Days)": "Caducidad (días)", "Export_as_file": "Exportar como archivo", "Export_Messages": "Exportar mensajes", "Export_My_Data": "Exportar mis datos (jSON)", @@ -1823,19 +1834,21 @@ "Facebook_Page": "Pagina de Facebook", "Failed": "Error", "Failed_to_activate_invite_token": "Error al activar el token de invitación", - "Failed_to_add_monitor": "Error al añadir monitor", + "Failed_to_add_monitor": "No se pudo agregar el monitor", "Failed_To_Download_Files": "Error al descargar ficheros", "Failed_to_generate_invite_link": "Error al generar enlace de invitación", "Failed_To_Load_Import_Data": "Error al cargar importación de datos", "Failed_To_Load_Import_History": "Error al cargar importación de histórico", "Failed_To_Load_Import_Operation": "Error al cargar operación de importación", "Failed_To_Start_Import": "Error al iniciar operación de importación", - "Failed_to_validate_invite_token": "Error al validar token de invitación", + "Failed_to_validate_invite_token": "Error al validar el token de invitación", "False": "Falso", "Favorite": "Favorito", "Favorite_Rooms": "Habilitar salas favoritas", "Favorites": "Favoritos", + "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "Esta función depende del proveedor de llamadas seleccionado anteriormente que se habilitará desde la configuración de administración.", "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "Esta función depende de \"Enviar el historial de navegación de visitantes como un mensaje\" para que se habilite.", + "Feature_Limiting": "Limitación de funciones", "Features": "Características", "Features_Enabled": "Funcionalidades habilitadas", "Feature_Disabled": "Característica deshabilitada", @@ -1880,10 +1893,10 @@ "FEDERATION_Discovery_Method_Description": "Puede usar el hub o un SRV y una entrada TXT en sus registros DNS.", "FEDERATION_Domain": "Dominio", "FEDERATION_Domain_Alert": "No cambie esto después de habilitar la función, todavía no podemos manejar los cambios de dominio.", - "FEDERATION_Domain_Description": "Añada el dominio al que este servidor debería estar vinculado, por ejemplo: @rocket.chat.", - "FEDERATION_Enabled": "Intentar integrar la federación de soporte.", + "FEDERATION_Domain_Description": "Agregue el dominio al que debe estar vinculado este servidor, por ejemplo: @ rocket.chat", + "FEDERATION_Enabled": "Intente integrar el soporte de la federación.", "FEDERATION_Enabled_Alert": "La federación de soporte está en progreso. Su uso en un entorno de producción no se recomienda de momento.", - "FEDERATION_Error_user_is_federated_on_rooms": "No se puede eliminar a los usuarios federados que pertenecen a las salas", + "FEDERATION_Error_user_is_federated_on_rooms": "No puede eliminar usuarios federados que pertenecen a salas", "FEDERATION_Hub_URL": "URL del Hub", "FEDERATION_Hub_URL_Description": "Configure la URL del concentrador, por ejemplo: https://hub.rocket.chat. También se aceptan puertos.", "FEDERATION_Public_Key": "Clave pública", @@ -1897,7 +1910,7 @@ "FEDERATION_Unique_Id_Description": "Este es el ID único de su federación, que se utiliza para identificar a su par en la red.", "Field": "Campo", "Field_removed": "Campo eliminado", - "Field_required": "Campo requerido", + "Field_required": "Campo obligatorio", "File": "Archivo", "File_Downloads_Started": "Descargas de archivos iniciadas", "File_exceeds_allowed_size_of_bytes": "El archivo supera el tamaño permitido de __size__ ", @@ -1931,10 +1944,10 @@ "FileUpload_FileSystemPath": "Ruta del sistema", "FileUpload_GoogleStorage_AccessId": "ID de acceso de almacenamiento de Google", "FileUpload_GoogleStorage_AccessId_Description": "El identificador de acceso generalmente está en un formato de correo electrónico, por ejemplo: \"example-test@example.iam.gserviceaccount.com\"", - "FileUpload_GoogleStorage_Bucket": "Nombre del bucket de Google", + "FileUpload_GoogleStorage_Bucket": "Nombre del Bucket Google Storage", "FileUpload_GoogleStorage_Bucket_Description": "Nombre del bucket en el que deben cargarse los archivos.", "FileUpload_GoogleStorage_Proxy_Avatars": "Avatares Proxy", - "FileUpload_GoogleStorage_Proxy_Avatars_Description": "Transmisiones de archivos proxy avatar a través de su servidor en lugar de acceso directo a la URL del activo", + "FileUpload_GoogleStorage_Proxy_Avatars_Description": "Transmisiones de archivos de avatar de proxy a través de su servidor en lugar de acceso directo a la URL del activo", "FileUpload_GoogleStorage_Proxy_Uploads": "Subidas de Proxy", "FileUpload_GoogleStorage_Proxy_Uploads_Description": "Proxy carga transmisiones de archivos a través de su servidor en lugar de acceso directo a la URL del activo", "FileUpload_GoogleStorage_Secret": "Secreto de almacenamiento de Google", @@ -1955,10 +1968,10 @@ "FileUpload_RotateImages_Description": "Habilitar esta configuración puede causar pérdida de calidad de imagen", "FileUpload_S3_Acl": "Amazon S3 acl", "FileUpload_S3_AWSAccessKeyId": "Access Key", - "FileUpload_S3_AWSSecretAccessKey": "Secret Key", + "FileUpload_S3_AWSSecretAccessKey": "Clave Secreta", "FileUpload_S3_Bucket": "Nombre de Bucket", "FileUpload_S3_BucketURL": "Bucket URL", - "FileUpload_S3_CDN": "Dominio CDN para Descargas", + "FileUpload_S3_CDN": "Dominio CDN para descargas", "FileUpload_S3_ForcePathStyle": "Estilo de ruta de fuerza", "FileUpload_S3_Proxy_Avatars": "Avatares Proxy", "FileUpload_S3_Proxy_Avatars_Description": "Proxy transmisiones de archivos de avatar a través de su servidor en lugar de acceso directo a la URL del activo", @@ -1967,14 +1980,14 @@ "FileUpload_S3_Region": "Región", "FileUpload_S3_SignatureVersion": "Versión de firma", "FileUpload_S3_URLExpiryTimeSpan": "Tiempo de caducidad de las URLs", - "FileUpload_S3_URLExpiryTimeSpan_Description": "Tiempo después el cual las direcciones de Amazon S3 generadas dejarán de ser válidas (en segundos). Si se establece a menos de 5 segundos, este campo será ignorado.", + "FileUpload_S3_URLExpiryTimeSpan_Description": "Tiempo después del cual las URL generadas por Amazon S3 ya no serán válidas (en segundos). Si se establece en menos de 5 segundos, este campo se ignorará.", "FileUpload_Storage_Type": "Tipo de Almacenamiento", "FileUpload_Webdav_Password": "Contraseña de WebDAV", "FileUpload_Webdav_Proxy_Avatars": "Avatares Proxy", - "FileUpload_Webdav_Proxy_Avatars_Description": "Transmisiones de archivos de avatar a través de su servidor en lugar de acceso directo a la URL del activo", + "FileUpload_Webdav_Proxy_Avatars_Description": "Transmisiones de archivos de avatar de proxy a través de su servidor en lugar de acceso directo a la URL del activoivo", "FileUpload_Webdav_Proxy_Uploads": "Subidas de Proxy", "FileUpload_Webdav_Proxy_Uploads_Description": "Transmisiones de archivos de carga proxy a través de su servidor en lugar de acceso directo a la URL del activo", - "FileUpload_Webdav_Server_URL": "URL de acceso al servidor WebDAV", + "FileUpload_Webdav_Server_URL": "UURL de acceso al servidor WebDAV", "FileUpload_Webdav_Upload_Folder_Path": "Cargar ruta de carpeta", "FileUpload_Webdav_Upload_Folder_Path_Description": "Ruta de la carpeta WebDAV en la que se deben cargar los archivos", "FileUpload_Webdav_Username": "Nombre de usuario WebDAV", @@ -1997,20 +2010,20 @@ "For_more_details_please_check_our_docs": "Para obtener más detalles, consulte nuestros documentos.", "For_your_security_you_must_enter_your_current_password_to_continue": "Por su seguridad, debe introducir su contraseña actual para continuar", "Force_Disable_OpLog_For_Cache": "Forzar la desactivación de OpLog para caché", - "Force_Disable_OpLog_For_Cache_Description": "No usará OpLog para sincronizar el caché, incluso cuando esté disponible", + "Force_Disable_OpLog_For_Cache_Description": "No usará OpLog para sincronizar la caché incluso cuando esté disponible", "Force_Screen_Lock": "Forzar bloqueo de pantalla", "Force_Screen_Lock_After": "Forzar bloqueo de pantalla después de", "Force_Screen_Lock_After_description": "El tiempo para solicitar la contraseña nuevamente después de finalizar la última sesión, en segundos.", "Force_Screen_Lock_description": "Cuando esté habilitado, obligará a sus usuarios a usar un PIN / BIOMETRÍA / FACEID para desbloquear la aplicación.", "Force_SSL": "Forzar SSL", - "Force_SSL_Description": "* Precaución! * _Force SSL_ nunca debe ser usado con proxy inverso. Si usted tiene un proxy inverso, debería hacer la redirección AHÍ. Esta opción existe para los despliegues como Heroku, que no permite la configuración de redirección en el proxy inverso.", + "Force_SSL_Description": "* ¡Precaución! * _Force SSL_ nunca debe usarse con proxy inverso. Si tiene un proxy inverso, debe realizar la redirección ALLÍ. Esta opción existe para implementaciones como Heroku, que no permite la configuración de redireccionamiento en el proxy inverso.", "Force_visitor_to_accept_data_processing_consent": "Obligar al visitante a aceptar el consentimiento del procesamiento de datos", "Force_visitor_to_accept_data_processing_consent_description": "Los visitantes no pueden empezar a chatear sin consentimiento.", "Force_visitor_to_accept_data_processing_consent_enabled_alert": "El acuerdo con el procesamiento de datos debe basarse en una comprensión transparente del motivo del procesamiento. Debido a esto, debe completar la configuración a continuación que se mostrará a los usuarios para proporcionar las razones para recopilar y procesar su información personal.", "force-delete-message": "Forzar borrar mensaje", "force-delete-message_description": "Permiso para eliminar un mensaje que pasa por alto todas las restricciones", "Forgot_password": "¿Olvidó su contraseña?", - "Forgot_Password_Description": "Puede usar los siguientes marcadores de posición:
      • [Forgot_Password_Url] para la URL de recuperación de contraseña.
      • [nombre], [fname], [lname] para el nombre completo, el nombre o el apellido del usuario, respectivamente.
      • [email] para el correo electrónico del usuario.
      • [Site_Name] y [Site_URL] para el nombre de la aplicación y la URL, respectivamente.
      ", + "Forgot_Password_Description": "Puede utilizar los siguientes marcadores de posición:
      • [Forgot_Password_Url] para la URL de recuperación de contraseña.
      • [name], [fname] , [lname] para el nombre completo, nombre o apellido del usuario, respectivamente.
      • [email] para el correo electrónico del usuario.
      • [Site_Name ] y [Site_URL] para el nombre de la aplicación y la URL respectivamente.
      ", "Forgot_Password_Email": "Haga clic en aquí para restablecer su contraseña.", "Forgot_Password_Email_Subject": "[Site_Name] - Recuperación de contraseña", "Forgot_password_section": "Olvidó su contraseña", @@ -2050,7 +2063,7 @@ "Global_purge_override_warning": "Existe una política de retención global. Si deja desactivada la opción \"Anular la política de retención global\", solo puede aplicar una política que sea más estricta que la política global.", "Global_Search": "Búsqueda global", "Go_to_your_workspace": "Ve a tu espacio de trabajo", - "GoogleCloudStorage": "Google Cloud Storage", + "GoogleCloudStorage": "Almacenamiento en la nube de Google", "GoogleNaturalLanguage_ServiceAccount_Description": "Clave de la cuenta de servicio archivo JSON. Puede encontrar más información [aquí] (https://cloud.google.com/natural-language/docs/common/auth#set_up_a_service_account)", "GoogleTagManager_id": "Id del Administrador de etiquetas de Google", "Government": "Gobierno", @@ -2088,6 +2101,7 @@ "Hide_System_Messages": "Ocultar mensajes de sistema", "Hide_Unread_Room_Status": "Ocultar el estado de una sala no leída", "Hide_usernames": "Ocultar nombres de usuario", + "Hide_video": "Ocultar video", "Highlights": "Destacados", "Highlights_How_To": "Para ser notificado cuando alguien menciona una palabra o frase, añadir aquí. Puede separar las palabras o frases con comas. Resaltar palabras no distingue entre mayúsculas y minúsculas.", "Highlights_List": "Resaltar palabras", @@ -2098,11 +2112,11 @@ "hours": "horas", "Hours": "Horas", "How_friendly_was_the_chat_agent": "¿Ha sido amable el agente de chat?", - "How_knowledgeable_was_the_chat_agent": "¿Cuánto sabía el agente de chat?", + "How_knowledgeable_was_the_chat_agent": "¿Qué tan informado estaba el agente de chat?", "How_long_to_wait_after_agent_goes_offline": "Cuánto tiempo esperar después de que el agente se desconecte", "How_long_to_wait_to_consider_visitor_abandonment": "¿Cuánto tiempo esperar para considerar el abandono de visitantes?", "How_long_to_wait_to_consider_visitor_abandonment_in_seconds": "¿Cuánto tiempo hay que esperar para considerar el abandono de visitantes?", - "How_responsive_was_the_chat_agent": "¿Cómo de rápido ha respondido nuestro agente de chat?", + "How_responsive_was_the_chat_agent": "¿Qué tan receptivo fue el agente de chat?", "How_satisfied_were_you_with_this_chat": "¿Cómo de satisfecho está con esta conversación?", "How_to_handle_open_sessions_when_agent_goes_offline": "Cómo manejar sesiones abiertas cuando el agente se desconecta", "I_Saved_My_Password": "He guardado mi contraseña", @@ -2114,16 +2128,16 @@ "If_you_are_sure_type_in_your_username": "Si está seguro ingrese su nombre de usuario:", "If_you_didnt_ask_for_reset_ignore_this_email": "Si no solicitó el restablecimiento de su contraseña, puede ignorar este correo electrónico.", "If_you_didnt_try_to_login_in_your_account_please_ignore_this_email": "Si no intentó iniciar sesión en su cuenta, ignore este correo electrónico.", - "If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours": "Si no tiene uno, envíe un correo electrónico a [omni@rocket.chat](mailto: omni@rocket.chat) para obtener el suyo.", + "If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours": "Si no tiene uno, envíe un correo electrónico a [omni@rocket.chat] (mailto: omni@rocket.chat) para obtener el suyo.", "Iframe_Integration": "Integración iframe", - "Iframe_Integration_receive_enable": "Habilitar Recibir", + "Iframe_Integration_receive_enable": "Habilitar recibir", "Iframe_Integration_receive_enable_Description": "Permitir que la ventana principal envíe comandos a Rocket.Chat.", "Iframe_Integration_receive_origin": "Recibir orígenes", - "Iframe_Integration_receive_origin_Description": "Orígenes con prefijo de protocolo, separados por comas, que pueden recibir comandos, p. 'https://localhost, http://localhost', o * para permitir la recepción desde cualquier lugar.", + "Iframe_Integration_receive_origin_Description": "Orígenes con prefijo de protocolo, separados por comas, que pueden recibir comandos, p. Ej. 'https: // localhost, http: // localhost', o * para permitir recibir desde cualquier lugar.", "Iframe_Integration_send_enable": "Habilitar envío", "Iframe_Integration_send_enable_Description": "Enviar eventos a la ventana principal", "Iframe_Integration_send_target_origin": "Enviar origen de destino", - "Iframe_Integration_send_target_origin_Description": "Origen con prefijo de protocolo, cuyos comandos se envían a, p. 'https://localhost', o * para permitir el envío a cualquier parte.", + "Iframe_Integration_send_target_origin_Description": "Origen con prefijo de protocolo, al que se envían los comandos, p. Ej. 'https: // localhost', o * para permitir el envío a cualquier lugar.", "Iframe_Restrict_Access": "Restringir el acceso dentro de cualquier iframe", "Iframe_Restrict_Access_Description": "Esta configuración habilita / deshabilita las restricciones para cargar el RC dentro de cualquier iframe", "Iframe_X_Frame_Options": "Opciones de X-Frame-Options", @@ -2144,7 +2158,7 @@ "Importer_CSV_Information": "El importador de CSV requiere un formato específico; lea la documentación sobre cómo estructurar su archivo zip:", "Importer_done": "¡Importación terminada!", "Importer_ExternalUrl_Description": "También puede utilizar una URL para un archivo de acceso público:", - "Importer_finishing": "Finalizando la importación.", + "Importer_finishing": "Terminando la importación.", "Importer_From_Description": "Las importaciones __from__ datos's en Rocket.Chat.", "Importer_HipChatEnterprise_BetaWarning": "Tenga en cuenta que esta importación sigue siendo un trabajo en progreso. Informe cualquier error que ocurra en GitHub:", "Importer_HipChatEnterprise_Information": "El archivo cargado debe ser un tar.gz descifrado, lea la documentación para obtener más información:", @@ -2159,7 +2173,7 @@ "Importer_not_setup": "El importador no está configurado correctamente, ya que no devolvió ningún dato.", "Importer_Prepare_Restart_Import": "Reiniciar importación", "Importer_Prepare_Start_Import": "Iniciar importación", - "Importer_Prepare_Uncheck_Archived_Channels": "Descarmar salas archivadas", + "Importer_Prepare_Uncheck_Archived_Channels": "Desmarcar Channel archivadas", "Importer_Prepare_Uncheck_Deleted_Users": "Desmarcar usuarios eliminados", "Importer_progress_error": "No se pudo obtener el progreso de la importación.", "Importer_setup_error": "Se produjo un error al configurar el importador.", @@ -2184,7 +2198,7 @@ "importer_status_uploading": "Subiendo archivo", "importer_status_user_selection": "Preparado para seleccionar qué importar", "Importer_Upload_FileSize_Message": "La configuración de su servidor permite subir archivos de tamaño hasta __maxFileSize__.", - "Importer_Upload_Unlimited_FileSize": "La configuración de su servidor permite la subida de archivos de cualquier tamaño.", + "Importer_Upload_Unlimited_FileSize": "La configuración de su servidor permite la carga de archivos de cualquier tamaño.", "Importing_channels": "Importando canales", "Importing_Data": "Importando datos", "Importing_messages": "Importando mensajes", @@ -2203,19 +2217,19 @@ "Install": "Instalar", "Install_Extension": "Instalar Extensión", "Install_FxOs": "Instalar Rocket.Chat en su Firefox", - "Install_FxOs_done": "¡Genial! Ya puede comenzar a usarRocket.Chat mediante el ícono en su Escritorio. ¡Diviértase usando Rocket.Chat!", + "Install_FxOs_done": "¡Excelente! Ahora puede usar Rocket.Chat a través del ícono en su pantalla de inicio. Diviértete con Rocket.Chat!", "Install_FxOs_error": "Lo sentimos, ¡eso no funcionó como se esperaba! El siguiente error apareció:", "Install_FxOs_follow_instructions": "Por favor confirma la instalación de la aplicación en tu dispositivo (presione \"Instalar\" cuando se le solicite).", "Install_package": "Paquete de instalación", "Installation": "Instalación ", "Installed": "Instalado", - "Installed_at": "Instalación", + "Installed_at": "Instalación en", "Instance": "Instancia", "Instances": "Instancias", "Instances_health": "Estado de la Instancias", "Instance_Record": "Registro de instancia", "Instructions": "Instrucciones", - "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Instrucciones para sus visitantes completen el formulario para enviar un mensaje", + "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Instrucciones para su visitante complete el formulario para enviar un mensaje", "Insert_Contact_Name": "Inserte el nombre del contacto", "Insert_Placeholder": "Insertar marcador de posición", "Insurance": "Seguro", @@ -2235,23 +2249,23 @@ "Integration_Outgoing_WebHook_History_Http_Response_Error": "Error de respuesta HTTP", "Integration_Outgoing_WebHook_History_Messages_Sent_From_Prepare_Script": "Mensajes enviados desde Prepare Step Script", "Integration_Outgoing_WebHook_History_Messages_Sent_From_Process_Script": "Mensajes enviados desde el paso de respuesta del proceso", - "Integration_Outgoing_WebHook_History_Time_Ended_Or_Error": "Tiempo que terminó o error", + "Integration_Outgoing_WebHook_History_Time_Ended_Or_Error": "Hora de finalización o error", "Integration_Outgoing_WebHook_History_Time_Triggered": "Integración de tiempo activada", "Integration_Outgoing_WebHook_History_Trigger_Step": "Último paso de activación", - "Integration_Outgoing_WebHook_No_History": "Esta integración saliente de webhook aún no tiene ningún historial registrado.", + "Integration_Outgoing_WebHook_No_History": "Esta integración de webhook saliente aún no tiene ningún historial registrado.", "Integration_Retry_Count": "Reintentar recuento", - "Integration_Retry_Count_Description": "¿Cuántas veces debería intentarse la integración si falla la llamada a la url?", + "Integration_Retry_Count_Description": "¿Cuántas veces se debe intentar la integración si falla la llamada a la URL?", "Integration_Retry_Delay": "Delay del reintento", "Integration_Retry_Delay_Description": "¿Qué algoritmo de retraso debería usar el reintento? 10 ^ xo 2 ^ xo x * 2", "Integration_Retry_Failed_Url_Calls": "Reintentar llamadas de URL fallidas", - "Integration_Retry_Failed_Url_Calls_Description": "¿Debe la integración intentar un tiempo razonable si falla la llamada a la url?", + "Integration_Retry_Failed_Url_Calls_Description": "¿Debería intentar la integración una cantidad de tiempo razonable si falla la llamada a la URL?", "Integration_Run_When_Message_Is_Edited": "Ejecutar en ediciones", "Integration_Run_When_Message_Is_Edited_Description": "¿Debería ejecutarse la integración cuando se edite el mensaje? Al establecer esto en falso, la integración solo se ejecutará en nuevos mensajes.", "Integration_updated": "La Integración ha sido actualizada", "Integration_Word_Trigger_Placement": "Colocación de palabras en cualquier lugar", "Integration_Word_Trigger_Placement_Description": "¿Debería activarse la Palabra cuando se coloca en cualquier lugar de la oración que no sea el principio?", "Integrations": "Integraciones", - "Integrations_for_all_channels": "Introduzca all_public_channels para escuchar en todos los canales públicos, all_private_groups para escuchar en todos los grupos privados, y all_direct_messages para escuchar todos los mensajes directos.", + "Integrations_for_all_channels": "IIngrese all_public_channels para escuchar en todos los canales públicos, all_private_groups para escuchar en todos los grupos privados y all_direct_messages para escuchar todos los mensajes directos", "Integrations_Outgoing_Type_FileUploaded": "Archivo Subido", "Integrations_Outgoing_Type_RoomArchived": "Sala archivada", "Integrations_Outgoing_Type_RoomCreated": "Sala Creada (pública y privada)", @@ -2267,7 +2281,7 @@ "InternalHubot_reload": "Recargar los scripts", "InternalHubot_ScriptsToLoad": "Scripts a Cargar", "InternalHubot_ScriptsToLoad_Description": "Por favor introduzca una lista separada por comas de scripts a cargar desde su carpeta personalizada ", - "InternalHubot_Username_Description": "Este debe ser un nombre de usuario válido de un bot registrado en su servidor.", + "InternalHubot_Username_Description": "Debe ser un nombre de usuario válido de un bot registrado en su servidor.", "Invalid Canned Response": "Respuesta predefinida no válida", "Invalid_confirm_pass": "La confirmación de la contraseña no coincide con la contraseña", "Invalid_Department": "Departamento incorrecto", @@ -2290,7 +2304,7 @@ "Invitation": "Invitación", "Invitation_Email_Description": "Es posible utilizar los siguientes marcadores:
      • [email] para el correo electrónico del destinatario.
      • [Site_Name] y [Site_URL] para el nombre de la aplicación y la URL, respectivamente.
      ", "Invitation_HTML": "HTML de la Invitación", - "Invitation_HTML_Default": "

      Se le ha invitado a [Site_Name]

      Ir a [Site_URL] y probar la mejor solución de chat de código abierto disponibles en la actualidad!

      ", + "Invitation_HTML_Default": "

      Se le ha invitado a [Site_Name]

      Ir a [Site_URL]y probar la mejor solución de chat de código abierto disponibles en la actualidad!

      ", "Invitation_Subject": "Asunto de la Invitación", "Invitation_Subject_Default": "Se le ha invitado a [Site_Name]", "Invite": "Invitación", @@ -2311,7 +2325,7 @@ "IRC_Federation_Disabled": "La Federación IRC está desactivada.", "IRC_Hostname": "El servidor de host IRC para conectarse.", "IRC_Login_Fail": "Salida después de una conexión fallida al servidor IRC.", - "IRC_Login_Success": "Salida después de una conexión exitosa al servidor IRC.", + "IRC_Login_Success": "Salida tras una conexión exitosa al servidor IRC.", "IRC_Message_Cache_Size": "El límite de caché para el manejo de mensajes salientes.", "IRC_Port": "El puerto al que enlazar en el servidor host IRC.", "IRC_Private_Message": "Salida del comando PRIVMSG.", @@ -2337,12 +2351,14 @@ "Jitsi_Limit_Token_To_Room": "Limitar token a la Room Jitsi", "Job_Title": "Título Profesional", "join": "Unirse", + "Join_call": "Unirse a la llamada", "Join_audio_call": "Unirse a la llamada", "Join_Chat": "Unirse al chat", "Join_default_channels": "Unirse a los canales predeterminados", "Join_the_Community": "Únete a la Comunidad", "Join_the_given_channel": "Unirse al canal dado", "Join_video_call": "Unirse a la video llamada", + "Join_my_room_to_start_the_video_call": "Únete a mi sala para iniciar la videollamada", "join-without-join-code": "Únete sin el código de unión", "join-without-join-code_description": "Permiso para eludir el código de unión en canales con código de unión activado", "Joined": "Unido", @@ -2371,7 +2387,7 @@ "Keyboard_Shortcuts_Mark_all_as_read": "Marcar todos los mensajes (en todos los canales) como leídos", "Keyboard_Shortcuts_Move_To_Beginning_Of_Message": "Mover al comienzo del mensaje", "Keyboard_Shortcuts_Move_To_End_Of_Message": "Mover al final del mensaje", - "Keyboard_Shortcuts_New_Line_In_Message": "Nueva línea en el mensaje de redacción de mensaje", + "Keyboard_Shortcuts_New_Line_In_Message": "Nueva línea en la entrada de redacción del mensaje", "Keyboard_Shortcuts_Open_Channel_Slash_User_Search": "Abrir canal/búsqueda de usuario", "Keyboard_Shortcuts_Title": "Atajos de teclado", "Knowledge_Base": "Base de conocimiento", @@ -2457,7 +2473,7 @@ "LDAP_Authentication_UserDN": "DN de usuario", "LDAP_Authentication_UserDN_Description": "El usuario LDAP que realiza búsquedas de usuario para autenticar a otros usuarios cuando inician sesión en.
      Esta es normalmente una cuenta de servicio creado específicamente para integraciones de terceros. Utilizar un nombre completo, como `cn = Administrador, cn = Users, dc = ejemplo, dc = com`.", "LDAP_Avatar_Field": "Campo de avatar de usuario", - "LDAP_Avatar_Field_Description": "Qué campo se utilizará como *avatar* para los usuarios. Déjelo en blanco para usar `thumbnailPhoto` primero y` jpegPhoto` como respaldo.", + "LDAP_Avatar_Field_Description": "Qué campo se utilizará como * avatar * para los usuarios. Déjelo en blanco para usar `thumbnailPhoto` primero y` jpegPhoto` como respaldo.", "LDAP_Background_Sync": "Sincronización de fondo", "LDAP_Background_Sync_Avatars": "Sincronización de fondo de avatar", "LDAP_Background_Sync_Avatars_Description": "Habilite un proceso en segundo plano separado para sincronizar los avatares de los usuarios.", @@ -2469,12 +2485,12 @@ "LDAP_Background_Sync_Keep_Existant_Users_Updated": "Actualización de sincronización de fondo de usuarios existentes", "LDAP_Background_Sync_Keep_Existant_Users_Updated_Description": "Sincronizará el avatar, los campos, el nombre de usuario, etc. (según su configuración) de todos los usuarios ya importados de LDAP en cada ** Intervalo de sincronización **", "LDAP_BaseDN": "Base DN", - "LDAP_BaseDN_Description": "El nombre distinguido (DN) completamente calificado de un subárbol LDAP que desea buscar usuarios y grupos. Puede agregar tantos como desee; sin embargo, cada grupo debe estar definido en la misma base de dominio que los usuarios que le pertenecen. Ejemplo: `ou = Usuarios + ou = Proyectos, dc = Ejemplo, dc = com`. Si especifica grupos de usuarios restringidos, solo los usuarios que pertenecen a esos grupos estarán dentro del alcance. Le recomendamos que especifique el nivel superior de su árbol de directorios LDAP como base de su dominio y utilice el filtro de búsqueda para controlar el acceso.", - "LDAP_CA_Cert": "CA Cert", + "LDAP_BaseDN_Description": "El nombre distinguido (DN) completo de un subárbol LDAP en el que desea buscar usuarios y grupos. Puede agregar tantos como desee; sin embargo, cada grupo debe estar definido en la misma base de dominio que los usuarios que pertenecen a él. Ejemplo: `ou = Usuarios + ou = Proyectos, dc = Ejemplo, dc = com`. Si especifica grupos de usuarios restringidos, solo los usuarios que pertenecen a esos grupos estarán dentro del alcance. Le recomendamos que especifique el nivel superior de su árbol de directorios LDAP como base de su dominio y utilice el filtro de búsqueda para controlar el acceso.", + "LDAP_CA_Cert": "Certificado de CA", "LDAP_Connect_Timeout": "Tiempo de espera de conexión(ms)", "LDAP_DataSync_AutoLogout": "Usuarios desactivados de cierre de sesión automático", "LDAP_Default_Domain": "Dominio Predeterminado", - "LDAP_Default_Domain_Description": "Si se proporciona, el Dominio predeterminado se usará para crear un correo electrónico exclusivo para los usuarios donde el correo electrónico no se haya importado de LDAP. El correo electrónico se montará como `username @ default_domain` o` unique_id @ default_domain`.
      Ejemplo: `rocket.chat`", + "LDAP_Default_Domain_Description": "si se proporciona, el dominio predeterminado se utilizará para crear un correo electrónico único para los usuarios en los que el correo electrónico no se importó desde LDAP. El correo electrónico se montará como `username @ default_domain` o` unique_id @ default_domain`.
      Ejemplo: `rocket.chat`", "LDAP_Enable": "Habilitar", "LDAP_Enable_Description": "Intentar utilizar LDAP como método de autenticación ", "LDAP_Enable_LDAP_Groups_To_RC_Teams": "Habilite el mapeo del equipo de LDAP a Rocket.Chat", @@ -2482,7 +2498,7 @@ "LDAP_Encryption_Description": "Metodo de cifrado usado para la comunicación segura hacia el servidor LDAP. Algunos ejemplos 'sin cifrado', 'SSL/LDAPS (cifrado desde el inicio), y 'StartTLS' ( actualizar a comunicaciónes cifradas una vez conectado).", "LDAP_Find_User_After_Login": "Encontrar usuario después de iniciar sesión", "LDAP_Find_User_After_Login_Description": "Realizará una búsqueda del DN del usuario después de la vinculación para garantizar que la vinculación se realizó correctamente y evitará el inicio de sesión con contraseñas vacías cuando lo permita la configuración de AD.", - "LDAP_Group_Filter_Enable": "Habilitar filtro de grupo de usuarios LDAP", + "LDAP_Group_Filter_Enable": "Habilitar el filtro de grupo de usuarios LDAP", "LDAP_Group_Filter_Enable_Description": "Restringir el acceso a los usuarios en un grupo LDAP
      Útil para permitir que los servidores OpenLDAP sin un filtro * memberOf * restrinjan el acceso por grupos", "LDAP_Group_Filter_Group_Id_Attribute": "Atributo de ID de grupo", "LDAP_Group_Filter_Group_Id_Attribute_Description": "Por ejemplo. *OpenLDAP:* cn", @@ -2501,10 +2517,10 @@ "LDAP_Idle_Timeout_Description": "Cuántos milisegundos esperan después de la última operación LDAP hasta que se cierra la conexión. (Cada operación abrirá una nueva conexión)", "LDAP_Import_Users_Description": "Su verdadero proceso de sincronización importará a todos los usuarios de LDAP
      *¡Atención!* Especifique el filtro de búsqueda para no importar el exceso de usuarios.", "LDAP_Internal_Log_Level": "Nivel de registro interno", - "LDAP_Login_Fallback": "Login Fallback", + "LDAP_Login_Fallback": "Respaldo de inicio de sesión", "LDAP_Login_Fallback_Description": "Si el inicio de sesión en LDAP no es exitoso, intente iniciar sesión en el sistema predeterminado/cuenta local. Ayuda cuando el LDAP está inactivo por alguna razón.", "LDAP_Merge_Existing_Users": "Fusionar usuarios existentes", - "LDAP_Merge_Existing_Users_Description": "* ¡Precaución! * Al importar un usuario de LDAP y ya existe un usuario con el mismo nombre de usuario, la información y la contraseña de LDAP se establecerán en el usuario existente.", + "LDAP_Merge_Existing_Users_Description": "* ¡Precaución! * Cuando se importa un usuario de LDAP y ya existe un usuario con el mismo nombre de usuario, la información y la contraseña de LDAP se establecerán en el usuario existente.", "LDAP_Port": "Puerto", "LDAP_Port_Description": "Puerto para acceder a LDAP. Por ejemplo. `389` o `636` para LDAPS", "LDAP_Prevent_Username_Changes": "Impedir que los usuarios de LDAP cambien su nombre de usuario de Rocket.Chat", @@ -2516,13 +2532,13 @@ "LDAP_Search_Page_Size": "Tamaño de página de búsqueda", "LDAP_Search_Page_Size_Description": "El número máximo de entradas que cada página de resultados volverá a procesarse", "LDAP_Search_Size_Limit": "Límite de tamaño de búsqueda", - "LDAP_Search_Size_Limit_Description": "El número máximo de entradas para devolver.
      **Atención** Este número debe ser mayor que **Tamaño de página de búsqueda**", + "LDAP_Search_Size_Limit_Description": "El número máximo de entradas para devolver.
      ** Atención ** Este número debe ser mayor que ** Tamaño de la página de búsqueda **", "LDAP_Sync_Custom_Fields": "Sincronizar campos personalizados", "LDAP_CustomFieldMap": "Asignación de campos personalizados", "LDAP_Sync_AutoLogout_Enabled": "Habilitar cierre de sesión automático", "LDAP_Sync_AutoLogout_Interval": "Intervalo de cierre de sesión automático", "LDAP_Sync_Now": "Sincronizar ahora", - "LDAP_Sync_Now_Description": "Esto iniciará una operación ** Sincronización en segundo plano ** ahora, sin esperar a la próxima sincronización programada. \nEsta acción es asincrónica; consulte los registros para obtener más información.", + "LDAP_Sync_Now_Description": "Esto iniciará una operación ** Sincronización en segundo plano ** ahora, sin esperar a la próxima sincronización programada.\nEsta acción es asincrónica; consulte los registros para obtener más información.", "LDAP_Sync_User_Active_State": "Sincronizar el estado de actividad del usuario", "LDAP_Sync_User_Active_State_Both": "Habilitar y deshabilitar usuarios", "LDAP_Sync_User_Active_State_Description": "Determine si los usuarios deben estar habilitados o deshabilitados en Rocket.Chat según el estado de LDAP. El atributo 'pwdAccountLockedTime' se utilizará para determinar si el usuario está deshabilitado.", @@ -2550,15 +2566,19 @@ "LDAP_Sync_User_Data_Roles_Filter_Description": "El filtro de búsqueda LDAP que se usa para verificar si un usuario está en un grupo.", "LDAP_Sync_User_Data_RolesMap": "Mapa de grupo de datos de usuario", "LDAP_Sync_User_Data_RolesMap_Description": "Mapea los grupos LDAP a los roles de usuario de Rocket.Chat
      Como ejemplo, `{\"rocket-admin\":\"admin\", \"tech-support\":\"support\"}` mapeará el grupo LDAP de rocket-admin al rol de \"admin\" de Rocket.", + "LDAP_Teams_BaseDN": "Equipos LDAP BaseDN", + "LDAP_Teams_BaseDN_Description": "LDAP BaseDN utilizado para buscar equipos de usuarios.", + "LDAP_Teams_Name_Field": "Atributo de nombre de equipo LDAP", + "LDAP_Teams_Name_Field_Description": "El atributo LDAP que Rocket.Chat debe usar para cargar el nombre del equipo. Puede especificar más de un posible nombre de atributo si los separa con una coma.", "LDAP_Timeout": "Tiempo de espera (ms)", "LDAP_Timeout_Description": "Cuántos milisegundos esperan un resultado de búsqueda antes de devolver un error", "LDAP_Unique_Identifier_Field": "Campo Identificador Único ", - "LDAP_Unique_Identifier_Field_Description": "Qué campo se utilizará para vincular al usuario LDAP y el usuario Rocket.Chat. Puede informar a varios valores separados por una coma para tratar de obtener el valor del registro de LDAP.
      El valor por defecto es `objectGUID, IBM-entryUUID, GUID, dominoUNID, nsuniqueid, uidNumber`", + "LDAP_Unique_Identifier_Field_Description": "Qué campo se utilizará para vincular el usuario LDAP y el usuario de Rocket.Chat. Puede informar varios valores separados por comas para intentar obtener el valor del registro LDAP.
      El valor predeterminado es `objectGUID, ibm-entryUUID, GUID, dominoUNID, nsuniqueId, uidNumber`", "LDAP_User_Found": "Usuario LDAP encontrado", "LDAP_User_Search_AttributesToQuery": "Atributos para consulta", "LDAP_User_Search_AttributesToQuery_Description": "Especifique qué atributos deben devolverse en las consultas LDAP, separándolos con comas. Valores predeterminados para todo. `*` representa todos los atributos regulares y `+` representa todos los atributos operativos. Asegúrese de incluir todos los atributos que utilizan todas las opciones de sincronización de Rocket.Chat.", "LDAP_User_Search_Field": "Campo de búsqueda", - "LDAP_User_Search_Field_Description": "El atributo LDAP que identifica al usuario LDAP que intenta la autenticación. Este campo debe ser \"sAMAccountName\" para la mayoría de las instalaciones de Active Directory, pero puede ser \"uid\" para otras soluciones LDAP, como OpenLDAP. Puede usar `mail` para identificar a los usuarios por correo electrónico o cualquier atributo que desee.
      Puede usar varios valores separados por comas para permitir que los usuarios inicien sesión usando múltiples identificadores como nombre de usuario o correo electrónico.", + "LDAP_User_Search_Field_Description": "El atributo LDAP que identifica al usuario LDAP que intenta la autenticación. Este campo debe ser \"sAMAccountName\" para la mayoría de las instalaciones de Active Directory, pero puede ser \"uid\" para otras soluciones LDAP, como OpenLDAP. Puede usar `mail` para identificar a los usuarios por correo electrónico o cualquier atributo que desee.
      Puede usar varios valores separados por comas para permitir que los usuarios inicien sesión usando múltiples identificadores como nombre de usuario o correo electrónico.", "LDAP_User_Search_Filter": "Filtro", "LDAP_User_Search_Filter_Description": "Si se les permitirá especificados, sólo los usuarios que coincidan con este filtro para iniciar sesión. Si no se especifica ningún filtro, todos los usuarios dentro del alcance de la base de dominio especificado serán capaces de iniciar sesión.
      Por ejemplo, para Active Directory `memberOf = cn = ROCKET_CHAT, ou = Groups` general.
      Por ejemplo, para OpenLDAP (búsqueda de coincidencia de extensible) `ou: dn: = ROCKET_CHAT`.", "LDAP_User_Search_Scope": "Alcance", @@ -2571,10 +2591,10 @@ "Lead_capture_phone_regex": "Regex de teléfono de captura clientes potenciales", "Leave": "Salir", "Leave_a_comment": "Dejar un comentario", - "Leave_Group_Warning": "¿Seguro que quieres dejar el grupo \"%s\"?", + "Leave_Group_Warning": "¿Estás seguro de que quieres dejar el grupo \"% s\"?", "Leave_Livechat_Warning": "¿Seguro que quieres salir de la sala Omnichannel con \"%s\"?", "Leave_Private_Warning": "¿Seguro que quieres salir de la discusión con \"%s\"?", - "Leave_room": "Salir de la sala", + "Leave_room": "Salir ", "Leave_Room_Warning": "¿Seguro que quieres salir de la sala \"%s\"?", "Leave_the_current_channel": "Salir del canal actual", "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "Deje el campo de descripción en blanco si no desea mostrar el rol", @@ -2616,7 +2636,7 @@ "Livechat_Facebook_API_Key": "Clave API de Facebook", "Livechat_Facebook_API_Secret": "API Secreto Facebook", "Livechat_Facebook_Enabled": "Integración de Facebook habilitada", - "Livechat_forward_open_chats": "Reenviar charlas abiertas", + "Livechat_forward_open_chats": "Reenviar chats abiertos", "Livechat_forward_open_chats_timeout": "Tiempo de espera (en segundos) para reenviar los chats", "Livechat_guest_count": "Contador de invitados", "Livechat_Inquiry_Already_Taken": "Solicitud de Omnichannel ya atendida", @@ -2627,10 +2647,11 @@ "Livechat_Managers": "Administradores", "Livechat_max_queue_wait_time_action": "Cómo manejar los chats en cola cuando se alcanza el tiempo máximo de espera", "Livechat_maximum_queue_wait_time": "Tiempo máximo de espera en cola", + "Livechat_maximum_queue_wait_time_description": "Tiempo máximo (en minutos) para mantener los chats en cola. -1 significa ilimitado", "Livechat_message_character_limit": "Límite de caracteres del mensaje de LiveChat", "Livechat_monitors": "Monitores de Omnichannel", "Livechat_Monitors": "Monitores", - "Livechat_offline": "LiveChat desconectado", + "Livechat_offline": "Omnichannel desconectado", "Livechat_offline_message_sent": "Mensaje de Livechat enviado sin conexión", "Livechat_OfflineMessageToChannel_enabled": "Enviar mensajes sin conexión de LiveChat a un canal", "Omnichannel_on_hold_chat_resumed": "Reanudación del chat en espera: __comment__", @@ -2639,12 +2660,12 @@ "Omnichannel_On_Hold_due_to_inactivity": "El chat se puso automáticamente en espera porque no hemos recibido ninguna respuesta de __guest__ en __timeout__ segundos", "Omnichannel_On_Hold_manually": "El chat fue puesto manualmente en espera por __user__", "Omnichannel_onHold_Chat": "Poner chat en espera", - "Livechat_online": "LiveChat en línea", + "Livechat_online": "Omnichannel en línea", "Omnichannel_placed_chat_on_hold": "Chat en espera: __comment__", "Livechat_Queue": "Cola de Omnichannel", "Livechat_registration_form": "Formulario de Registro", "Livechat_registration_form_message": "Mensaje del formulario de registro", - "Livechat_room_count": "Recuento de salas Omnichannel", + "Livechat_room_count": "Recuento de Room de Omnichannel ", "Livechat_Routing_Method": "Método de enrutamiento del Omnichannel", "Livechat_status": "Estado de LiveChat", "Livechat_Take_Confirm": "¿Quiere aceptar este cliente?", @@ -2653,7 +2674,7 @@ "Livechat_transcript_already_requested_warning": "La transcripción de este chat ya ha sido solicitada y será enviada tan pronto como la conversación termine.", "Livechat_transcript_has_been_requested": "Se ha solicitado la transcripción del chat.", "Livechat_transcript_request_has_been_canceled": "Se canceló la solicitud de transcripción del chat.", - "Livechat_transcript_sent": "Se ha enviado una transcripción de LiveChat", + "Livechat_transcript_sent": "Se ha enviado una transcripción de Omnichannel", "Livechat_transfer_return_to_the_queue": "__from__ devolvió el chat a la cola", "Livechat_transfer_to_agent": "__from__ transfirió el chat a __to__", "Livechat_transfer_to_agent_with_a_comment": "__from__ transfirió el chat a __to__ con un comentario: __comment__", @@ -2661,7 +2682,8 @@ "Livechat_transfer_to_department_with_a_comment": "__from__ transfirió el chat a el departamento __to__ con un comentario: __comment__", "Livechat_Triggers": "Activadores LiveChat", "Livechat_user_sent_chat_transcript_to_visitor": "__agent__ envió la transcripción del chat a __guest__", - "Livechat_Users": "Usuarios de LiveChat", + "Livechat_Users": "Usuarios de Omnichannel", + "Livechat_Calls": "Llamadas Livechat", "Livechat_visitor_email_and_transcript_email_do_not_match": "El correo electrónico del visitante y el de la transcripción no coinciden", "Livechat_visitor_transcript_request": "__guest__ pidió la transcripción del chat", "LiveStream & Broadcasting": "Transmisión en directo y transmisión", @@ -2688,7 +2710,7 @@ "Local_Time": "Hora local", "Local_Timezone": "Zona horaria local", "Local_Time_time": "Hora local: __time__", - "Localization": "Idioma", + "Localization": "Localización", "Location": "Ubicación", "Log_Exceptions_to_Channel": "Registrar excepciones al canal", "Log_Exceptions_to_Channel_Description": "Un canal que recibirá todas las excepciones capturadas. Déjalo vacío para ignorar las excepciones.", @@ -2720,17 +2742,17 @@ "Longest_reaction_time": "Tiempo de reacción más largo", "Longest_response_time": "Tiempo de respuesta más largo", "Looked_for": "Buscado", - "Mail_Message_Invalid_emails": "Ha proporcionado uno o mas correos electronicos invalidos %s", + "Mail_Message_Invalid_emails": "Ha proporcionado uno o más correos electrónicos inválidos %s", "Mail_Message_Missing_subject": "Debes proporcionar un asunto del correo electrónico.", "Mail_Message_Missing_to": "Debe seleccionar uno o más usuarios o proporcionar una o más direcciones de correo electrónico, separadas por comas.", "Mail_Message_No_messages_selected_select_all": "No ha seleccionado ningún mensaje", "Mail_Messages": "Mensajes de correo", "Mail_Messages_Instructions": "Seleccione los mensajes que desea enviar por correo electrónico haciendo clic en los mensajes", - "Mail_Messages_Subject": "Aquí hay una parte seleccionada de %s mensajes", + "Mail_Messages_Subject": "Aquí hay una parte seleccionada de% s mensajes", "mail-messages": "Mensajes de correo", "mail-messages_description": "Permiso para usar la opción de mensajes de correo", "Mailer": "Remitente", - "Mailer_body_tags": "Debe utilizar [unsubscribe] para el enlace de anulación de la suscripción.
      Es posible utilizar [name], [fname], [lname] para el nombre completo del usuario, nombre o apellido, respectivamente.
      Es posible utilizar [email] para el correo electrónico del usuario.", + "Mailer_body_tags": "Usted debe usar [unsubscribe] para el enlace de cancelación de suscripción.
      Puede usar [name], [fname], [lname] para el nombre completo, nombre o apellido del usuario, respectivamente.
      Puede usar [email] para el correo electrónico del usuario.", "Mailing": "Envío", "Make_Admin": "Hacer Administrador", "Make_sure_you_have_a_copy_of_your_codes_1": "Asegúrese de tener una copia de sus códigos:", @@ -2751,9 +2773,9 @@ "manage-integrations_description": "Permiso para administrar las integraciones del servidor", "manage-livechat-agents": "Administrar agentes de Omnichannel", "manage-livechat-agents_description": "Permiso para gestionar agentes Omnichannel", - "manage-livechat-departments": "Administrar departamentos de Omnichannel", + "manage-livechat-departments": "Administrar departamentos de Omnichannel", "manage-livechat-departments_description": "Permiso para gestionar departamentos Omnichannel", - "manage-livechat-managers": "Administrar administradores de Omnichannel", + "manage-livechat-managers": "Administrar administradores de Omnichannel", "manage-livechat-managers_description": "Permiso para gestionar gestores Omnichannel", "manage-oauth-apps": "Administrar aplicaciones Oauth", "manage-oauth-apps_description": "Permiso para administrar las aplicaciones Oauth del servidor", @@ -2776,7 +2798,7 @@ "Manager_removed": "Administrador eliminado", "Managers": "Administradores", "Managing_assets": "La gestión de administradores", - "Managing_integrations": "La gestión de integraciones", + "Managing_integrations": "Gestión de integraciones", "Manual_Selection": "Selección manual", "Manufacturing": "Fabricación", "MapView_Enabled": "Habilitar Mapview", @@ -2795,9 +2817,10 @@ "Markdown_Marked_SmartLists": "Habilitar listas inteligentes marcadas", "Markdown_Marked_Smartypants": "Habilitar Smartypants marcados", "Markdown_Marked_Tables": "Habilitar tablas marcadas", - "Markdown_Parser": "Markdown Parser", - "Markdown_SupportSchemesForLink": "Planes de apoyo de rebajas de Enlace", + "Markdown_Parser": "Analizador de Markdown", + "Markdown_SupportSchemesForLink": "Esquemas de soporte de Markdown para enlace", "Markdown_SupportSchemesForLink_Description": "Lista separada por comas de los esquemas permitidos", + "Marketplace": "Mercado", "Marketplace_view_marketplace": "Ver Marketplace", "MAU_value": "MAU __value__", "Max_length_is": "La longitud máxima es %s", @@ -2828,7 +2851,7 @@ "Message_AllowDeleting": "Permitir la eliminación de mensajes", "Message_AllowDeleting_BlockDeleteInMinutes": "Bloquear la Eliminación de Mensajes Despues de (n) Minutos", "Message_AllowDeleting_BlockDeleteInMinutes_Description": "Introduzca 0 para desactivar el bloqueo.", - "Message_AllowDirectMessagesToYourself": "Permitir mensajes directos del usuario a usted mismo", + "Message_AllowDirectMessagesToYourself": "Permitir que los usuarios se envíen mensajes directos a usted mismo", "Message_AllowEditing": "Permitir la edición de mensajes", "Message_AllowEditing_BlockEditInMinutes": "Bloquear la Edicion de Mensajes Despues de (n) Minutos", "Message_AllowEditing_BlockEditInMinutesDescription": "Ingrese 0 para deshabilitar el bloqueo.", @@ -2841,7 +2864,7 @@ "Message_AlwaysSearchRegExp": "Siempre buscar utilizando RegExp", "Message_AlwaysSearchRegExp_Description": "Recomendamos establecer `TRUE si el idioma no es compatible con la búsqueda de texto MongoDB .", "Message_Attachments": "Adjuntos de mensajes", - "Message_Attachments_GroupAttach": "Botones de archivos adjuntos", + "Message_Attachments_GroupAttach": "Grupo de botones de archivos adjuntos", "Message_Attachments_GroupAttachDescription": "Esto agrupa los iconos debajo de un menú expandible. Toma menos espacio de pantalla.", "Message_Attachments_Thumbnails_Enabled": "Habilite las miniaturas de imágenes para ahorrar ancho de banda", "Message_Attachments_Thumbnails_Width": "Ancho máximo de la miniatura (en píxeles)", @@ -2897,7 +2920,7 @@ "Message_HideType_room_unarchived": "Ocultar mensajes de \"Sala no archivada\"", "Message_HideType_ru": "Ocultar mensajes de \"Usuario borrado\"", "Message_HideType_subscription_role_added": "Ocultar mensajes de \"Rol establecido\"", - "Message_HideType_subscription_role_removed": "Ocultar mensajes de \"Rol ya no definido\"", + "Message_HideType_subscription_role_removed": "Ocultar mensajes de \"Rol no definido\"", "Message_HideType_uj": "Ocultar mensajes de \"Usuario unido\"", "Message_HideType_ul": "Ocultar mensajes de \"Salida de usuario\"", "Message_HideType_ut": "Ocultar mensajes de \"El usuario se unió a la conversación\"", @@ -2930,7 +2953,7 @@ "Message_too_long": "Mensaje demasiado largo", "Message_UserId": "ID del usuario", "Message_VideoRecorderEnabled": "Activar grabador de video", - "Message_VideoRecorderEnabledDescription": "Requer que los archivos de tipo 'video/webm' sean un tipo de medio aceptado en la configuracion 'Subida ficheros'.", + "Message_VideoRecorderEnabledDescription": "Requiere que los archivos 'video / webm' sean un tipo de medio aceptado dentro de la configuración de 'Carga de archivos'.", "Message_view_mode_info": "Esto cambia la cantidad de mensajes espacio ocupan en la pantalla.", "MessageBox_view_mode": "Modo de visualización del panel de mensajes", "messages": "mensajes", @@ -2949,7 +2972,7 @@ "meteor_status_connecting": "Conectando...", "meteor_status_failed": "La conexión con el servidor falló", "meteor_status_offline": "Modo fuera de línea.", - "meteor_status_reconnect_in": "reintentando automáticamente en un segundo...", + "meteor_status_reconnect_in": "intentando de nuevo en un segundo ...", "meteor_status_reconnect_in_plural": "reintentando automáticamente en __count__ segundos...", "meteor_status_try_now_offline": "Conectar de nuevo", "meteor_status_try_now_waiting": "Intentar ahora", @@ -2961,16 +2984,18 @@ "minute": "minuto", "minutes": "minutos", "Mobex_sms_gateway_address": "Dirección Mobex SMS Gateway", - "Mobex_sms_gateway_address_desc": "IP o Host de su servicio Mobex con un puerto específico. Por ejemplo, `http://192.168.1.1:1401` o `https://www.example.com:1401`", - "Mobex_sms_gateway_from_number": "Desde", - "Mobex_sms_gateway_from_number_desc": "Direccióm/número de teléfono de origen al enviar un nuevo SMS al cliente de Omnichannel ", + "Mobex_sms_gateway_address_desc": "IP o Host de su servicio Mobex con puerto especificado. P.ej. `http: //192.168.1.1: 1401` o` https: //www.example.com: 1401`", + "Mobex_sms_gateway_from_number": "De", + "Mobex_sms_gateway_from_number_desc": "Dirección /número de teléfono de origen al enviar un nuevo SMS al cliente de Omnichannel", "Mobex_sms_gateway_from_numbers_list": "Lista de números desde los que enviar SMS", "Mobex_sms_gateway_from_numbers_list_desc": "Lista de números separados por comas para usar en el envío de mensajes nuevos, por ejemplo 123456789, 123456788, 123456888", "Mobex_sms_gateway_password": "Contraseña", - "Mobex_sms_gateway_restful_address": "Dirección Mobex SMS REST API", + "Mobex_sms_gateway_restful_address": "Dirección de la API REST de SMS de Mobex", "Mobex_sms_gateway_restful_address_desc": "IP o Host de su Mobex REST API. Por ejemplo, `http://192.168.1.1:8080` o `https://www.example.com:8080`", "Mobex_sms_gateway_username": "Nombre de usuario", "Mobile": "Móvil", + "mobile-download-file": "Permitir la descarga de archivos en dispositivos móviles", + "mobile-upload-file": "Permitir la carga de archivos en dispositivos móviles", "Mobile_Push_Notifications_Default_Alert": "Alerta predeterminada de notificaciones móviles", "Monday": "Lunes", "Mongo_storageEngine": "Motor de almacenamiento Mongo", @@ -2989,17 +3014,19 @@ "More_groups": "Más grupos privados", "More_unreads": "Más no leídos", "Most_popular_channels_top_5": "Canales más populares (Top 5)", - "Move_beginning_message": "`%s` - Mover al comienzo del mensaje", + "Move_beginning_message": "`%s` - Ir al principio del mensaje", "Move_end_message": "`%s` - Mover al final del mensaje", "Move_queue": "Mover a la cola", "Msgs": "Mensajes", "multi": "multi", "multi_line": "línea múltiple", + "Mute": "Silenciar", "Mute_all_notifications": "Silenciar todas las notificaciones", "Mute_Focused_Conversations": "Silenciar conversaciones enfocadas", "Mute_Group_Mentions": "Silenciar @all i @here menciones", "Mute_someone_in_room": "Silenciar a alguien en la sala", "Mute_user": "Silenciar usuario", + "Mute_microphone": "Silenciar micrófono", "mute-user": "Usuario silenciado", "mute-user_description": "Permiso para silenciar a otros usuarios en el mismo canal", "Muted": "Silenciado", @@ -3041,7 +3068,7 @@ "New_Password_Placeholder": "Por favor ingrese nueva contraseña ...", "New_Priority": "Nueva prioridad", "New_role": "Nuevo rol", - "New_Room_Notification": "Notificación de Nueva Sala", + "New_Room_Notification": "Notificación de Nueva Room", "New_Tag": "Nueva etiqueta", "New_Trigger": "Nuevo disparador", "New_Unit": "Nueva unidad", @@ -3110,9 +3137,9 @@ "Notification_Desktop_Default_For": "Mostrar notificaciones de escritorio para", "Notification_Push_Default_For": "Notificaciones móviles push para", "Notification_RequireInteraction": "Requerir interacción para descartar la notificación de escritorio", - "Notification_RequireInteraction_Description": "Funciona solo con versiones de Google Chrome > 50. Utilice el parámetro Utilizes the requireInteraction para mostrar la notificación de escritorio de forma indefinida hasta que el usuario interactúe con ella.", + "Notification_RequireInteraction_Description": "Funciona solo con las versiones del navegador Chrome> 50. Utiliza el parámetro requireInteraction para mostrar la notificación de escritorio de forma indefinida hasta que el usuario interactúe con ella.", "Notifications": "Notificaciones", - "Notifications_Max_Room_Members": "Número máximo de miembros de la sala antes de deshabilitar todas las notificaciones de mensajes", + "Notifications_Max_Room_Members": "Número máximo de miembros de la Room antes de deshabilitar todas las notificaciones de mensajes", "Notifications_Max_Room_Members_Description": "Número máximo de miembros en la sala cuando se deshabilitan las notificaciones de todos los mensajes. Los usuarios aún pueden cambiar la configuración de cada habitación para recibir todas las notificaciones de forma individual. (0 para deshabilitar)", "Notifications_Muted_Description": "Si elige silenciar todo, no verá el resaltado de la sala en la lista cuando haya mensajes nuevos, a excepción de las menciones. Las notificaciones de silenciamiento anularán las configuraciones de notificaciones.", "Notifications_Preferences": "Preferencias de notificaciones", @@ -3134,7 +3161,7 @@ "Number_of_federated_users": "Número de usuarios federados", "Number_of_messages": "Número de mensajes", "Number_of_most_recent_chats_estimate_wait_time": "Número de chats recientes para calcular el tiempo de espera estimado", - "Number_of_most_recent_chats_estimate_wait_time_description": "Este valor define el número de las últimas salas reservadas que serán utilizadas para calcular los tiempos de espera en cola. ", + "Number_of_most_recent_chats_estimate_wait_time_description": "Este número define el número de las últimas salas atendidas que se utilizarán para calcular los tiempos de espera de la cola.", "Number_of_users_autocomplete_suggestions": "Número de sugerencias de autocompletar de los usuarios.", "OAuth Apps": "Aplicaciones OAuth", "OAuth_Application": "Aplicación de OAuth", @@ -3150,14 +3177,14 @@ "Offline_DM_Email": "Se le ha contactado directamente por __user__", "Offline_Email_Subject_Description": "Puede usar los siguientes marcadores de posición:
      • [Site_Name], [Site_URL], [User] y [Room] para el nombre de la aplicación, la URL, el nombre de usuario y el nombre de la habitación, respectivamente.
      ", "Offline_form": "Formulario fuera de línea", - "Offline_form_unavailable_message": "Mensaje a enviar mientras está fuera de línea", + "Offline_form_unavailable_message": "Mensaje de formulario sin conexión no disponible", "Offline_Link_Message": "IR AL MENSAJE", "Offline_Mention_All_Email": "Mencione todo el asunto del correo electrónico", - "Offline_Mention_Email": "Usted ha sido mencionado por __user__ en #__room__", + "Offline_Mention_Email": "Mencione el asunto del correo electrónico", "Offline_message": "Mensaje fuera de línea", "Offline_Message": "Mensaje fuera de línea", "Offline_Message_Use_DeepLink": "Usar formato de URL de enlace profundo", - "Offline_messages": "Mensajes fuera de línea", + "Offline_messages": "Mensajes sin conexión", "Offline_success_message": "Mensaje fuera de línea correcto", "Offline_unavailable": "No disponible sin conexión", "Ok": "De acuerdo", @@ -3166,7 +3193,9 @@ "Older_than": "Más antiguo que", "Omnichannel": "Omnichannel", "Omnichannel_Directory": "Directorio de Omnichannel", - "Omnichannel_appearance": "Apariencia de LiveChat", + "Omnichannel_appearance": "Apariencia de Omnichannel", + "Omnichannel_calculate_dispatch_service_queue_statistics": "Calcular y enviar estadísticas de colas de espera Omnichannel", + "Omnichannel_calculate_dispatch_service_queue_statistics_Description": "Procesar y enviar estadísticas de la cola de espera, como la posición y el tiempo de espera estimado. Si el * canal de chat en vivo * no está en uso, se recomienda deshabilitar esta configuración y evitar que el servidor realice procesos innecesarios.", "Omnichannel_Contact_Center": "Centro de contactos de Omnichannel", "Omnichannel_contact_manager_routing": "Asignar nuevas conversaciones al administrador de contactos", "Omnichannel_contact_manager_routing_Description": "Esta configuración asigna un chat al Administrador de contactos asignado, siempre que el Administrador de contactos esté en línea cuando se inicia el chat", @@ -3177,6 +3206,7 @@ "Omnichannel_External_Frame_URL": "URL del marco externo", "On": "Activar", "On_Hold_Chats": "En espera", + "On_Hold_conversations": "Conversaciones en espera", "online": "en línea", "Online": "Conectado", "Only_authorized_users_can_write_new_messages": "Sólo los usuarios autorizados pueden escribir nuevos mensajes", @@ -3190,7 +3220,7 @@ "Oops_page_not_found": "Vaya, página no encontrada", "Oops!": "Uy!", "Open": "Abierto", - "Open_channel_user_search": "`%s` - Abrir canal / búsqueda de usuario", + "Open_channel_user_search": "`%s` - Abrir channel / búsqueda de usuario", "Open_conversations": "Conversaciones abiertas", "Open_Days": "Días abiertos", "Open_days_of_the_week": "Días abiertos de la semana", @@ -3212,7 +3242,7 @@ "Organization_Type": "Tipo de Organización", "Original": "Original", "OS_Arch": "Arquitectura de SO", - "OS_Cpus": "Recuento de CPUs", + "OS_Cpus": "Recuento de CPU del SO", "OS_Freemem": "Memoria Libre del SO", "OS_Loadavg": "Promedio de Carga del SO", "OS_Platform": "Plataforma del SO", @@ -3229,7 +3259,7 @@ "Outgoing_WebHook": "WebHook saliente", "Outgoing_WebHook_Description": "Obtenga datos de Rocket.Chat en tiempo real.", "Output_format": "Formato de salida", - "Override_URL_to_which_files_are_uploaded_This_url_also_used_for_downloads_unless_a_CDN_is_given": "URL de alteración a la que se cargan los archivos. Este sitio de Internet también se utiliza para las descargas a menos de un CDN es dado", + "Override_URL_to_which_files_are_uploaded_This_url_also_used_for_downloads_unless_a_CDN_is_given": "Reemplazar la URL a la que se cargan los archivos. Esta URL también se usa para descargas a menos que se proporcione un CDN", "Page_title": "Título de la página", "Page_URL": "URL de la página", "Pages": "Páginas", @@ -3276,7 +3306,7 @@ "PiwikAnalytics_domains": "Ocultar enlaces salientes", "PiwikAnalytics_domains_Description": "En el informe \"Enlaces externos\", oculte los clics a URL de alias conocidas. Inserte un dominio por línea y no use separadores.", "PiwikAnalytics_prependDomain": "Anteponer el dominio", - "PiwikAnalytics_prependDomain_Description": "Anteponga el dominio del sitio al título de la página cuando realiza el seguimiento", + "PiwikAnalytics_prependDomain_Description": "Anteponga el dominio del sitio al título de la página cuando realice el seguimiento", "PiwikAnalytics_siteId_Description": "La Identificación del sitio a utilizar para la identificación de este sitio. Ejemplo: 17", "PiwikAnalytics_url_Description": "La url donde reside el Piwik, asegúrese de incluir la barra probando. Ejemplo: //piwik.rocket.chat/", "Placeholder_for_email_or_username_login_field": "Marcador de posición para el campo de inicio de sesión de correo electrónico o nombre de usuario", @@ -3333,7 +3363,7 @@ "Privacy_Policy": "Política de Privacidad", "Private": "Privado", "Private_Channel": "Canal Privado", - "Private_Channels": "Canales privados", + "Private_Channels": "Channels privados", "Private_Chats": "Chats privados", "Private_Group": "Grupo Privado", "Private_Groups": "Grupos Privados", @@ -3368,7 +3398,7 @@ "Purchase_for_free": "Compra GRATIS", "Purchase_for_price": "Compra por $%s", "Purchased": "Comprado", - "Push": "Push", + "Push": "Pulsar", "Push_Notifications": "Notificaciones Push", "Push_apn_cert": "Certificado de APN", "Push_apn_dev_cert": "Certificado de APN de desarrollador", @@ -3393,6 +3423,7 @@ "Query_description": "Condiciones adicionales para determinar a que usuarios enviar el correo electrónico. Los usuarios que optaron por cancelar su suscripción serán eliminados de la consulta. Debe ser JSON valido. Ejemplo: \"{\"createdAt\":{\"$gt\":{\"$date\": \"2015-01-01T00:00:00.000Z\"}}}\"", "Query_is_not_valid_JSON": "La consulta no es JSON válido", "Queue": "Cola", + "Queue_delay_timeout": "Tiempo de espera de espera de procesamiento de cola", "Queue_Time": "Tiempo de cola", "Queue_management": "Gestión de colas", "quote": "cita", @@ -3407,7 +3438,7 @@ "Reactions": "Reacciones", "Read_by": "Leído por", "Read_only": "Sólo lectura", - "Read_only_changed_successfully": "Sólo lectura cambiado con éxito", + "Read_only_changed_successfully": "Solo lectura cambiado correctamente", "Read_only_channel": "Canal de sólo lectura", "Read_only_group": "Grupo de sólo lectura", "Real_Estate": "Bienes raíces", @@ -3460,8 +3491,8 @@ "Remove_from_team": "Elimina del equipo", "Remove_last_admin": "Eliminando el último administrador", "Remove_someone_from_room": "Eliminar a alguien de la sala", - "remove-closed-livechat-room": "Quitar sala cerrada de Omnichannel", - "remove-closed-livechat-rooms": "Eliminar salas cerradas de Omnichannel", + "remove-closed-livechat-room": "Quitar Room cerrada de Omnichannel", + "remove-closed-livechat-rooms": "Eliminar Rooms cerradas de Omnichannel", "remove-closed-livechat-rooms_description": "Permiso para eliminar salas Omnichannel cerradas", "remove-livechat-department": "Elimina los departamentos Omnichannel", "remove-user": "Eliminar usuario", @@ -3471,7 +3502,7 @@ "Replay": "Repetición", "Replied_on": "Respondió en", "Replies": "Respuestas", - "Reply": "Responder", + "Reply": "Respuesta", "reply_counter": "__counter__ respuesta", "reply_counter_plural": "__counter__ respuestas", "Reply_in_direct_message": "Responder en mensaje directo", @@ -3539,21 +3570,21 @@ "RetentionPolicy_Precision": "Precisión del temporizador", "RetentionPolicy_Precision_Description": "Con qué frecuencia debe funcionar el temporizador de poda. Establecer esto en un valor más preciso hace que los canales con temporizadores de retención rápidos funcionen mejor, pero podría costar potencia de procesamiento adicional en comunidades grandes.", "RetentionPolicy_RoomWarning": "Los mensajes anteriores a __time__ se eliminan automáticamente aquí", - "RetentionPolicy_RoomWarning_FilesOnly": "Los archivos anteriores a __time__ se eliminarán automáticamente aquí (los mensajes permanecen intactos)", + "RetentionPolicy_RoomWarning_FilesOnly": "Los archivos anteriores a __time__ se eliminan automáticamente aquí (los mensajes permanecen intactos)", "RetentionPolicy_RoomWarning_Unpinned": "Los mensajes no fijados anteriores a __time__ se eliminarán automáticamente aquí", - "RetentionPolicy_RoomWarning_UnpinnedFilesOnly": "Los archivos no fijados anteriores a __time__ se eliminarán automáticamente aquí (los mensajes permanecen intactos)", + "RetentionPolicy_RoomWarning_UnpinnedFilesOnly": "Los archivos no fijados anteriores a __time__ se eliminan automáticamente aquí (los mensajes permanecen intactos)", "RetentionPolicyRoom_Enabled": "Borrar mensajes antiguos automáticamente", "RetentionPolicyRoom_ExcludePinned": "Excluir mensajes fijados", "RetentionPolicyRoom_FilesOnly": "Borre solo archivos, mantenga mensajes", "RetentionPolicyRoom_MaxAge": "Antigüedad máxima del mensaje en días (predeterminado: __max__)", "RetentionPolicyRoom_OverrideGlobal": "Anular la política de retención global", - "RetentionPolicyRoom_ReadTheDocs": "¡Cuidado! Ajustar estas configuraciones sin el mayor cuidado puede destruir todo el historial de mensajes. Lea la documentación antes de activar la función aquí .", + "RetentionPolicyRoom_ReadTheDocs": "¡Cuidado! Ajustar estas configuraciones sin el mayor cuidado puede destruir todo el historial de mensajes. Lea la documentación antes de activar la función aquí .", "Retry": "procesar de nuevo", "Retry_Count": "Contador de reintentos", "Return_to_home": "Volver a inicio", "Return_to_previous_page": "Volver a la página anterior", "Return_to_the_queue": "Regresar a la cola", - "Robot_Instructions_File_Content": "Contenido del fichero Robots.txt", + "Robot_Instructions_File_Content": "Contenido del archivo Robots.txt", "Default_Referrer_Policy": "Política de referencia predeterminada", "Default_Referrer_Policy_Description": "Controla la cabecera 'referrer' que se envía al solicitar medios incrustados de otros servidores. Para más información, consulte este enlace de MDN. Recuerde que se requiere una actualización completa de la página para que esto surta efecto", "No_Referrer": "Sin remitente", @@ -3577,12 +3608,12 @@ "Room_archivation_state_true": "Archivado", "Room_archived": "Sala Archivada", "room_changed_announcement": "El anuncio de la sala cambió a: __room_announcement__ por __user_by__", - "room_changed_avatar": "Avatar de la sala cambiado por __user_by__", + "room_changed_avatar": "Avatar de Room cambiado por__user_by__ ", "room_changed_description": "Descripción de la sala cambiada a: __room_description__ por __user_by__.", "room_changed_privacy": "Tipo de sala cambiado a: __room_type__ por __user_by__", "room_changed_topic": "Tema de la sala cambiado a: __room_topic__ por __user_by__", "Room_default_change_to_private_will_be_default_no_more": "Este es un canal predeterminado y cambiarlo a un grupo privado hará que deje de ser un canal predeterminado. ¿Quieres proceder?", - "Room_description_changed_successfully": "Descripción de la sala cambiada con éxito", + "Room_description_changed_successfully": "Room Descripción cambiada con éxito", "room_disallowed_reacting": "Room no permitida reaccionando por __user_by__ ", "Room_Edit": "Editar Room ", "Room_has_been_archived": "La sala ha sido archivada", @@ -3601,10 +3632,10 @@ "room_removed_read_only": "Room agregó permiso de escritura por __user_by__ ", "room_set_read_only": "Room configurada como de solo lectura por __user_by__ ", "Room_tokenpass_config_changed_successfully": "La configuración tokenpass de la sala cambiada con éxito", - "Room_topic_changed_successfully": "Tema de la sala cambiado con éxito", + "Room_topic_changed_successfully": "Room Tema de la sala cambiado con éxito", "Room_type_changed_successfully": "Tipo de sala cambiado con éxito", "Room_type_of_default_rooms_cant_be_changed": "Esta es una sala predeterminada y no se puede cambiar el tipo. Consulte a su administrador.", - "Room_unarchived": "Sala no archivada", + "Room_unarchived": "Room no archivada", "Room_updated_successfully": "¡Sala actualizada correctamente!", "Room_uploaded_file_list": "Lista de Archivos", "Room_uploaded_file_list_empty": "Ningún archivo disponible.", @@ -3678,9 +3709,9 @@ "SAML_Metadata_Template_Description": "Las siguientes variables están disponibles:\n- **\\_\\_sloLocation\\_\\_**: La URL de cierre de sesión simple de Rocket.Chat\n- **\\_\\_issuer\\_\\_**: The value of the __Custom Issuer__ setting.\n- **\\_\\_identifierFormat\\_\\_**: El valor de la opción __Identifier Format__\n- **\\_\\_certificateTag\\_\\_**: Si un certificado privado es configurado, esto incluirá el __Metadata Certificate Template__, de lo contrario será ignorado.\n- **\\_\\_callbackUrl\\_\\_**: La URL de llamada de Rocket.Chat", "SAML_MetadataCertificate_Template": "Plantilla de certificado de metadatos", "SAML_NameIdPolicy_Template": "Plantilla de política de NameID", - "SAML_NameIdPolicy_Template_Description": "Puede utilizar cualquier variable de la plantilla de solicitud de autorización aquí.", + "SAML_NameIdPolicy_Template_Description": "Puede utilizar cualquier variable de la Plantilla de solicitud de autorización aquí.", "SAML_Role_Attribute_Name": "Nombre del atributo de rol", - "SAML_Role_Attribute_Name_Description": "Si este atributo se encuentra en la respuesta SAML, sus valores se usarán como nombres de rol para los nuevos usuarios.", + "SAML_Role_Attribute_Name_Description": "Si este atributo se encuentra en la respuesta SAML, sus valores se usarán como nombres de roles para nuevos usuarios.", "SAML_Role_Attribute_Sync": "Sincronizar los roles de usuario", "SAML_Role_Attribute_Sync_Description": "Sincronice los roles de usuario de SAML al iniciar sesión (sobrescribe los roles de usuario local).", "SAML_Section_1_User_Interface": "Interfaz de usuario", @@ -3715,7 +3746,7 @@ "Search_Apps": "Buscar aplicaciones", "Search_by_file_name": "Buscar por nombre de archivo", "Search_by_username": "Búsqueda por nombre de usuario", - "Search_Channels": "Canales de búsqueda", + "Search_Channels": "búsqueda Channel", "Search_Chat_History": "Buscar en el historial de chat", "Search_current_provider_not_active": "El proveedor de búsqueda actual no está activo", "Search_Integrations": "Integraciones de búsqueda", @@ -3733,7 +3764,7 @@ "Secret_token": "Token secreto", "Security": "Seguridad", "See_full_profile": "Ver perfil completo", - "Select_a_department": "Seleccionar un departamento", + "Select_a_department": "Seleccione un departamento", "Select_a_room": "Seleccione una sala", "Select_a_user": "Seleccione un usuario", "Select_an_avatar": "Selecciona un avatar", @@ -3764,6 +3795,7 @@ "Send_invitation_email_error": "No has proporcionado ninguna dirección de correo electrónico valida. ", "Send_invitation_email_info": "Puede enviar múltiples invitaciones por correo electrónico a la vez", "Send_invitation_email_success": "Has enviado con éxito una invitación por correo electrónico a las siguientes direcciones:", + "Send_it_as_attachment_instead_question": "¿Enviarlo como archivo adjunto?", "Send_me_the_code_again": "Envíame el código de nuevo", "Send_request_on": "Enviar solicitud en", "Send_request_on_agent_message": "Enviar solicitud en mensajes del agente", @@ -3800,7 +3832,6 @@ "Server_Type": "Tipo de servidor", "Service": "Servicio", "Service_account_key": "Clave de cuenta de servicio", - "Sessions": "Sesiones", "Set_as_favorite": "Establecer como favorito", "Set_as_leader": "Establecer como líder", "Set_as_moderator": "Establecer como moderador", @@ -3821,6 +3852,7 @@ "Setup_Wizard": "Asistente de configuración", "Setup_Wizard_Info": "Lo guiaremos para configurar su primer usuario administrador, configurar su organización y registrar su servidor para recibir notificaciones push gratuitas y más.", "Share_Location_Title": "¿Compartir ubicacion?", + "Share_screen": "Compartir pantalla", "New_CannedResponse": "Nueva respuesta preparada", "Edit_CannedResponse": "Editar respuesta predefinida", "Sharing": "Intercambio", @@ -3848,7 +3880,8 @@ "Show_room_counter_on_sidebar": "Mostrar contador de salas en la barra lateral", "Show_Setup_Wizard": "Mostrar el asistente de configuración", "Show_the_keyboard_shortcut_list": "Mostrar la lista de atajos de teclado", - "Showing_archived_results": "

      Mostrando resultados archivados %s

      ", + "Show_video": "Ver video", + "Showing_archived_results": "

      Mostrando %sarchivados resultados

      ", "Showing_online_users": "Mostrando: __total_showing__ En linea: __online__ Total:__total__ ", "Showing_results": "

      Mostrando %s resultados

      ", "Showing_results_of": "Mostrando resultados %s - %s de %s", @@ -3860,12 +3893,12 @@ "Site_Url": "URL del Sitio", "Site_Url_Description": "Ejemplo: https://chat.domain.com", "Size": "Tamaño", - "Skip": "Saltar", + "Skip": "Omitir", "Slack_Users": "CSV de los usuarios de Slack", "SlackBridge_APIToken": "API Tokens", - "SlackBridge_APIToken_Description": "Puede configurar varios servidores slack agregando un token API por línea.", + "SlackBridge_APIToken_Description": "Puede configurar varios servidores slack agregando un token de API por línea.", "Slackbridge_channel_links_removed_successfully": "Los enlaces del canal de Slackbridge se han eliminado correctamente.", - "SlackBridge_error": "SlackBridge obtuvo un error al importar sus mensajes en %s: %s", + "SlackBridge_error": "SlackBridge recibió un error al importar sus mensajes en% s:% s", "SlackBridge_finish": "SlackBridge ha terminado de importar los mensajes en%s. Por favor, vuelva a cargar para ver todos los mensajes.", "SlackBridge_Out_All": "SlackBridge Out All", "SlackBridge_Out_All_Description": "Envía mensajes de todos los canales que existen en Slack y el bot se ha unido", @@ -3887,16 +3920,16 @@ "Smarsh_Email": "Smarsh Email", "Smarsh_Email_Description": "Dirección de correo electrónico Smarsh para enviar el archivo .eml a.", "Smarsh_Enabled": "Smarsh habilitado", - "Smarsh_Enabled_Description": "Si el conector eml de Smarsh está habilitado o no (se debe completar 'De correo electrónico' en Correo electrónico -> SMTP).", + "Smarsh_Enabled_Description": "Si el conector eml de Smarsh está habilitado o no (es necesario completar 'Desde correo electrónico' en Correo electrónico -> SMTP).", "Smarsh_Interval": "Intervalo Smarsh", - "Smarsh_Interval_Description": "La cantidad de tiempo que se debe esperar antes de enviar los chats (se debe completar \"De correo electrónico\" en Correo electrónico -> SMTP).", + "Smarsh_Interval_Description": "La cantidad de tiempo de espera antes de enviar los chats (es necesario completar 'Desde correo electrónico' en Correo electrónico -> SMTP).", "Smarsh_MissingEmail_Email": "Falta el correo electrónico", - "Smarsh_MissingEmail_Email_Description": "El correo electrónico para mostrar una cuenta de usuario cuando falta su dirección de correo electrónico, generalmente ocurre con las cuentas bot.", + "Smarsh_MissingEmail_Email_Description": "El correo electrónico que se muestra para una cuenta de usuario cuando falta su dirección de correo electrónico, generalmente ocurre con las cuentas de bot.", "Smarsh_Timezone": "Zona horaria Smarsh", "Smileys_and_People": "Sonrisas y Personas", "SMS": "SMS", "SMS_Default_Omnichannel_Department": "Departamento de Omnichannel (por defecto)", - "SMS_Default_Omnichannel_Department_Description": "Si se establece, todos los nuevos chats entrantes iniciados por esta integración se enrutarán a este departamento.", + "SMS_Default_Omnichannel_Department_Description": "Si se establece, todos los nuevos chats entrantes iniciados por esta integración se enrutarán a este departamento. \nEsta configuración se puede sobrescribir pasando el parámetro de consulta del departamento en la solicitud. \nEj.https:///api/v1/livechat/sms-entrante/twilio?department=.\nNota: si está usando el nombre del departamento, entonces debería ser URL seguro.", "SMS_Enabled": "SMS Habilitado", "SMTP": "SMTP", "SMTP_Host": "Servidor SMTP", @@ -3932,7 +3965,7 @@ "start-discussion": "Iniciar discusión", "start-discussion_description": "Permiso para iniciar una discusión", "start-discussion-other-user": "Iniciar discusión (Otro usuario)", - "start-discussion-other-user_description": "Permiso para iniciar una discusión, que le da permiso al usuario para crear una discusión a partir de un mensaje enviado también por otro usuario.", + "start-discussion-other-user_description": "Permiso para iniciar una discusión, que le da permiso al usuario para crear una discusión a partir de un mensaje enviado por otro usuario también.", "Started": "Comenzó", "Started_a_video_call": "Comenzó una videollamada", "Started_At": "Comenzó a las", @@ -3978,6 +4011,7 @@ "StatusMessage_Placeholder": "¿Qué estás haciendo en este momento?", "StatusMessage_Too_Long": "El mensaje de estado debe tener menos de 120 caracteres.", "Step": "Paso", + "Stop_call": "Detener llamada", "Stop_Recording": "Detener grabación", "Store_Last_Message": "Almacenar el último mensaje", "Store_Last_Message_Sent_per_Room": "Almacenar el último mensaje enviado en cada sala.", @@ -4012,7 +4046,7 @@ "Taken_at": "Tomado en", "Target user not allowed to receive messages": "El usuario objetivo no puede recibir mensajes", "TargetRoom": "Sala objetiva", - "TargetRoom_Description": "La sala donde se enviarán los mensajes que son el resultado de este evento. Solo se permite una sala objetivo y debe existir.", + "TargetRoom_Description": "La sala a la que se enviarán los mensajes que son el resultado de la activación de este evento. Solo se permite una sala de destino y debe existir.", "Team_Add_existing_channels": "Agregar Channels existentes", "Team_Add_existing": "Agregar existentes", "Team_Auto-join": "Unirse automáticamente", @@ -4083,7 +4117,7 @@ "Texts": "Textos", "Thank_you_exclamation_mark": "¡Gracias!", "Thank_you_for_your_feedback": "Gracias por su comentario", - "The_application_name_is_required": "El nombre de la aplicación es requerido", + "The_application_name_is_required": "El nombre de la aplicación es obligatorio.", "The_channel_name_is_required": "El nombre del canal es requerido", "The_emails_are_being_sent": "Los correos electrónicos están siendo enviados.", "The_empty_room__roomName__will_be_removed_automatically": "La sala vacía __roomName__ se eliminará automáticamente.", @@ -4105,7 +4139,7 @@ "theme-color-attention-color": "Color de atención", "theme-color-component-color": "Color de componente", "theme-color-content-background-color": "Color de fondo del contenido", - "theme-color-custom-scrollbar-color": "Barra de desplazamiento de color personalizado", + "theme-color-custom-scrollbar-color": "Color de barra de desplazamiento personalizado", "theme-color-error-color": "Color de error", "theme-color-info-font-color": "Color del Texto de Información", "theme-color-link-font-color": "Color del Texto de los Enlaces", @@ -4157,7 +4191,7 @@ "theme-color-transparent-lightest": "Luz aún más transparente", "theme-color-unread-notification-color": "Color de Notificaciones No Leídas", "theme-custom-css": "CSS personalizado", - "theme-font-body-font-family": "Familia de fuentes Body", + "theme-font-body-font-family": "Familia de fuentes de cuerpo", "There_are_no_agents_added_to_this_department_yet": "Todavía no hay agentes agregados a este departamento.", "There_are_no_applications": "Aún no se han agregado aplicaciones OAuth.", "There_are_no_applications_installed": "Actualmente no hay aplicaciones Rocket.Chat instaladas.", @@ -4206,7 +4240,7 @@ "toggle-room-e2e-encryption": "Alternar cifrado Room E2E", "toggle-room-e2e-encryption_description": "Permiso para alternar la sala de cifrado e2e", "Token": "Token", - "Token_Access": "Acceso Token", + "Token_Access": "Token de Acceso", "Token_Controlled_Access": "Acceso controlado Token", "Token_required": "Token requerido", "Tokenpass_Channel_Label": "Canal Tokenpass", @@ -4235,7 +4269,7 @@ "Transcript": "Transcripción", "Transcript_Enabled": "Preguntar al visitante si le gustaría una transcripción después de que se cerró el chat", "Transcript_message": "Mensaje para mostrar al preguntar sobre la transcripción", - "Transcript_of_your_livechat_conversation": "Transcripción de su conversación de Omnichannel.", + "Transcript_of_your_livechat_conversation": "Transcripción de su conversación de omnichannel.", "Transcript_Request": "Solicitud de transcripción", "transfer-livechat-guest": "Transferir invitados de Omnichannel", "transfer-livechat-guest_description": "Permiso para transferir invitados al Omnichannel", @@ -4246,7 +4280,7 @@ "Travel_and_Places": "Viajes y Lugares", "Trigger_removed": "Disparador eliminado", "Trigger_Words": "Palabras de activación", - "Triggers": "Disparadores", + "Triggers": "Activadores", "Troubleshoot": "Solución de problemas", "Troubleshoot_Description": "Estas configuraciones están diseñadas para habilitarse solo con la guía de los equipos de soporte o desarrollo de Rocket.Chat. ¡No los toques si no sabes lo que estás haciendo!", "Troubleshoot_Disable_Data_Exporter_Processor": "Desactivar el procesador de exportación de datos", @@ -4258,7 +4292,7 @@ "Troubleshoot_Disable_Notifications": "Deshabilitar notificaciones", "Troubleshoot_Disable_Notifications_Alert": "Esta configuración desactiva completamente el sistema de notificaciones; ¡los sonidos, las notificaciones de escritorio, las notificaciones de móvil y los correos electrónicos se detendrán!", "Troubleshoot_Disable_Presence_Broadcast": "Desactivar la transmisión de la presencia", - "Troubleshoot_Disable_Presence_Broadcast_Alert": "Esta configuración evita que todas las instancias envíen los cambios de estado de los usuarios a sus clientes, manteniendo a todos los usuarios con su estado de presencia desde la primera carga!", + "Troubleshoot_Disable_Presence_Broadcast_Alert": "¡Esta configuración evita que todas las instancias envíen los cambios de estado de los usuarios a sus clientes, manteniendo a todos los usuarios con su estado de presencia desde la primera carga!", "Troubleshoot_Disable_Sessions_Monitor": "Desactivar el Monitor de Sesiones", "Troubleshoot_Disable_Sessions_Monitor_Alert": "¡Esta configuración detiene el procesamiento de las sesiones de visita de Omnichannel causando que las estadísticas dejen de funcionar correctamente!", "Troubleshoot_Disable_Statistics_Generator": "Desactivar el generador de estadísticas", @@ -4269,8 +4303,10 @@ "Tuesday": "Martes", "Turn_OFF": "Apagar", "Turn_ON": "Encender", + "Turn_on_video": "Activar el video", + "Turn_off_video": "Desactivar el video", "Two Factor Authentication": "Autenticación de dos factores", - "Two-factor_authentication": "Autenticación en 2 pasos vía TOTP", + "Two-factor_authentication": "Autenticación de dos factores a través de TOTP", "Two-factor_authentication_disabled": "Autenticación en 2 pasos deshabilitada", "Two-factor_authentication_email": "Autenticación en 2 pasos vía correo electrónico", "Two-factor_authentication_email_is_currently_disabled": "La autenticación en 2 pasos vía correo electrónico está actualmente desactivada", @@ -4293,7 +4329,7 @@ "UI_Click_Direct_Message_Description": "Omita la pestaña de perfil de apertura, en lugar de ir directamente a la conversación", "UI_DisplayRoles": "Mostrar Roles", "UI_Group_Channels_By_Type": "Agrupar canales por tipo", - "UI_Merge_Channels_Groups": "Unir grupos privados con Canales", + "UI_Merge_Channels_Groups": "Unir grupos privados con Channels", "UI_Show_top_navbar_embedded_layout": "Mostrar la barra de navegación superior en el diseño incrustado", "UI_Unread_Counter_Style": "Estilo de contador no leído", "UI_Use_Name_Avatar": "Utilice las iniciales del nombre completo para generar un avatar predeterminado", @@ -4303,7 +4339,7 @@ "unarchive-room": "Habitación desarchivada", "unarchive-room_description": "Permiso para desarchivar canales", "Unavailable": "No disponible", - "Unblock_User": "Desbloquear usuario", + "Unblock_User": "Desbloquear usuario ", "Uncheck_All": "Desmarcar todo", "Uncollapse": "Desplegar", "Undefined": "No definido", @@ -4314,21 +4350,23 @@ "Unit_removed": "Unidad eliminada", "Unknown_Import_State": "Estado de importación desconocido", "Unlimited": "Ilimitado", - "Unmute_someone_in_room": "Des-silenciar a alguien en la sala", + "Unmute": "Activar sonido", + "Unmute_someone_in_room": "Activar el sonido de alguien en la sala", "Unmute_user": "Des-silenciar usuario", "Unnamed": "Sin nombre", "Unpin": "Quitar de fijados", "Unpin_Message": "Desfijar Mensaje", "unpinning-not-allowed": "No se permite quitar de fijados", "Unread": "No leído", - "Unread_Count": "Cuenta no leída", - "Unread_Count_DM": "Cuenta no leída para mensajes directos", + "Unread_Count": "Recuento de no leídos", + "Unread_Count_DM": "Recuento de mensajes no leídos para mensajes directos", "Unread_Messages": "Mensajes no leídos", "Unread_on_top": "No leídos arriba", "Unread_Rooms": "Salas sin leer", "Unread_Rooms_Mode": "Modo Salas sin leer", "Unread_Tray_Icon_Alert": "Ícono de alerta de no leidos", "Unstar_Message": "Eliminar Destacado", + "Unmute_microphone": "Activar sonido del micrófono", "Update": "Actualización", "Update_EnableChecker": "Habilitar el Update Checker", "Update_EnableChecker_Description": "Comprueba automáticamente si hay nuevas actualizaciones / mensajes importantes de los desarrolladores de Rocket.Chat y recibe notificaciones cuando están disponibles. La notificación aparece una vez por nueva versión como un banner en el que se puede hacer clic y como un mensaje del bot Rocket.Cat, ambos visibles solo para los administradores.", @@ -4336,7 +4374,7 @@ "Update_LatestAvailableVersion": "Actualizar a la última versión disponible", "Update_to_version": "Actualizar a la __version__", "Update_your_RocketChat": "Actualiza tu Rocket.Chat", - "Updated_at": "Actualizado", + "Updated_at": "Actualizado en", "Upload": "Subir", "Uploads": "Cargas", "Upload_app": "Subir la aplicación", @@ -4363,7 +4401,7 @@ "Use_Room_configuration": "Sobrescribe la configuración del servidor y utiliza la configuración de la sala", "Use_Server_configuration": "Usar la configuración del servidor", "Use_service_avatar": "Usar %s avatar", - "Use_this_response": "Usar esta respuesta", + "Use_this_response": "Utilizar esta respuesta", "Use_response": "Respuesta de uso", "Use_this_username": "Usar este nombre de usuario", "Use_uploaded_avatar": "Utilizar avatar subido", @@ -4377,7 +4415,7 @@ "User__username__is_now_a_owner_of__room_name_": "El usuario __username__ es ahora un propietario de __room_name__", "User__username__muted_in_room__roomName__": "Usuario __username__ silenciado en la sala __roomName__", "User__username__removed_from__room_name__leaders": "El usuario __username__ fue removido de los líderes de __room_name__", - "User__username__removed_from__room_name__moderators": "El usuario __username__ fue removido de los moderadores de __room_name__ ", + "User__username__removed_from__room_name__moderators": "El usuario __username__ fue retirado de los moderadores de __room_name__", "User__username__removed_from__room_name__owners": "El usuario __username__ fue removido de los propietarios de __room_name__", "User__username__unmuted_in_room__roomName__": "Usuario __username__ sin silenciar en la sala __roomName__", "User_added": "Usuario __user_added__ añadido.", @@ -4410,7 +4448,7 @@ "User_joined_team": "Se ha unido al equipo.", "User_joined_team_female": "Se ha unido al equipo.", "User_joined_team_male": "Se ha unido al equipo.", - "User_left": "__user_left__ ha salido del canal.", + "User_left": "Ha salido del canal.", "User_left_female": "Ha salido del canal.", "User_left_male": "Ha salido del canal.", "User_left_team": "Ha dejado el equipo.", @@ -4439,7 +4477,7 @@ "User_uploaded_a_file_to_you": "__username__ le envió un archivo", "User_uploaded_file": "Subió un archivo", "User_uploaded_image": "Subió una imagen", - "user-generate-access-token": "User Generate Access Token", + "user-generate-access-token": "Token de acceso generado por el usuario", "user-generate-access-token_description": "Permiso para que los usuarios generen tokens de acceso", "UserData_EnableDownload": "Habilitar la descarga de datos de usuario", "UserData_FileSystemPath": "Ruta del sistema (archivos exportados)", @@ -4474,7 +4512,7 @@ "Users": "Usuarios", "Users must use Two Factor Authentication": "Los usuarios deben utilizar la autenticación de dos factores", "Users_added": "Los usuarios se han añadido", - "Users_and_rooms": "Usuarios y Salas", + "Users_and_rooms": "Usuarios y Rooms", "Users_by_time_of_day": "Usuarios por hora del día", "Users_in_role": "Usuarios en el rol", "Users_key_has_been_reset": "Se restableció la clave del usuario", @@ -4489,6 +4527,7 @@ "UTF8_User_Names_Validation_Description": "RegExp que se utilizará para validar nombres de usuario", "UTF8_Channel_Names_Validation": "Validación de nombres de channel UTF8", "UTF8_Channel_Names_Validation_Description": "RegExp que se utilizará para validar los nombres de los canales", + "Videocall_enabled": "Videollamada habilitada", "Validate_email_address": "Validar correo electrónico", "Validation": "Validación", "Value_messages": "__value__ messages", @@ -4510,10 +4549,12 @@ "Video_Conference": "Videoconferencia", "Video_message": "Mensaje de video", "Videocall_declined": "Videollamada rechazada.", - "Videocall_enabled": "Videollamada habilitada", + "Video_and_Audio_Call": "Llamada de audio y video", "Videos": "Vídeos", - "View_All": "Ver Todos los Miembros", + "View_All": "Ver todos los miembros", "View_channels": "Ver Channel s", + "view-omnichannel-contact-center": "Ver centro de contacto Omnichannel", + "view-omnichannel-contact-center_description": "Permiso para ver e interactuar con el centro de contacto Omnichannel", "View_Logs": "Ver Registros", "View_mode": "Modo de vista", "View_original": "Ver original", @@ -4522,7 +4563,7 @@ "view-broadcast-member-list_description": "Permiso para ver la lista de usuarios en el canal de transmisión.", "view-c-room": "Ver canal público", "view-c-room_description": "Permiso para ver canales públicos", - "view-canned-responses": "Ver modelos de respuesta", + "view-canned-responses": "Veure respostes emmagatzemades", "view-d-room": "Ver mensajes directos", "view-d-room_description": "Permiso para ver mensajes directos", "View_full_conversation": "Ver conversación completa", @@ -4534,7 +4575,7 @@ "view-join-code_description": "Permiso para ver el código de unión de canal", "view-joined-room": "Ver sala unida", "view-joined-room_description": "Permiso para ver los canales actualmente unidos", - "view-l-room": "Ver salas de Omnichannel", + "view-l-room": "Ver Rooms de Omnichannel", "view-l-room_description": "Permiso para ver salas de Omnichannel", "view-livechat-analytics": "Ver analíticas de Omnichannel", "view-livechat-analytics_description": "Permiso para ver análisis de Omnichannel", @@ -4563,7 +4604,7 @@ "view-livechat-unit": "Ver las unidades de Omnichannel", "view-logs": "Ver los registros", "view-logs_description": "Permiso para ver los registros del servidor", - "view-other-user-channels": "Ver otros canales de usuario", + "view-other-user-channels": "Ver otro usuario Channels ", "view-other-user-channels_description": "Permiso para ver los canales propiedad de otros usuarios", "view-outside-room": "Vista exterior de Room", "view-outside-room_description": "Permiso para ver usuarios fuera de la sala actual", @@ -4583,10 +4624,11 @@ "Visit_Site_Url_and_try_the_best_open_source_chat_solution_available_today": "Visite __Site_URL__ y pruebe la mejor solución de chat de código abierto disponible hoy.", "Visitor": "Visitante", "Visitor_Email": "Correo electrónico del visitante", - "Visitor_Info": "Información para visitantes", + "Visitor_Info": "Información del visitante", "Visitor_message": "Mensajes de visitantes", "Visitor_Name": "Nombre del visitante", "Visitor_Name_Placeholder": "Por favor, introduzca el nombre del visitante...", + "Visitor_does_not_exist": "El visitante no existe!", "Visitor_Navigation": "Navegación visitante", "Visitor_page_URL": "URL de la página del visitante", "Visitor_time_on_site": "Tiempo del visitante en el sitio", @@ -4599,7 +4641,7 @@ "Warnings": "Advertencias", "WAU_value": "WAU __value__", "We_appreciate_your_feedback": "Agradecemos sus comentarios", - "We_are_offline_Sorry_for_the_inconvenience": "Fuera de línea. Disculpe las molestias.", + "We_are_offline_Sorry_for_the_inconvenience": "Estamos fuera de línea. Disculpen las molestias.", "We_have_sent_password_email": "Te hemos enviado un e-mail con las instrucciones para resetear la contraseña. Si no recibes un correo en breve, por favor regresa y vuelve a intentarlo.", "We_have_sent_registration_email": "Te hemos enviado un e-mail para confirmar tu registro. Si no recibes un correo en breve, por favor regresa y vuelve a intentarlo.", "Webdav Integration": "Integración Webdav", @@ -4614,6 +4656,7 @@ "Webhook_Details": "Detalles WebHook", "Webhook_URL": "URL Webhook", "Webhooks": "WebHooks", + "WebRTC_Call": "Llamada WebRTC", "WebRTC_direct_audio_call_from_%s": "Llamada de audio directa de %s", "WebRTC_direct_video_call_from_%s": "Videollamada directa de %s", "WebRTC_Enable_Channel": "Habilitar para Canales Públicos", @@ -4624,6 +4667,8 @@ "WebRTC_monitor_call_from_%s": "Monitoriza la llamada de %s", "WebRTC_Servers": "Servidores STUN / TURN", "WebRTC_Servers_Description": "Una lista de servidores STUN y TURN separadas por comas.
      Nombre de usuario, contraseña y el puerto están permitidos en el formato `username:password@stun:host:port` o `username:password@turn:host:port`.", + "WebRTC_call_ended_message": "La llamada finalizó a las __endTime__ - Duró __callDuration__", + "WebRTC_call_declined_message": " Llamada rechazada por contacto.", "Website": "Sitio web", "Wednesday": "Miércoles", "Weekly_Active_Users": "Usuarios activos semanales", @@ -4657,13 +4702,13 @@ "Yes_unarchive_it": "Sí, desarchivarlo.", "yesterday": "ayer", "Yesterday": "Ayer", - "You": "Tú", + "You": "Usted", "You_are_converting_team_to_channel": "Estás convirtiendo este equipo en un canal.", "you_are_in_preview_mode_of": "Estás en modo vista previa del canal #__room_name__", "you_are_in_preview_mode_of_incoming_livechat": "Estás en el modo de vista previa de este chat", "You_are_logged_in_as": "Ha iniciado sesión como", "You_are_not_authorized_to_view_this_page": "No está autorizado para ver esta página.", - "You_can_change_a_different_avatar_too": "Puedes anular el avatar usado para publicar desde esta integración.", + "You_can_change_a_different_avatar_too": "Puede anular el avatar utilizado para publicar desde esta integración.", "You_can_close_this_window_now": "Ya puedes cerrar esta ventana.", "You_can_search_using_RegExp_eg": "Puede buscar utilizando una Expresión Regular. Por ejemplo: /^text$/i", "You_can_use_an_emoji_as_avatar": "También puede utilizar un emoji como avatar.", @@ -4672,7 +4717,7 @@ "You_followed_this_message": "Seguiste este mensaje.", "You_have_a_new_message": "Tiene un nuevo mensaje", "You_have_been_muted": "Has sido silenciado y no puedes hablar en esta sala", - "You_have_n_codes_remaining": "Usted tiene códigos __number__ restantes.", + "You_have_n_codes_remaining": "Te quedan __number__ códigos.", "You_have_not_verified_your_email": "Aún no ha verificado su correo electrónico.", "You_have_successfully_unsubscribed": "Se ha dado de baja correctamente de nuestra lista de distribución de correos", "You_have_to_set_an_API_token_first_in_order_to_use_the_integration": "Primero debe establecer un token API para usar la integración.", @@ -4681,27 +4726,27 @@ "You_need_install_an_extension_to_allow_screen_sharing": "Necesita instalar una extensión para permitir compartir su pantalla", "You_need_to_change_your_password": "Es necesario cambiar la contraseña", "You_need_to_type_in_your_password_in_order_to_do_this": "¡Usted tiene que escribir su contraseña con el fin de hacer esto!", - "You_need_to_type_in_your_username_in_order_to_do_this": "¡Es necesario teclear su nombre de usuario con el fin de hacer esto!", + "You_need_to_type_in_your_username_in_order_to_do_this": "¡Necesita escribir su nombre de usuario para hacer esto!", "You_need_to_verifiy_your_email_address_to_get_notications": "Es necesario haber comprobado su dirección de correo electrónico para recibir notificaciones", - "You_need_to_write_something": "¡Usted tiene que escribir algo!", + "You_need_to_write_something": "¡Necesitas escribir algo!", "You_reached_the_maximum_number_of_guest_users_allowed_by_your_license": "Alcanzaste el máximo número de usuarios invitados permitido por tu licencia.", "You_should_inform_one_url_at_least": "Debe definir al menos una URL.", - "You_should_name_it_to_easily_manage_your_integrations": "Nombralo para poder administrar fácilmente sus integraciones", + "You_should_name_it_to_easily_manage_your_integrations": "Debería nombrarlo para administrar fácilmente sus integraciones.", "You_unfollowed_this_message": "Dejaste de seguir este mensaje.", - "You_will_be_asked_for_permissions": "Se le pedirá permiso", + "You_will_be_asked_for_permissions": "Se le pedirán permisos", "You_will_not_be_able_to_recover": "No podrás recuperar este mensaje", "You_will_not_be_able_to_recover_email_inbox": "No podrá recuperar esta bandeja de entrada de correo electrónico", - "You_will_not_be_able_to_recover_file": "No será capaz de recuperar este archivo", - "You_wont_receive_email_notifications_because_you_have_not_verified_your_email": "No recibirá notificaciones por correo electrónico, ya que no ha comprobado su correo electrónico.", + "You_will_not_be_able_to_recover_file": "¡No podrá recuperar este archivo!", + "You_wont_receive_email_notifications_because_you_have_not_verified_your_email": "No recibirá notificaciones por correo electrónico porque no ha verificado su correo electrónico.", "Your_e2e_key_has_been_reset": "Su clave E2E ha sido restablecida.", "Your_email_address_has_changed": "Su dirección de correo electrónico ha sido modificada.", - "Your_email_has_been_queued_for_sending": "Su correo electrónico se ha puesto en cola para envío", + "Your_email_has_been_queued_for_sending": "Su correo electrónico se ha puesto en cola para su envío.", "Your_entry_has_been_deleted": "Tu entrada ha sido eliminada", "Your_file_has_been_deleted": "Su archivo ha sido eliminado.", "Your_invite_link_will_expire_after__usesLeft__uses": "Su enlace de invitación expirará después de __usesLeft__ usos.", "Your_invite_link_will_expire_on__date__": "Su enlace de invitación expirará el día __date__.", "Your_invite_link_will_expire_on__date__or_after__usesLeft__uses": "Su enlace de invitación expirará en __date__ o después de __usesLeft__ usos.", - "Your_invite_link_will_never_expire": "Su enlace de invitación no expirará.", + "Your_invite_link_will_never_expire": "Su enlace de invitación nunca caducará.", "Your_mail_was_sent_to_s": "Su correo electrónico fue enviado a %s", "your_message": "su mensaje", "your_message_optional": "su mensaje (opcional)", @@ -4714,4 +4759,4 @@ "Your_temporary_password_is_password": "Su contraseña temporal es [password].", "Your_TOTP_has_been_reset": "Su TOTP de dos factores ha sido restablecido.", "Your_workspace_is_ready": "Su espacio de trabajo está listo para usar 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/et.i18n.json b/packages/rocketchat-i18n/i18n/et.i18n.json index 1e2d4ba7a29c..a267d7e21209 100644 --- a/packages/rocketchat-i18n/i18n/et.i18n.json +++ b/packages/rocketchat-i18n/i18n/et.i18n.json @@ -74,5 +74,6 @@ "Type_your_email": "Sisesta oma e-mail", "Type_your_message": "Sisestage oma sõnum", "Type_your_name": "Sisestage oma nimi", - "Upload_file_question": "Faili üles laadima?" + "Upload_file_question": "Faili üles laadima?", + "User_left": "Kasutaja lahkus" } \ No newline at end of file diff --git a/packages/rocketchat-i18n/i18n/eu.i18n.json b/packages/rocketchat-i18n/i18n/eu.i18n.json index 6b15defb7b1d..348a9c5a76c2 100644 --- a/packages/rocketchat-i18n/i18n/eu.i18n.json +++ b/packages/rocketchat-i18n/i18n/eu.i18n.json @@ -116,6 +116,7 @@ "User_joined_channel": "Kanalera batu da.", "User_joined_channel_female": "Kanalera batu da.", "User_joined_channel_male": "kanalera batu da.", + "User_left": "Erabiltzailea irten da", "Users": "Erabiltzaileak", "We_are_offline_Sorry_for_the_inconvenience": "Lineaz kanpo gaude. Barkatu eragozpenak.", "Yes": "Bai" diff --git a/packages/rocketchat-i18n/i18n/fa.i18n.json b/packages/rocketchat-i18n/i18n/fa.i18n.json index 962f5dbde45e..3fd78b586a0b 100644 --- a/packages/rocketchat-i18n/i18n/fa.i18n.json +++ b/packages/rocketchat-i18n/i18n/fa.i18n.json @@ -380,6 +380,8 @@ "Apps_Marketplace_Login_Required_Description": "خرید برنامه از Rocket.Chat Marketplace نیاز به ثبت فضای کاری شما و ورود به سیستم دارد.", "Apps_Marketplace_Login_Required_Title": "نیازمند ورود به بازار", "Apps_Marketplace_Modify_App_Subscription": "تصحیح کردن اشتراک", + "Apps_Marketplace_pricingPlan_yearly": null, + "Apps_Marketplace_pricingPlan_yearly_perUser": null, "Apps_Marketplace_Uninstall_App_Prompt": "آیا واقعاً می خواهید این برنامه را حذف کنید؟", "Apps_Marketplace_Uninstall_Subscribed_App_Anyway": "در هر صورت حذف کنید", "Apps_Marketplace_Uninstall_Subscribed_App_Prompt": "این برنامه اشتراک دارد و حذف آن لغو نخواهد شد. اگر می خواهید این کار را انجام دهید ، لطفاً قبل از حذف اشتراک ، اشتراک خود را اصلاح کنید.", @@ -1533,7 +1535,7 @@ "hours": "ساعت ها", "Hours": "ساعت ها", "How_friendly_was_the_chat_agent": "عامل چت چقدر دوستانه بود؟", - "How_knowledgeable_was_the_chat_agent": "عامل چت چقدر آگاه بود؟", + "How_knowledgeable_was_the_chat_agent": "مامور گفتگو تا چه حد مسلط و آگاه بود؟", "How_long_to_wait_after_agent_goes_offline": "چقدر طول می کشد پس از اینکه نماینده به صورت آفلاین می رود", "How_responsive_was_the_chat_agent": "عامل چت چقدر پاسخگو بود؟", "How_satisfied_were_you_with_this_chat": "چه میزان از این چت راضی بودید؟", @@ -2279,7 +2281,7 @@ "Please_fill_a_name": "لطفا یک نام را پر کنید", "Please_fill_a_username": "لطفا یک نام کاربری پر کردن", "Please_fill_all_the_information": "لطفا تمام اطلاعات را پر کنید", - "Please_fill_name_and_email": "لطفا نام و ایمیل را پر کنید", + "Please_fill_name_and_email": "لطفا نام و ایمیل را وارد نمایید", "Please_go_to_the_Administration_page_then_Livechat_Facebook": "لطفا به صفحه مدیریت و سپس کانال همه‌کاره > فیسبوک بروید", "Please_select_an_user": "لطفا یک کاربر را انتخاب کنید", "Please_select_enabled_yes_or_no": "لطفا یک گزینه برای فعال را انتخاب کنید", @@ -2442,6 +2444,8 @@ "RetentionPolicy_MaxAge_Groups": "حداکثر سن پیام در گروه های خصوصی", "RetentionPolicy_Precision": "تایمر دقیق", "RetentionPolicy_Precision_Description": "هر چند وقت یکبار تایمر بره باید اجرا شود تنظیم این به یک مقدار دقیق تر باعث می شود کانال های با تایمر نگهداری سریع کار بهتر، اما ممکن است پردازش قدرت اضافی در جوامع بزرگ هزینه.", + "RetentionPolicy_RoomWarning_FilesOnly": null, + "RetentionPolicy_RoomWarning_UnpinnedFilesOnly": null, "RetentionPolicyRoom_Enabled": "پیام های قدیمی را به طور خودکار خرد کنید", "RetentionPolicyRoom_ExcludePinned": "پیام های پین شده را حذف کنید", "RetentionPolicyRoom_FilesOnly": "فقط پرونده ها را ببندید، پیام ها را نگه دارید", @@ -2536,7 +2540,7 @@ "seconds": "ثانیه", "Secret_token": "علامت رمز", "Security": "امنیت", - "Select_a_department": "انتخاب بخش", + "Select_a_department": "یک بخش را انتخاب کنید", "Select_a_user": "یک کاربر را انتخاب کنید", "Select_an_avatar": "انتخاب تصویر", "Select_an_option": "یک گزینه را انتخاب کنید", @@ -2825,6 +2829,8 @@ "This_email_has_already_been_used_and_has_not_been_verified__Please_change_your_password": "این ایمیل قبلا استفاده شده است و تأیید نشده است. لطفا رمز عبور خود را تغییر دهید.", "This_is_a_desktop_notification": "این یک اعلان دسکتاپ است", "This_is_a_push_test_messsage": "این messsage آزمون فشار است", + "This_room_has_been_archived_by__username_": null, + "This_room_has_been_unarchived_by__username_": null, "Thursday": "پنج شنبه", "Time_in_seconds": "زمان در ثانیه", "Title": "عنوان", @@ -2873,7 +2879,7 @@ "Type_your_email": "نوع ایمیل خود را", "Type_your_job_title": "عنوان شغلی خود را تایپ کنید", "Type_your_message": "نوع پیام خود را", - "Type_your_name": "نامتان را بنویسید", + "Type_your_name": "نام خود را وارد نمایید", "Type_your_new_password": "کلمه عبور جدید را وارد کنید", "Type_your_password": "رمز عبور خود را تایپ کنید", "Type_your_username": "نام کاربری خود را وارد کنید", @@ -3011,6 +3017,7 @@ "Users_added": "کاربران اضافه شده اند", "Users_in_role": "کاربران در نقش", "UTF8_Names_Slugify": "UTF8 نام slugify را", + "Videocall_enabled": "تماس ویدیویی فعال شد", "Validate_email_address": "اعتبار آدرس ایمیل", "Verification": "تایید", "Verification_Description": "شما ممکن است از متغیرهایی زیر استفاده کنید:
      • [Verification_Url] برای URL تأیید.
      • [نام]، [نام خانوادگی]، [lname] برای نام کامل، نام یا نام خانوادگی کاربر، به ترتیب.
      • [ایمیل] برای ایمیل کاربر.
      • [نام سایت] و [Site_URL] برای نام برنامه و URL به ترتیب.
      ", @@ -3025,7 +3032,6 @@ "Video_Conference": "ویدیو کنفرانس", "Video_message": "پیام ویدویی", "Videocall_declined": "تماس ویدیویی رد شد", - "Videocall_enabled": "تماس ویدیویی فعال شد", "View_All": "مشاهده همه", "View_Logs": "نمایش سیاهههای مربوط", "View_mode": "شیوه نمایش", @@ -3125,6 +3131,7 @@ "you_are_in_preview_mode_of": "شما در حالت پیش نمایش از کانال # __room_name__ هستند", "You_are_logged_in_as": "شما وارد شدید با عنوان", "You_are_not_authorized_to_view_this_page": "شما به این صفحه مجاز است.", + "You_can_change_a_different_avatar_too": "شما می توانید نماد مورد استفاده برای ارسال از این ادغام را لغو کنید.", "You_can_search_using_RegExp_eg": "شما میتوانید با استفاده از عبارت با قاعده جستجو کنید. به عنوان مثلا/^text$/i", "You_can_use_an_emoji_as_avatar": "همچنین می توانید از یک شکلک برای تصویر استفاده کنید.", "You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM": "می توانید از وب‌قلاب‌ها برای یکپارچه‌سازی کانال همه‌کاره با مدیریت ارتباط مشتری استفاده کنید.", @@ -3157,4 +3164,4 @@ "Your_push_was_sent_to_s_devices": "فشار خود را به دستگاه %s را ارسال شد", "Your_server_link": "لینک سرور شما", "Your_workspace_is_ready": "فضای کاری شما آماده استفاده است" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/fi.i18n.json b/packages/rocketchat-i18n/i18n/fi.i18n.json index f0da58be8949..f816082b7fb7 100644 --- a/packages/rocketchat-i18n/i18n/fi.i18n.json +++ b/packages/rocketchat-i18n/i18n/fi.i18n.json @@ -555,6 +555,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Jatkuva ääniilmoitukset uudelle livechat-huoneelle", "Conversation": "Keskustelu", "Conversation_closed": "Keskustelu suljettu: __comment__.", + "Conversation_finished": "Keskustelu päättynyt", "Conversation_finished_message": "Keskustelun valmiin viestin", "conversation_with_s": "keskustelu %s: lla", "Convert_Ascii_Emojis": "Muunna ASCII-merkit Emojiksi", @@ -1687,6 +1688,7 @@ "Max_length_is": "Suurin pituus on%s", "Media": "tiedotusvälineet", "Medium": "keskikokoinen", + "Members": "Jäsenet", "Members_List": "Jäsenlista", "mention-all": "Mainitse kaikki", "mention-all_description": "Käyttöoikeus käyttää @all-maininta", @@ -2302,6 +2304,7 @@ "Show_Setup_Wizard": "Näytä ohjattu asennustoiminto", "Show_the_keyboard_shortcut_list": "Näytä pikanäppäinten luettelo", "Showing_archived_results": "

      Näytetään %s arkistoitua tulosta

      ", + "Showing_online_users": null, "Showing_results": "

      Näytetään %s tulosta

      ", "Sidebar": "sivupalkki", "Sidebar_list_mode": "Sivupalkin kanavaluettelotila", @@ -2681,6 +2684,7 @@ "Users_added": "Käyttäjät on lisätty", "Users_in_role": "Roolin käyttäjiä", "UTF8_Names_Slugify": "Siisti UTF8-nimet", + "Videocall_enabled": "Videopuhelu käytössä", "Validate_email_address": "Validoi sähköpostiosoite", "Verification": "Varmistus", "Verification_Description": "Voit käyttää seuraavia paikanvaraajia:
      • [Verification_Url] vahvistus-URL-osoitteelle.
      • [nimi], [fname], [lname] käyttäjän koko nimen, etunimen tai sukunimen osalta.
      • [email] käyttäjän sähköposti.
      • [Sivuston nimi] ja [Sivusto_URL].
      ", @@ -2695,7 +2699,6 @@ "Video_Conference": "Videoneuvottelu", "Video_message": "VIdeoviesti", "Videocall_declined": "Videopuhelu hylätty", - "Videocall_enabled": "Videopuhelu käytössä", "View_All": "Katso kaikki", "View_Logs": "Katso lokit", "View_mode": "Näkymätila", @@ -2817,4 +2820,4 @@ "Your_push_was_sent_to_s_devices": "Push-viestisi lähetettiin %s laitteeseen", "Your_server_link": "Palvelimesi linkki", "Your_workspace_is_ready": "Työtila on valmis käyttämään 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/fr.i18n.json b/packages/rocketchat-i18n/i18n/fr.i18n.json index 5e9f924d701c..cad2e6136c12 100644 --- a/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -13,16 +13,16 @@ "%_of_conversations": "% des conversations", "0_Errors_Only": "0 - Erreurs seulement", "1_Errors_and_Information": "1 - Erreurs et informations", - "2_Erros_Information_and_Debug": "2 - Erreurs, informations et débogage ", - "12_Hour": "Horloge 12h", + "2_Erros_Information_and_Debug": "2 - Erreurs, informations et débogage", + "12_Hour": "Horloge de 12 heures", "24_Hour": "Horloge 24h", - "A_new_owner_will_be_assigned_automatically_to__count__rooms": "Un nouveau propriétaire sera automatiquement assigné à __count__ salons.", + "A_new_owner_will_be_assigned_automatically_to__count__rooms": "Un nouveau propriétaire sera automatiquement attribué à __count__ salons.", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "Un nouveau propriétaire sera automatiquement assigné au salon __roomName__.", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Un nouveau propriétaire sera automatiquement assigné à ces __count__ salons :
      __rooms__.", "Accept": "Accepter", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Acceptez les demandes omnicanales entrantes même si il n'y a pas d'agents en ligne", "Accept_new_livechats_when_agent_is_idle": "Accepter les nouvelles demandes omnicanal lorsque l'agent est inactif", - "Accept_with_no_online_agents": "Accepter sans agent en ligne", + "Accept_with_no_online_agents": "Accepter sans agents en ligne", "Access_not_authorized": "Accès non autorisé", "Access_Token_URL": "URL du jeton d'accès", "access-mailer": "Accéder à l'écran Mailer", @@ -114,6 +114,8 @@ "Accounts_OAuth_Custom_Merge_Users": "Fusionner les utilisateurs", "Accounts_OAuth_Custom_Name_Field": "Champ du nom", "Accounts_OAuth_Custom_Roles_Claim": "Nom du champ Rôles/Groupes", + "Accounts_OAuth_Custom_Roles_To_Sync": "Rôles à synchroniser", + "Accounts_OAuth_Custom_Roles_To_Sync_Description": "Rôles OAuth à synchroniser lors de la connexion et de la création de l'utilisateur (séparés par des virgules).", "Accounts_OAuth_Custom_Scope": "Portée", "Accounts_OAuth_Custom_Secret": "Secret", "Accounts_OAuth_Custom_Show_Button_On_Login_Page": "Montrer le bouton sur la page de connexion", @@ -268,7 +270,7 @@ "Add_Sender_To_ReplyTo": "Ajouter l'expéditeur à répondre à", "Add_user": "Ajouter un utilisateur", "Add_User": "Ajouter un utilisateur", - "Add_users": "Ajouter des utilisateurs", + "Add_users": "Ajouter plusieurs utilisateurs", "Add_members": "Ajouter des membres", "add-livechat-department-agents": "Ajouter des agents omnicanaux aux départements", "add-livechat-department-agents_description": "Autorisation d'ajouter des agents omnicanaux aux départements", @@ -370,6 +372,8 @@ "API_Enable_Rate_Limiter_Dev": "Activer le limiteur de débit en développement", "API_Enable_Rate_Limiter_Dev_Description": "Faut-il limiter le nombre d'appels aux points de terminaison dans l'environnement de développement ?", "API_Enable_Rate_Limiter_Limit_Calls_Default": "Le numéro par défaut appelle le limiteur de débit", + "Rate_Limiter_Limit_RegisterUser": "Nombre d'appels par défaut au limiteur de débit pour l'enregistrement d'un utilisateur", + "Rate_Limiter_Limit_RegisterUser_Description": "Nombre d'appels par défaut pour les points de terminaison d'enregistrement des utilisateursr (API REST et en temps réel), autorisées dans la plage de temps définie dans la section limiteur de taux API.", "API_Enable_Rate_Limiter_Limit_Calls_Default_Description": "Nombre d'appels par défaut pour chaque point de terminaison de l'API REST, autorisés dans la plage de temps définie ci-dessous", "API_Enable_Rate_Limiter_Limit_Time_Default": "Limite de temps par défaut pour le limiteur de débit (en ms)", "API_Enable_Rate_Limiter_Limit_Time_Default_Description": "Délai d'expiration par défaut pour limiter le nombre d'appels à chaque point de terminaison de l'API REST (en ms)", @@ -385,13 +389,14 @@ "API_Personal_Access_Tokens_Regenerate_Modal": "Si vous avez perdu ou oublié votre jeton, vous pouvez le régénérer, mais n'oubliez pas que toutes les applications utilisant ce jeton doivent être mises à jour", "API_Personal_Access_Tokens_Remove_Modal": "Voulez-vous vraiment supprimer ce jeton d'accès personnel ?", "API_Personal_Access_Tokens_To_REST_API": "Jetons d'accès personnels à l'API REST", + "API_Rate_Limiter": "Limiteur de taux API (API Rate Limiter)", "API_Shield_Types": "Types de bouclier", "API_Shield_Types_Description": "Types de boucliers à activer en tant que liste séparée par des virgules, choisissez parmi `online`,` channel` ou `*` pour tout", "API_Shield_user_require_auth": "Exiger une authentification pour les boucliers des utilisateurs", "API_Token": "Jeton API", "API_Tokenpass_URL": "URL du serveur Tokenpass", "API_Tokenpass_URL_Description": "Exemple : https://domain.com (ne pas mettre le / de fin)", - "API_Upper_Count_Limit": "Nombre maximum d'enregistrements", + "API_Upper_Count_Limit": "Nombre maximal d'enregistrements", "API_Upper_Count_Limit_Description": "Quel est le nombre maximum d'enregistrements que REST API doit retourner (si pas défini en tant qu'illimité) ?", "API_Use_REST_For_DDP_Calls": "Utilisez REST au lieu de websocket pour les appels Meteor", "API_User_Limit": "Limite de l'utilisateur pour ajouter tous les utilisateurs au canal", @@ -415,7 +420,7 @@ "App_status_manually_disabled": "Désactivé : manuellement", "App_status_manually_enabled": "Activé", "App_status_unknown": "Inconnu", - "App_support_url": "support url", + "App_support_url": "URL de support", "App_Url_to_Install_From": "Installer depuis l'URL", "App_Url_to_Install_From_File": "Installer à partir d'un fichier", "App_user_not_allowed_to_login": "Les utilisateurs de l'application ne sont pas autorisés à se connecter directement.", @@ -550,7 +555,7 @@ "assign-roles": "Attribuer des rôles", "assign-roles_description": "Autorisation d'attribuer des rôles à d'autres utilisateurs", "at": "à", - "At_least_one_added_token_is_required_by_the_user": "Au moins un jeton ajouté est requis par l'utilisateur", + "At_least_one_added_token_is_required_by_the_user": "L'utilisateur a besoin d'au moins un jeton ajouté", "AtlassianCrowd": "Atlassian Crowd", "Attachment_File_Uploaded": "Fichier envoyé", "Attribute_handling": "Gestion des attributs", @@ -678,7 +683,7 @@ "Browse_Files": "Parcourir les fichiers", "Browser_does_not_support_audio_element": "Votre navigateur ne supporte pas l'élément audio.", "Browser_does_not_support_video_element": "Votre application ne supporte pas le format de l'élément vidéo.", - "Bugsnag_api_key": "Clé d'API Bugsnag", + "Bugsnag_api_key": "Clé API Bugsnag", "Build_Environment": "Construire l'environnement", "bulk-register-user": "Créer des utilisateurs en masse", "bulk-register-user_description": "Permission de créer des utilisateurs en masse", @@ -701,6 +706,9 @@ "By_author": "Par __author__", "cache_cleared": "Cache effacé", "Call": "Appel", + "Call_declined": "Appel refusé !", + "Call_provider": "Fournisseur d'appels", + "Call_Already_Ended": "Appel déjà terminé", "call-management": "Gestion des appels", "call-management_description": "Autorisation de démarrer une réunion", "Caller": "Appelant", @@ -729,7 +737,7 @@ "CAS_Creation_User_Enabled": "Autoriser la création d'utilisateurs", "CAS_Creation_User_Enabled_Description": "Autoriser la création d'utilisateurs CAS à partir des données fournies dans le ticket CAS.", "CAS_enabled": "Activé", - "CAS_Login_Layout": "Disposition de connexion CAS", + "CAS_Login_Layout": "Présentation de la connexion CAS", "CAS_login_url": "URL de login SSO", "CAS_login_url_Description": "URL de connexion pour votre service externe de connexion SSO, par exemple : https://sso.example.undef/sso/login", "CAS_popup_height": "Hauteur de la fenêtre popup de connexion", @@ -761,7 +769,7 @@ "Channel_doesnt_exist": "Le canal `#%s` n'existe pas.", "Channel_Export": "Exportation de canal", "Channel_name": "Nom du canal", - "Channel_Name_Placeholder": "Veuillez entrer le nom du canal...", + "Channel_Name_Placeholder": "Veuillez saisir le nom du canal...", "Channel_to_listen_on": "Canal à écouter", "Channel_Unarchived": "Canal avec le nom `#%s` a été désarchivé avec succès", "Channels": "Canaux", @@ -770,7 +778,7 @@ "Channels_list": "Liste des canaux publics", "Channel_what_is_this_channel_about": "De quoi parle ce canal ?", "Chart": "Graphique", - "Chat_button": "Bouton chat", + "Chat_button": "Bouton de chat", "Chat_close": "Fermer chat", "Chat_closed": "Chat fermé", "Chat_closed_by_agent": "Chat fermé par l'agent", @@ -799,7 +807,7 @@ "Chatpal_Base_URL": "Url de base", "Chatpal_Base_URL_Description": "Trouver une description de la façon d'exécuter une instance locale sur github. L'URL doit être absolue et pointer vers le noyau chatpal, par ex. http://localhost:8983/solr/chatpal.", "Chatpal_Batch_Size": "Taille du lot d'index", - "Chatpal_Batch_Size_Description": "La taille du lot des documents d'index (lors de l'amorçage)", + "Chatpal_Batch_Size_Description": "La taille du lot de documents d'index (lors de l'amorçage)", "Chatpal_channel_not_joined_yet": "Canal pas encore rejoint", "Chatpal_create_key": "Créer une clé", "Chatpal_created_key_successfully": "Clé d'API créée avec succès", @@ -815,7 +823,7 @@ "Chatpal_Get_more_information_about_chatpal_on_our_website": "Obtenez plus d'informations sur Chatpal sur http://chatpal.io!", "Chatpal_go_to_message": "Sauter", "Chatpal_go_to_room": "Sauter", - "Chatpal_go_to_user": "Envoyer un message privé", + "Chatpal_go_to_user": "Envoyer un message direct", "Chatpal_HTTP_Headers": "En-têtes HTTP", "Chatpal_HTTP_Headers_Description": "Liste des en-têtes HTTP, un en-tête par ligne. Format: nom: valeur", "Chatpal_Include_All_Public_Channels": "Inclure tous les canaux publics", @@ -829,10 +837,10 @@ "Chatpal_no_search_results": "Aucun résultat", "Chatpal_one_search_result": "1 résultat trouvé", "Chatpal_Rooms": "Salons", - "Chatpal_run_search": "Recherche", + "Chatpal_run_search": "Rechercher", "Chatpal_search_page_of": "Page %s sur %s", "Chatpal_search_results": "%s résultats trouvés ", - "Chatpal_Search_Results": "Résultats de la recherche", + "Chatpal_Search_Results": "Résultats de recherche", "Chatpal_Suggestion_Enabled": "Suggestions activées", "Chatpal_TAC_read": "J'ai lu les termes et conditions", "Chatpal_Terms_and_Conditions": "Termes et conditions", @@ -1265,7 +1273,7 @@ "Created_as": "Créé comme", "Created_at": "Créé le", "Created_at_s_by_s": "Créé le %s par %s", - "Created_at_s_by_s_triggered_by_s": "Créé à %s en %s déclenché par %s", + "Created_at_s_by_s_triggered_by_s": "Créé à %s par %s déclenché par %s", "Created_by": "Créé par", "CRM_Integration": "Intégration CRM (GRC)", "CROWD_Allow_Custom_Username": "Autoriser le nom d'utilisateur personnalisé dans Rocket.Chat", @@ -1340,6 +1348,7 @@ "Days": "Jours", "DB_Migration": "Mise à jour de la base de données", "DB_Migration_Date": "Date de mise à jour de la base de données", + "DDP_Rate_Limit": "Limite du taux DDP", "DDP_Rate_Limit_Connection_By_Method_Enabled": "Limite par connexion par méthode : activée", "DDP_Rate_Limit_Connection_By_Method_Interval_Time": "Limite par connexion par méthode : temps d'intervalle", "DDP_Rate_Limit_Connection_By_Method_Requests_Allowed": "Limite par connexion par méthode : demandes autorisées", @@ -1369,7 +1378,7 @@ "Delete_Role_Warning": "La suppression d'un rôle est définitive. Elle ne peut être annulée.", "Delete_Room_Warning": "Supprimer un salon supprimera également tous les messages postés dans le salon. Cette action est irréversible.", "Delete_User_Warning": "Supprimer un utilisateur va également supprimer tous les messages de celui-ci. Cette action est irréversible.", - "Delete_User_Warning_Delete": "Supprimer un utilisateur va également supprimer tous les messages de celui-ci. Cette action est irréversible.", + "Delete_User_Warning_Delete": "Supprimer un utilisateur supprimera également tous les messages de celui-ci. Cette action est irréversible.", "Delete_User_Warning_Keep": "L'utilisateur sera supprimé, mais ses messages resteront visibles. Ça ne peut pas être annulé.", "Delete_User_Warning_Unlink": "Supprimer un utilisateur supprimera le nom d'utilisateur de tous ses messages. Ça ne peut pas être annulé.", "delete-c": "Supprimer les canaux publics", @@ -1396,7 +1405,7 @@ "Desktop": "Bureau", "Desktop_Notification_Test": "Test des notifications sur le bureau", "Desktop_Notifications": "Notifications de bureau", - "Desktop_Notifications_Default_Alert": "Alerte notification de bureau par défaut", + "Desktop_Notifications_Default_Alert": "Alerte par défaut pour les notifications de bureau", "Desktop_Notifications_Disabled": "Les notifications de bureau sont désactivées, Modifiez les préférences de votre navigateur si vous avez besoin de les activer.", "Desktop_Notifications_Duration": "Durée des notifications de bureau", "Desktop_Notifications_Duration_Description": "Secondes pour afficher une notification de bureau. Cela peut affecter le Centre de Notification de OS X. Entrez 0 pour utiliser les paramètres du navigateur par défaut et ne pas affecter le Centre de Notification de OS X.", @@ -1609,6 +1618,8 @@ "Encryption_key_saved_successfully": "Votre clé de chiffrement a été enregistrée avec succès.", "EncryptionKey_Change_Disabled": "Vous ne pouvez pas définir de mot de passe pour votre clé de chiffrement car votre clé privée n'est pas présente sur ce client. Afin de définir un nouveau mot de passe, vous devez charger votre clé privée en utilisant votre mot de passe existant ou utiliser un client où la clé est déjà chargée.", "End": "Fin", + "End_call": "Mettre fin à l'appel", + "Expand_view": "Agrandir la vue", "End_OTR": "Arrêter OTR", "Engagement_Dashboard": "Tableau de bord d'engagement", "Enter": "Entrer", @@ -1750,7 +1761,7 @@ "error-password-same-as-current": "Mot de passe saisi identique au mot de passe actuel", "error-personal-access-tokens-are-current-disabled": "Les jetons d'accès personnels sont actuellement désactivés", "error-pinning-message": "Le message n'a pas pu être épinglé", - "error-push-disabled": "Push est désactivé", + "error-push-disabled": "Le Push est désactivé", "error-remove-last-owner": "Cet utilisateur est le dernier propriétaire. Veuillez sélectionner un nouveau propriétaire avant de retirer celui-ci.", "error-returning-inquiry": "Erreur lors du renvoi de la demande dans la file d'attente", "error-role-in-use": "Impossible de supprimer le rôle car il est utilisé", @@ -1836,7 +1847,9 @@ "Favorite": "Favori", "Favorite_Rooms": "Activer les salons favoris", "Favorites": "Favoris", + "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "Cette fonction dépend du fournisseur d'appel sélectionné ci-dessus à activer à partir des paramètres d'administration.", "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "Cette fonctionnalité dépend de l'activation de \"Envoyer l'historique de navigation des visiteurs sous forme de message\".", + "Feature_Limiting": "Limitation des fonctionnalités", "Features": "Fonctionnalités", "Features_Enabled": "Fonctionnalités activées", "Feature_Disabled": "Fonction désactivée", @@ -2012,7 +2025,7 @@ "force-delete-message_description": "Autorisation de supprimer un message en contournant toutes les restrictions", "Forgot_password": "Mot de passe oublié ?", "Forgot_Password_Description": "Vous pouvez utiliser les espaces réservés suivants :
      • [Forgot_Password_Url] pour l'URL de récupération du mot de passe.
      • [nom], [fname], [lname] pour le nom complet, le prénom ou le nom de famille de l'utilisateur, respectivement.
      • [email] pour l'adresse e-mail de l'utilisateur.
      • [Site_Name] et [Site_URL] pour le nom de l'application et l'URL, respectivement.
      ", - "Forgot_Password_Email": "Cliquez ici pour remettre à zéro votre mot de passe.", + "Forgot_Password_Email": "Cliquez ici pour réinitialiser votre mot de passe.", "Forgot_Password_Email_Subject": "[Site_Name] - Récupération du mot de passe", "Forgot_password_section": "Mot de passe oublié", "Forward": "Transmettre", @@ -2073,7 +2086,7 @@ "Healthcare": "Soins de santé", "Helpers": "Aides", "Here_is_your_authentication_code": "Voici votre code d'authentification :", - "Hex_Color_Preview": "Aperçu des couleurs hexadécimales", + "Hex_Color_Preview": "Aperçu de la couleur hexadécimale", "Hi": "Salut", "Hi_username": "Bonjour __name__", "Hidden": "Caché", @@ -2089,6 +2102,7 @@ "Hide_System_Messages": "Masquer les messages système", "Hide_Unread_Room_Status": "Masquer le statut des salons non lus", "Hide_usernames": "Masquer les noms d'utilisateur", + "Hide_video": "Masquer la vidéo", "Highlights": "Mises en avant", "Highlights_How_To": "Pour être notifié(e) lorsque quelqu'un écrit un mot ou une phrase spécifique, ajoutez le/la ici. Vous pouvez les séparer par des virgules. Les termes surveillés ne sont pas sensibles à la casse.", "Highlights_List": "Mots surveillés", @@ -2100,10 +2114,10 @@ "Hours": "Heures", "How_friendly_was_the_chat_agent": "Votre interlocuteur était-il amical ?", "How_knowledgeable_was_the_chat_agent": "Votre interlocuteur était-il clair ?", - "How_long_to_wait_after_agent_goes_offline": "Combien de temps attendre après que l'agent soit hors ligne", + "How_long_to_wait_after_agent_goes_offline": "Combien de temps attendre une fois que l'agent est hors ligne", "How_long_to_wait_to_consider_visitor_abandonment": "Combien de temps faut-il attendre avant d'envisager l'abandon d'un visiteur?", "How_long_to_wait_to_consider_visitor_abandonment_in_seconds": "Combien de temps faut-il attendre avant d'envisager l'abandon d'un visiteur?", - "How_responsive_was_the_chat_agent": "Quel a été la réactivité de l'agent de chat ?", + "How_responsive_was_the_chat_agent": "Quelle a été la réactivité de l'agent de chat ?", "How_satisfied_were_you_with_this_chat": "Étiez-vous satisfait de ce chat?", "How_to_handle_open_sessions_when_agent_goes_offline": "Comment gérer les sessions ouvertes lorsque l'agent est déconnecté", "I_Saved_My_Password": "J'ai enregistré mon mot de passe", @@ -2115,7 +2129,7 @@ "If_you_are_sure_type_in_your_username": "Si vous êtes certain(e), saisissez votre nom d'utilisateur :", "If_you_didnt_ask_for_reset_ignore_this_email": "Si vous n'avez pas demandé la réinitialisation de votre mot de passe, vous pouvez ignorer cet e-mail.", "If_you_didnt_try_to_login_in_your_account_please_ignore_this_email": "Si vous n'avez pas essayé de vous connecter à votre compte, veuillez ignorer cet e-mail.", - "If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours": "Si vous n'en avez pas, envoyez un courriel à [omni@rocket.chat](mailto: omni@rocket.chat) pour obtenir le vôtre.", + "If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours": "Si vous n'en avez pas, envoyez un e-mail à [omni@rocket.chat](mailto: omni@rocket.chat) pour obtenir le vôtre.", "Iframe_Integration": "Intégration Iframe", "Iframe_Integration_receive_enable": "Activer la réception", "Iframe_Integration_receive_enable_Description": "Autoriser la fenêtre parente à envoyer des commandes à Rocket.Chat.", @@ -2148,7 +2162,7 @@ "Importer_finishing": "Finalisation de l'importation.", "Importer_From_Description": "Importer les données de __from__ dans Rocket.Chat.", "Importer_HipChatEnterprise_BetaWarning": "Veuillez noter que cette importation est toujours en cours de développement, veuillez signaler toute erreur qui se produit dans GitHub :", - "Importer_HipChatEnterprise_Information": "Le fichier téléchargé doit être un tar.gz déchiffré, veuillez lire la documentation pour plus d'informations :", + "Importer_HipChatEnterprise_Information": "Le fichier envoyé doit être un tar.gz déchiffré, veuillez lire la documentation pour plus d'informations :", "Importer_import_cancelled": "Importation annulée.", "Importer_import_failed": "Une erreur est survenue lors de l'importation.", "Importer_importing_channels": "Importation des canaux.", @@ -2204,7 +2218,7 @@ "Install": "Installer", "Install_Extension": "Installer l'extension", "Install_FxOs": "Installez Rocket.Chat sur votre Firefox", - "Install_FxOs_done": "Génial ! Vous pouvez désormais utiliser Rocket.Chat via l'icône sur votre écran d'accueil. Amusez-vous avec Rocket.Chat !", + "Install_FxOs_done": "Génial ! Vous pouvez maintenant utiliser Rocket.Chat via l'icône sur votre écran d'accueil. Amusez-vous avec Rocket.Chat !", "Install_FxOs_error": "Désolé, cela n'a pas fonctionné comme prévu ! L'erreur suivante est apparue :", "Install_FxOs_follow_instructions": "Veuillez confirmer l'installation de l'application sur votre appareil (appuyez sur \"Installer\" lorsque c'est demandé).", "Install_package": "Installer le paquet", @@ -2214,7 +2228,7 @@ "Instance": "Instance", "Instances": "Instances", "Instances_health": "Santé des instances", - "Instance_Record": "Instance Record", + "Instance_Record": "Enregistrement d'instance", "Instructions": "Instructions", "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Instructions à votre visiteur remplissez le formulaire pour envoyer un message", "Insert_Contact_Name": "Insérez le nom du contact", @@ -2291,7 +2305,7 @@ "Invitation": "Invitation", "Invitation_Email_Description": "Vous pouvez utiliser les espaces réservés suivants :
      • [email] pour l'adresse e-mail du destinataire,
      • [Site_Name] et [Site_URL] pour le nom de l'application et l'URL respectivement.
      ", "Invitation_HTML": "HTML d'invitation", - "Invitation_HTML_Default": "

      Vous avez été invité à rejoindre [Site_Name]

      Accédez à [Site_URL] et essayez la meilleure solution de chat open source disponible aujourd'hui !

      ", + "Invitation_HTML_Default": "

      Vous avez été invité à rejoindre [Site_Name]

      Allez sur [Site_URL] et essayez la meilleure solution de chat open source disponible aujourd'hui !

      ", "Invitation_Subject": "Sujet de l'invitation", "Invitation_Subject_Default": "Vous avez été invité à [Site_Name]", "Invite": "Inviter", @@ -2338,12 +2352,14 @@ "Jitsi_Limit_Token_To_Room": "Limiter le jeton au salon Jitsi", "Job_Title": "Titre d'emploi", "join": "Rejoindre", + "Join_call": "Rejoindre l'appel", "Join_audio_call": "Rejoindre l'appel audio", "Join_Chat": "Rejoindre le chat", "Join_default_channels": "Rejoindre les canaux par défaut", "Join_the_Community": "Rejoignez la communauté", "Join_the_given_channel": "Rejoindre le canal choisi", "Join_video_call": "Rejoindre l'appel vidéo", + "Join_my_room_to_start_the_video_call": "Rejoignez mon salon pour démarrer l'appel vidéo", "join-without-join-code": "Rejoindre sans code d'adhésion", "join-without-join-code_description": "Autorisation de contourner le code de participation dans les canaux avec le code de participation activé", "Joined": "A rejoint", @@ -2352,7 +2368,7 @@ "Jump_to_first_unread": "Aller au premier non lu", "Jump_to_message": "Aller au message", "Jump_to_recent_messages": "Aller aux messages récents", - "Just_invited_people_can_access_this_channel": "Seules les personnes invitées peuvent accéder à cette chaîne.", + "Just_invited_people_can_access_this_channel": "Seules les personnes invitées peuvent accéder à ce canal.", "Katex_Dollar_Syntax": "Autoriser la syntaxe dollar", "Katex_Dollar_Syntax_Description": "Autoriser les syntaxes : $$bloc katex$$ et $katex inline$", "Katex_Enabled": "Katex activé", @@ -2372,7 +2388,7 @@ "Keyboard_Shortcuts_Mark_all_as_read": "Marquer tous les messages (dans tous les canaux) comme lus", "Keyboard_Shortcuts_Move_To_Beginning_Of_Message": "Aller au début du message", "Keyboard_Shortcuts_Move_To_End_Of_Message": "Aller à la fin du message", - "Keyboard_Shortcuts_New_Line_In_Message": "Nouvelle ligne dans l'entrée de composition du message", + "Keyboard_Shortcuts_New_Line_In_Message": "Nouvelle ligne dans l'entrée de composition de message", "Keyboard_Shortcuts_Open_Channel_Slash_User_Search": "Ouvrir la recherche de canal / utilisateur", "Keyboard_Shortcuts_Title": "Raccourcis clavier", "Knowledge_Base": "Base de connaissances", @@ -2551,6 +2567,10 @@ "LDAP_Sync_User_Data_Roles_Filter_Description": "Le filtre de recherche LDAP utilisé pour vérifier si un utilisateur fait partie d'un groupe.", "LDAP_Sync_User_Data_RolesMap": "Carte des groupes de données utilisateur", "LDAP_Sync_User_Data_RolesMap_Description": "Mappez les groupes LDAP aux rôles des utilisateurs Rocket.Chat
      Par exemple, `{\"rocket-admin\":\"admin\", \"tech-support\":\"support\"}` mappera le groupe LDAP rocket-admin au rôle \"admin\" de Rocket.", + "LDAP_Teams_BaseDN": "LDAP teams BaseDN", + "LDAP_Teams_BaseDN_Description": "Le LDAP BaseDN utilisé pour rechercher des équipes d'utilisateurs.", + "LDAP_Teams_Name_Field": "Attribut de nom d'équipe LDAP", + "LDAP_Teams_Name_Field_Description": "L'attribut LDAP que Rocket.Chat doit utiliser pour charger le nom de l'équipe. Vous pouvez spécifier plusieurs noms d'attributs possibles si vous les séparez par une virgule.", "LDAP_Timeout": "Délai d'attente (ms)", "LDAP_Timeout_Description": "Combien de millisecondens faut-il attendre pour obtenir un résultat de recherche avant de renvoyer une erreur", "LDAP_Unique_Identifier_Field": "Champ d'identifiant unique", @@ -2628,6 +2648,7 @@ "Livechat_Managers": "Gestionnaires", "Livechat_max_queue_wait_time_action": "Comment gérer les chats en file d'attente lorsque le temps d'attente maximal est atteint", "Livechat_maximum_queue_wait_time": "Temps d'attente maximum dans la file d'attente", + "Livechat_maximum_queue_wait_time_description": "Durée maximale (en minutes) pour garder les chats en file d'attente. -1 signifie illimité", "Livechat_message_character_limit": "Limite de caractères des messages en direct", "Livechat_monitors": "Moniteurs de chat en direct", "Livechat_Monitors": "Moniteurs", @@ -2663,6 +2684,7 @@ "Livechat_Triggers": "Déclencheurs de chat en direct", "Livechat_user_sent_chat_transcript_to_visitor": "__agent__ a envoyé la transcription du chat à __guest__", "Livechat_Users": "Utilisateurs omnicanal", + "Livechat_Calls": "Appels de chat en direct", "Livechat_visitor_email_and_transcript_email_do_not_match": "L'e-mail du visiteur et l'e-mail de la transcription ne correspondent pas", "Livechat_visitor_transcript_request": "__guest__ a demandé la transcription du chat", "LiveStream & Broadcasting": "LiveStream et diffusion", @@ -2799,6 +2821,7 @@ "Markdown_Parser": "Markdown Parser", "Markdown_SupportSchemesForLink": "Schémas de liens pris en charge de Markdown", "Markdown_SupportSchemesForLink_Description": "Liste des schémas autorisés séparés par des virgules", + "Marketplace": "Place de marché", "Marketplace_view_marketplace": "Voir le Marketplace", "MAU_value": "MAU __value__", "Max_length_is": "La longueur maximale est %s", @@ -2916,7 +2939,7 @@ "Message_QuoteChainLimit": "Nombre maximum de citations enchaînées", "Message_Read_Receipt_Enabled": "Afficher les confirmations de lecture", "Message_Read_Receipt_Store_Users": "Reçus de lecture détaillés", - "Message_Read_Receipt_Store_Users_Description": "Affiche les conformations de lecture de chaque utilisateur", + "Message_Read_Receipt_Store_Users_Description": "Afficher les confirmations de lecture de chaque utilisateur", "Message_removed": "Message supprimé", "Message_sent_by_email": "Message envoyé par e-mail", "Message_ShowDeletedStatus": "Afficher le statut de suppression", @@ -2972,6 +2995,8 @@ "Mobex_sms_gateway_restful_address_desc": "IP ou hôte de votre REST API Mobex, par exemple `http://192.168.1.1:8080` ou `https://www.example.com:8080`", "Mobex_sms_gateway_username": "Nom d'utilisateur", "Mobile": "Mobile", + "mobile-download-file": "Autoriser le téléchargement de fichiers sur les appareils mobiles", + "mobile-upload-file": "Autoriser l'envoi de fichiers sur les appareils mobiles", "Mobile_Push_Notifications_Default_Alert": "Alerte par défaut des notifications push", "Monday": "Lundi", "Mongo_storageEngine": "Moteur de stockage Mongo", @@ -2996,11 +3021,13 @@ "Msgs": "Messages", "multi": "multiple", "multi_line": "multi ligne", + "Mute": "Mettre en sourdine", "Mute_all_notifications": "Mettre toutes les notifications en sourdine", "Mute_Focused_Conversations": "Mettre en sourdine les conversations ciblées", "Mute_Group_Mentions": "Ignorer les mentions @all et @here", "Mute_someone_in_room": "Rendre quelqu'un muet dans ce salon", "Mute_user": "Rendre l'utilisateur muet", + "Mute_microphone": "Couper le micro", "mute-user": "Rendre l'utilisateur muet", "mute-user_description": "Autorisation de couper le son des autres utilisateurs du même canal", "Muted": "En sourdine", @@ -3168,6 +3195,8 @@ "Omnichannel": "Omnicanal", "Omnichannel_Directory": "Annuaire omnicanal", "Omnichannel_appearance": "Apparence omnicanal", + "Omnichannel_calculate_dispatch_service_queue_statistics": "Calculer et diffuser les statistiques de file d'attente omnicanal", + "Omnichannel_calculate_dispatch_service_queue_statistics_Description": "Traitement et distribution des statistiques de file d'attente telles que la position et le temps d'attente estimé. Si *Livechat canal* n'est pas utilisé, il est recommandé de désactiver ce paramètre et d'empêcher le serveur d'effectuer des processus inutiles.", "Omnichannel_Contact_Center": "Centre de contact omnicanal", "Omnichannel_contact_manager_routing": "Attribuer de nouvelles conversations au gestionnaire de contacts", "Omnichannel_contact_manager_routing_Description": "Ce paramètre attribue un chat au gestionnaire de contacts attribué, tant que le gestionnaire de contacts est en ligne lors que le chat commence", @@ -3178,6 +3207,7 @@ "Omnichannel_External_Frame_URL": "URL du cadre externe", "On": "Sur", "On_Hold_Chats": "En attente", + "On_Hold_conversations": "Conversations en attente", "online": "en ligne", "Online": "Connecté", "Only_authorized_users_can_write_new_messages": "Seuls les utilisateurs autorisés peuvent écrire de nouveaux messages", @@ -3394,6 +3424,7 @@ "Query_description": "Conditions supplémentaires pour déterminer à quels utilisateurs envoyer l'e-mail. Les utilisateurs désabonnés sont automatiquement supprimés de la requête. La requête doit être au format JSON. Exemple : \"{\"createdAt\":{\"$gt\":{\"$date\": \"2015-01-01T00:00:00.000Z\"}}}\"", "Query_is_not_valid_JSON": "La requête n'est pas valide JSON", "Queue": "File d'attente", + "Queue_delay_timeout": "Délai d'attente du traitement de la file d'attente", "Queue_Time": "Temps de file d'attente", "Queue_management": "Gestion des files d'attente", "quote": "citation", @@ -3822,6 +3853,7 @@ "Setup_Wizard": "Assistant de configuration", "Setup_Wizard_Info": "Nous allons vous guider pour configurer votre premier compte administrateur, votre organisation et enregistrer votre serveur pour recevoir des notifications push gratuitement, et plus encore.", "Share_Location_Title": "Partager votre position ?", + "Share_screen": "Partager l'écran", "New_CannedResponse": "Nouveau modèle de réponse", "Edit_CannedResponse": "Modifier une réponse standardisée", "Sharing": "Partager", @@ -3849,6 +3881,7 @@ "Show_room_counter_on_sidebar": "Afficher le compteur du salon dans la barre latérale", "Show_Setup_Wizard": "Afficher l'assistant de configuration", "Show_the_keyboard_shortcut_list": "Afficher la liste des raccourcis clavier", + "Show_video": "Afficher la vidéo", "Showing_archived_results": "

      Affichage de %s résultats archivés

      ", "Showing_online_users": "Affichage de __total_showing__, en ligne : __online__, total : __total__ utilisateurs", "Showing_results": "

      Afficher %s résultats

      ", @@ -3897,7 +3930,7 @@ "Smileys_and_People": "Émojis & Portraits", "SMS": "SMS", "SMS_Default_Omnichannel_Department": "Département omnicanal (par défaut)", - "SMS_Default_Omnichannel_Department_Description": "S'il est défini, toutes les nouvelles discussions entrantes, initiées par cette intégration, seront acheminées vers ce département.", + "SMS_Default_Omnichannel_Department_Description": "S'il est défini, tous les nouveaux chats entrants initiés par cette intégration seront acheminés vers ce département.\nCe paramètre peut être écrasé en passant le paramètre de requête department dans la demande.\nExemple : https:///api/v1/livechat/sms-incoming/twilio?department=.\nRemarque : si vous utilisez le nom du département, l'URL doit être sécurisée.", "SMS_Enabled": "SMS activés", "SMTP": "SMTP", "SMTP_Host": "Hôte SMTP", @@ -3979,6 +4012,7 @@ "StatusMessage_Placeholder": "Que faites-vous en ce moment ?", "StatusMessage_Too_Long": "Message de statut doit être inférieure à 120 caractères.", "Step": "Étape", + "Stop_call": "Arrêter l'appel", "Stop_Recording": "Arrêter l'enregistrement", "Store_Last_Message": "Enregistrer le dernier message", "Store_Last_Message_Sent_per_Room": "Stocker le dernier message envoyé sur chaque salon.", @@ -4270,6 +4304,8 @@ "Tuesday": "Mardi", "Turn_OFF": "Éteindre", "Turn_ON": "Allumer", + "Turn_on_video": "Activer la vidéo", + "Turn_off_video": "Désactiver la vidéo", "Two Factor Authentication": "Authentification à deux facteurs", "Two-factor_authentication": "Authentification à deux facteurs via TOTP", "Two-factor_authentication_disabled": "Authentification à deux facteurs désactivée", @@ -4279,7 +4315,7 @@ "Two-factor_authentication_is_currently_disabled": "L'authentification à deux facteurs via TOTP est actuellement désactivée", "Two-factor_authentication_native_mobile_app_warning": "ATTENTION : Une fois que vous l'aurez activé, vous ne pourrez pas vous connecter aux applications mobiles natives (Rocket.Chat+) en utilisant votre mot de passe tant qu'elles n'auront pas implémenté le 2FA.", "Type": "Type", - "typing": "en tapant", + "typing": "en train d'écrire", "Types": "Types", "Types_and_Distribution": "Types et distribution", "Type_your_email": "Entrez votre e-mail", @@ -4315,6 +4351,7 @@ "Unit_removed": "Unité supprimée", "Unknown_Import_State": "Etat d'importation inconnu", "Unlimited": "Illimité", + "Unmute": "Pour réactiver le son", "Unmute_someone_in_room": "Rendre la parole à quelqu'un dans ce salon", "Unmute_user": "Rendre la parole", "Unnamed": "Sans nom", @@ -4330,6 +4367,7 @@ "Unread_Rooms_Mode": "Mode des salons non-lus", "Unread_Tray_Icon_Alert": "Icône d'alerte dans la barre de tâches pour les messages non lus", "Unstar_Message": "Supprimer des favoris", + "Unmute_microphone": "Activer le micro", "Update": "Mettre à jour", "Update_EnableChecker": "Activer la vérification des mises à jour", "Update_EnableChecker_Description": "Vérifie automatiquement les nouvelles mises à jour / messages importants des développeurs Rocket.Chat et reçoit des notifications lorsqu'elles sont disponibles. La notification apparaît une fois par nouvelle version sous forme de bannière cliquable et de message du bot Rocket.Cat, tous deux visibles uniquement pour les administrateurs.", @@ -4357,7 +4395,7 @@ "Usage": "Utilisation", "Use": "Utiliser", "Use_account_preference": "Utiliser les préférences du compte", - "Use_Emojis": "Utiliser les émoticônes", + "Use_Emojis": "Utiliser les Emojis", "Use_Global_Settings": "Utiliser les paramètres globaux", "Use_initials_avatar": "Utiliser les initiales de votre nom d'utilisateur", "Use_minor_colors": "Utiliser une palette de couleurs mineure (les valeurs par défaut héritent des couleurs principales)", @@ -4490,6 +4528,7 @@ "UTF8_User_Names_Validation_Description": "RegExp qui sera utilisé pour valider les noms d'utilisateur", "UTF8_Channel_Names_Validation": "Validation des noms de canaux UTF8", "UTF8_Channel_Names_Validation_Description": "RegExp qui sera utilisé pour valider les noms de canaux", + "Videocall_enabled": "Appel vidéo activé", "Validate_email_address": "Valider l'adresse e-mail", "Validation": "Validation", "Value_messages": "__value__ messages", @@ -4500,7 +4539,7 @@ "Verification_email_body": "Vous avez créé un compte avec succès sur [Site_Name]. Cliquez sur le bouton ci-dessous pour confirmer votre adresse e-mail et terminer votre inscription.", "Verification_email_sent": "E-mail de vérification envoyé", "Verification_Email_Subject": "[Site_Name] - Vérification de l'adresse e-mail", - "Verified": "Vérifié", + "Verified": "Vérifié(e)", "Verify": "Vérifier", "Verify_your_email": "Vérifiez votre e-mail", "Verify_your_email_for_the_code_we_sent": "Vérifiez votre e-mail pour le code que nous avons envoyé", @@ -4511,10 +4550,12 @@ "Video_Conference": "Conférence vidéo", "Video_message": "Message vidéo", "Videocall_declined": "Appel vidéo refusé.", - "Videocall_enabled": "Appel vidéo activé", + "Video_and_Audio_Call": "Appel vidéo et audio", "Videos": "Vidéos", "View_All": "Voir tous les membres", "View_channels": "Afficher les canaux", + "view-omnichannel-contact-center": "Afficher le centre de contact omnicanal", + "view-omnichannel-contact-center_description": "Autorisation d'afficher et d'interagir avec le centre de contact omnicanal", "View_Logs": "Voir les logs (journaux)", "View_mode": "Mode d'affichage", "View_original": "Voir l'original", @@ -4588,6 +4629,7 @@ "Visitor_message": "Messages des visiteurs", "Visitor_Name": "Nom du visiteur", "Visitor_Name_Placeholder": "Veuillez saisir un nom de visiteur...", + "Visitor_does_not_exist": "Le visiteur n'existe pas !", "Visitor_Navigation": "Navigation des visiteur", "Visitor_page_URL": "Page d'accueil du visiteur (URL)", "Visitor_time_on_site": "Temps des visiteurs sur le site", @@ -4615,6 +4657,7 @@ "Webhook_Details": "Détails du WebHook", "Webhook_URL": "Webhook URL", "Webhooks": "Webhooks", + "WebRTC_Call": "Appel WebRTC", "WebRTC_direct_audio_call_from_%s": "Appel audio direct de %s", "WebRTC_direct_video_call_from_%s": "Appel vidéo direct de %s", "WebRTC_Enable_Channel": "Activer pour les canaux publics", @@ -4625,6 +4668,8 @@ "WebRTC_monitor_call_from_%s": "Surveiller l'appel de %s", "WebRTC_Servers": "Serveurs STUN/TURN", "WebRTC_Servers_Description": "Une liste de serveurs STUN et TURN séparés par une virgule.
      Vous pouvez utiliser utilisateur, mot de passe et port selon le format `utilisateur:motdepasse@stun:hôte:port` ou `utilisateur:motdepasse@turn:hôte:port`.", + "WebRTC_call_ended_message": " Appel terminé à __endTime__ - Durée __callDuration__", + "WebRTC_call_declined_message": " Appel refusé par le contact.", "Website": "Site Internet", "Wednesday": "Mercredi", "Weekly_Active_Users": "Utilisateurs actifs chaque semaine", @@ -4709,10 +4754,10 @@ "Your_new_email_is_email": "Votre nouvelle adresse mail est [email].", "Your_password_is_wrong": "Votre mot de passe est incorrect !", "Your_password_was_changed_by_an_admin": "Votre mot de passe a été changé par un administrateur.", - "Your_push_was_sent_to_s_devices": "Votre notification a été envoyée à %s appareils", + "Your_push_was_sent_to_s_devices": "Votre push a été envoyé à %s appareils", "Your_question": "Votre question", "Your_server_link": "Le lien de votre serveur", "Your_temporary_password_is_password": "Votre mot de passe temporaire est [password].", "Your_TOTP_has_been_reset": "Votre TOTP à deux facteurs a été réinitialisé.", - "Your_workspace_is_ready": "Votre espace de travail est prêt à l'emploi 🎉" + "Your_workspace_is_ready": "Votre espace de travail est prêt à être utilisé 🎉" } \ No newline at end of file diff --git a/packages/rocketchat-i18n/i18n/gl.i18n.json b/packages/rocketchat-i18n/i18n/gl.i18n.json index 6c7048846587..8bcb59556fc3 100644 --- a/packages/rocketchat-i18n/i18n/gl.i18n.json +++ b/packages/rocketchat-i18n/i18n/gl.i18n.json @@ -184,8 +184,8 @@ "Type_your_message": "Escribe a túa mensaxe", "Unpin_Message": "Anular fixación da mensaxe", "User_Info": "Información do usuario", - "Videocall_declined": "Videochamada rexeitada.", "Videocall_enabled": "Videochamada habilitada", + "Videocall_declined": "Videochamada rexeitada.", "view-full-other-user-info": "Ver toda a información do usuario", "You_can_close_this_window_now": "Xa podes pechar esta ventá.", "You_can_use_an_emoji_as_avatar": "Tamén podes usar un emoji como avatar.", diff --git a/packages/rocketchat-i18n/i18n/he.i18n.json b/packages/rocketchat-i18n/i18n/he.i18n.json index c19169fedd62..e81fe86c3a3e 100644 --- a/packages/rocketchat-i18n/i18n/he.i18n.json +++ b/packages/rocketchat-i18n/i18n/he.i18n.json @@ -42,6 +42,7 @@ "Accounts_EmailVerification": "אימות דוא״ל", "Accounts_EmailVerification_Description": "בדוק שיש לך הגדרות SMTP נכונות כדי להשתמש בתכונה זו", "Accounts_Enrollment_Email": "אימייל הרשמה", + "Accounts_Enrollment_Email_Description": "אתה יכול להשתמש [name], [fname], [lname] עבור השם המלא של המשתמש, שם פרטי או שם משפחה, בהתאמה.
      אתה יכול להשתמש [email] עבור הדוא\"ל של המשתמש.", "Accounts_Enrollment_Email_Subject_Default": "ברוכים הבאים ל[Site_name]", "Accounts_Iframe_api_method": "שיטת Api", "Accounts_Iframe_api_url": "Url Api", @@ -118,6 +119,7 @@ "Accounts_ShowFormLogin": "טופס מבוסס צג כניסה", "Accounts_UseDefaultBlockedDomainsList": "השתמש בברירת מחדל רשימת Domains חסימה", "Accounts_UseDNSDomainCheck": "הסימון השתמש דומיין DNS", + "Accounts_UserAddedEmail_Description": "הנך רשאי להשתמש המשתנים הבאים:
      • [name], [fname], [lname] עבור השם המלא של המשתמש, שם פרטי או שם משפחה, בהתאמה.
      • [email] עבור הדוא\"ל של המשתמש.
      • [password] להזין את הסיסמה של המשתמש.
      • [Site_Name] ו [Site_URL] עבור שם היישום וה- URL בהתאמה.
      ", "Accounts_UserAddedEmailSubject_Default": "נוספת ל[Site_Name]", "Activate": "הפעל", "Active": "פָּעִיל", @@ -337,6 +339,7 @@ "Continue": "המשך", "Conversation": "שיחה", "Conversation_closed": "בשיחה סגורה: __comment__.", + "Conversation_finished": "שיחת סיים", "Conversation_finished_message": "הודעת סיום שיחה", "conversation_with_s": "השיחה עם s%", "Convert_Ascii_Emojis": "המרת ASCII לאימוג׳י", @@ -459,6 +462,7 @@ "Email_already_exists": "כתובת הדוא״ל כבר קיימת", "Email_body": "גוף הדוא\"ל", "Email_Change_Disabled": "מנהל ה-Rocket.Chat שלך ביטל את שינוי הדוא\"ל", + "Email_Footer_Description": "הנך רשאי להשתמש המשתנים הבאים:
      • [Site_Name] ו [Site_URL] עבור שם היישום וה- URL בהתאמה.
      ", "Email_from": "מאת", "Email_Notification_Mode": "הודעות דוא\"ל כשמשתמשים לא מקוונים", "Email_Notification_Mode_All": "כל אזכור / הודעה פרטית", @@ -656,7 +660,7 @@ "hours": "שעות", "Hours": "שעות", "How_friendly_was_the_chat_agent": "איך ידידותי היה סוכן הצ'אט?", - "How_knowledgeable_was_the_chat_agent": "איך ידע היה סוכן הצ'אט?", + "How_knowledgeable_was_the_chat_agent": "כמה ידע היה לסוכן הצ'אט?", "How_responsive_was_the_chat_agent": "מידת ההיענות היה סוכן צ'אט?", "How_satisfied_were_you_with_this_chat": "איך הייתם מרוצה הצ'אט הזה?", "if_they_are_from": "(אם הם מ%s)", @@ -725,6 +729,7 @@ "Invisible": "בלתי נראה", "Invitation": "הזמנה", "Invitation_HTML": "תבנית HTML להזמנה", + "Invitation_HTML_Default": "

      הוזמנת אל

      [Site_Name]

      עבור אל [Site_URL] ולנסות פתרון הצ'אט פתוח המקור הטוב ביותר הזמינים כיום!

      ", "Invitation_Subject": "נושא ההזמנה", "Invitation_Subject_Default": "הוזמנת ל[Site_Name]", "Invite_user_to_join_channel": "הזמן משתמש להצטרף לחדר", @@ -867,6 +872,7 @@ "Me": "אני", "Media": "מדיה", "Medium": "בינוני", + "Members": "חברים", "Members_List": "רשימת חברים", "mention-all": "תייג הכל", "mention-all_description": "הרשאה להשתמש בתיוג @all", @@ -998,7 +1004,7 @@ "Nothing": "שום דבר", "Nothing_found": "אין תוצאות", "Notification_Desktop_Default_For": "הצג נוטיפיקציות דסקטופ", - "Notifications": "התראות", + "Notifications": "התרעות", "Notifications_Max_Room_Members": "מקסימום חברים בRoom שלאחרם מבטלים את כל הנוטיפיקציות של ההודעות", "Notify_active_in_this_room": "להודיע לכל המחוברים שבחדר", "Notify_all_in_this_room": "להודיע לכל מי שבחדר", @@ -1060,7 +1066,7 @@ "Placeholder_for_password_login_field": "שומר מקום בשדה Login סיסמא", "Please_add_a_comment": "נא להוסיף הערה", "Please_add_a_comment_to_close_the_room": "אנא, הוסף תגובה כדי לסגור את החדר", - "Please_answer_survey": "קדש דק כדי לענות על סקר קצר על הצ'אט הזה", + "Please_answer_survey": "אנא הקדש מספר דקות כדי לענות על סקר קטן בנוגע לשיחה זו", "Please_enter_usernames": "הכנס רשימת משתמשים", "Please_enter_value_for_url": "הכנס ערך עבור הכתובת של תמונת הפרופיל שלך.", "Please_enter_your_new_password_below": "נא להזין את הסיסמה החדשה שלך למטה:", @@ -1198,7 +1204,7 @@ "Select_a_department": "בחירת מחלקה", "Select_an_avatar": "בחירת תמונה", "Select_department": "בחירת מחלקה", - "Select_file": "בחר קובץ", + "Select_file": "בחירת קובץ", "Select_role": "בחר תפקיד", "Select_service_to_login": "יש לבחור בשירות להתחבר דרכו לטעינת התמונה שלך או להעלות אחת ישירות מהמחשב שלך", "Select_user": "בחירת משתמש", @@ -1233,6 +1239,7 @@ "Show_only_online": "הצג רק מחוברים", "Show_preregistration_form": "צג טופס הרשמה מראש", "Showing_archived_results": "

      מציג %s תוצאות בארכיון

      ", + "Showing_online_users": null, "Showing_results": "

      מוצגות %s תוצאות

      ", "since_creation": "מאז %s", "Site_Name": "שם האתר", @@ -1469,6 +1476,7 @@ "Users": "משתמשים", "Users_in_role": "משתמשים בתפקיד", "UTF8_Names_Slugify": "UTF8 שמות Slugify", + "Videocall_enabled": "שיחות וידאו מאופשרות", "Verification_email_sent": "נשלחה הודעת דוא״ל לאימות", "Verified": "מְאוּמָת", "Version": "גרסה", @@ -1476,7 +1484,6 @@ "Video_Chat_Window": "צ'אט וידאו", "Video_Conference": "ועידת וידאו", "Videocall_declined": "שיחות וידאו נדחתה.", - "Videocall_enabled": "שיחות וידאו מאופשרות", "View_All": "הצגת הכול", "View_Logs": "יומנים", "View_mode": "מצב תצוגה", @@ -1552,4 +1559,4 @@ "Your_password_is_wrong": "הסיסמה שלך שגויה!", "Your_push_was_sent_to_s_devices": "הודעת ה-push נשלח בהצלחה ל-%s מכשירים", "Your_question": "השאלה שלך" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/hi-IN.i18n.json b/packages/rocketchat-i18n/i18n/hi-IN.i18n.json index a1aaae598c1a..e7c19485f7ba 100644 --- a/packages/rocketchat-i18n/i18n/hi-IN.i18n.json +++ b/packages/rocketchat-i18n/i18n/hi-IN.i18n.json @@ -209,6 +209,7 @@ "Type_your_message": "अपना संदेश टाइप करें", "Type_your_name": "अपना नाम लिखें", "Upload_file_question": "दस्तावेज अपलोड करें?", + "User_left": "उपयोगकर्ता छोड़ दिया", "We_are_offline_Sorry_for_the_inconvenience": "हम ऑफ़लाइन हैं। असुविधा के लिए खेद है।", "Yes": "हाँ", "You": "आप" diff --git a/packages/rocketchat-i18n/i18n/hr.i18n.json b/packages/rocketchat-i18n/i18n/hr.i18n.json index b99a18d09964..b11a1485c666 100644 --- a/packages/rocketchat-i18n/i18n/hr.i18n.json +++ b/packages/rocketchat-i18n/i18n/hr.i18n.json @@ -638,6 +638,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Neprekidne obavijesti o zvuku za novu sobu za livechat", "Conversation": "Razgovor", "Conversation_closed": "Razgovor je zatvoren: __comment__.", + "Conversation_finished": "Razgovor je završio", "Conversation_finished_message": "Poruka za završetak razgovora", "conversation_with_s": "razgovor s %s", "Conversations": "Razgovori", @@ -1100,6 +1101,7 @@ "Editing_room": "Uređivanje sobe", "Editing_user": "Uređivanje korisnika", "Education": "Obrazovanje", + "Email": "Email", "Email_address_to_send_offline_messages": "Adresa e-pošte za slanje offline poruka", "Email_already_exists": "Email već postoji", "Email_body": "Tijelo emaila", @@ -1818,6 +1820,7 @@ "Max_length_is": "Maksimalna dužina je %s", "Media": "media", "Medium": "Srednji", + "Members": "Članovi", "Members_List": "Lista Članova", "mention-all": "Spominjati sve", "mention-all_description": "Dopuštenje za korištenje @all spomen", @@ -2815,6 +2818,7 @@ "Users_added": "Korisnici su dodani", "Users_in_role": "Korisnici u ulozi", "UTF8_Names_Slugify": "UTF8 Imena Slugify", + "Videocall_enabled": "Videopoziv omogućen", "Validate_email_address": "Validiraj email adresu", "Verification": "Verifikacija", "Verification_Description": "Možete upotrebljavati sljedeća rezervirana mjesta:
      • [Verification_Url] za URL za potvrdu.
      • [ime], [fname], [lname] za puni naziv, ime ili prezime korisnika.
      • [e-pošta] za e-poštu korisnika.
      • [Site_Name] i [Site_URL] za naziv aplikacije i URL.
      ", @@ -2829,7 +2833,6 @@ "Video_Conference": "Video Konferencija", "Video_message": "Video poruka", "Videocall_declined": "Videopoziv odbijen", - "Videocall_enabled": "Videopoziv omogućen", "View_All": "Prikaži Sve", "View_Logs": "Pogledaj izvještaje", "View_mode": "Pregled", diff --git a/packages/rocketchat-i18n/i18n/hu.i18n.json b/packages/rocketchat-i18n/i18n/hu.i18n.json index fc96ff018f39..418a550eefaf 100644 --- a/packages/rocketchat-i18n/i18n/hu.i18n.json +++ b/packages/rocketchat-i18n/i18n/hu.i18n.json @@ -1,10 +1,12 @@ { "403": "Tiltott", - "500": "Belső kiszolgálóhiba", + "500": "Belső szerver hiba", "__count__empty_rooms_will_be_removed_automatically": "__count__ üres szobát automatikusan eltávolítunk.", "__count__empty_rooms_will_be_removed_automatically__rooms__": "__count__ üres szobát automatikusan eltávolítunk:
      __rooms__.", "__username__is_no_longer__role__defined_by__user_by_": "__user_by__ elvette __username__ __role__ jogosultságát", "__username__was_set__role__by__user_by_": "__user_by__ beállította __username__ __role__ jogosultságát", + "This_room_encryption_has_been_enabled_by__username_": "Ennek a szobának a titkosítását __username__ engedélyezte", + "This_room_encryption_has_been_disabled_by__username_": "Ennek a szobának a titkosítását __username__ letiltotta", "@username": "@felhasználónév", "@username_message": "@felhasználónév ", "#channel": "#csatorna", @@ -28,6 +30,7 @@ "access-permissions": "Hozzáférés a jogosultságok képernyőhöz", "access-permissions_description": "Különböző szerepek jogosultságainak módosítása.", "access-setting-permissions": "Beállításalapú jogosultságok módosítása", + "access-setting-permissions_description": "A beállításalapú engedélyek módosításának engedélyezése", "Accessing_permissions": "Hozzáférési jogosultságok", "Account_SID": "Fiók SID", "Accounts": "Fiókok", @@ -39,6 +42,7 @@ "Accounts_AllowDeleteOwnAccount": "Lehetővé tétel a felhasználóknak a saját fiókjuk törléséhez", "Accounts_AllowedDomainsList": "Engedélyezett tartományok listája", "Accounts_AllowedDomainsList_Description": "Engedélyezett tartományok vesszővel elválasztott listája", + "Accounts_AllowInvisibleStatusOption": "Láthatatlan állapot opció engedélyezése", "Accounts_AllowEmailChange": "E-mail-cím megváltoztatásának engedélyezése", "Accounts_AllowEmailNotifications": "E-mail értesítések engedélyezése", "Accounts_AllowPasswordChange": "Jelszó megváltoztatásának engedélyezése", @@ -96,18 +100,24 @@ "Accounts_OAuth_Custom_Button_Label_Color": "Gomb szövegének színe", "Accounts_OAuth_Custom_Button_Label_Text": "Gomb szövege", "Accounts_OAuth_Custom_Channel_Admin": "Felhasználói adatcsoport térkép", + "Accounts_OAuth_Custom_Channel_Map": "OAuth csoport Channel térkép", "Accounts_OAuth_Custom_Email_Field": "E-mail mező", "Accounts_OAuth_Custom_Enable": "Engedélyezés", + "Accounts_OAuth_Custom_Groups_Claim": "Szerepkörök/csoportok mező a csatornák leképezéséhez", "Accounts_OAuth_Custom_id": "Azonosító", "Accounts_OAuth_Custom_Identity_Path": "Személyazonosság útvonala", "Accounts_OAuth_Custom_Identity_Token_Sent_Via": "Személyazonossági token elküldve ezzel:", + "Accounts_OAuth_Custom_Key_Field": "Kulcsmező", "Accounts_OAuth_Custom_Login_Style": "Bejelentkezés stílusa", + "Accounts_OAuth_Custom_Map_Channels": "Szerepkörök/csoportok hozzárendelése csatornákhoz", "Accounts_OAuth_Custom_Merge_Roles": "Szerepek egyesítése SSO-ból", "Accounts_OAuth_Custom_Merge_Users": "Felhasználók egyesítése", "Accounts_OAuth_Custom_Name_Field": "Név mező", "Accounts_OAuth_Custom_Roles_Claim": "Szerepek vagy csoportok mezőjének neve", + "Accounts_OAuth_Custom_Roles_To_Sync": "Szinkronizálandó szerepkörök", + "Accounts_OAuth_Custom_Roles_To_Sync_Description": "A felhasználó bejelentkezésekor és létrehozásakor szinkronizálandó OAuth-szerepkörök (vesszővel elválasztva).", "Accounts_OAuth_Custom_Scope": "Hatókör", - "Accounts_OAuth_Custom_Secret": "Titok", + "Accounts_OAuth_Custom_Secret": "Titkos kulcs", "Accounts_OAuth_Custom_Show_Button_On_Login_Page": "Gomb megjelenítése a bejelentkezési oldalon", "Accounts_OAuth_Custom_Token_Path": "Token útvonala", "Accounts_OAuth_Custom_Token_Sent_Via": "Token elküldve ezzel:", @@ -137,7 +147,7 @@ "Accounts_OAuth_Google": "Google bejelentkezés", "Accounts_OAuth_Google_callback_url": "Google visszahívási URL", "Accounts_OAuth_Google_id": "Google azonosító", - "Accounts_OAuth_Google_secret": "Google titok", + "Accounts_OAuth_Google_secret": "Google titkos kulcs", "Accounts_OAuth_Linkedin": "LinkedIn bejelentkezés", "Accounts_OAuth_Linkedin_callback_url": "LinkedIn visszahívási URL", "Accounts_OAuth_Linkedin_id": "LinkedIn azonosító", @@ -195,6 +205,8 @@ "Accounts_Registration_AuthenticationServices_Default_Roles": "Hitelesítési szolgáltatások alapértelmezett szerepei", "Accounts_Registration_AuthenticationServices_Default_Roles_Description": "Alapértelmezett szerepek (vesszővel elválasztva), amelyeket a felhasználók akkor kapnak meg, ha hitelesítési szolgáltatásokon keresztül regisztrálnak", "Accounts_Registration_AuthenticationServices_Enabled": "Regisztráció hitelesítési szolgáltatásokkal", + "Accounts_Registration_Users_Default_Roles": "Alapértelmezett szerepkörök a felhasználók számára", + "Accounts_Registration_Users_Default_Roles_Enabled": "Alapértelmezett szerepkörök engedélyezése a kézi regisztrációhoz", "Accounts_Registration_InviteUrlType": "Meghívó URL típusa", "Accounts_Registration_InviteUrlType_Direct": "Közvetlen", "Accounts_Registration_InviteUrlType_Proxy": "Proxy", @@ -212,11 +224,15 @@ "Accounts_SearchFields": "A keresés során figyelembe vett mezők", "Accounts_Send_Email_When_Activating": "E-mail küldése a felhasználónak, ha a felhasználó aktiválva lett", "Accounts_Send_Email_When_Deactivating": "E-mail küldése a felhasználónak, ha a felhasználó inaktiválva lett", + "Accounts_Set_Email_Of_External_Accounts_as_Verified": "Külső fiókok e-mail címének beállítása ellenőrzöttként", "Accounts_SetDefaultAvatar": "Alapértelmezett profilkép beállítása", "Accounts_SetDefaultAvatar_Description": "Megpróbálja meghatározni az alapértelmezett profilképet az OAuth fiók vagy a Gravatar alapján", "Accounts_ShowFormLogin": "Alapértelmezett bejelentkezési űrlap megjelenítése", + "Accounts_TwoFactorAuthentication_By_TOTP_Enabled": "Kétfaktoros hitelesítés engedélyezése TOTP-n keresztül", + "Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In": "Új felhasználók automatikus bejelentkezése a kétfaktoros rendszerhez e-mailben", "Accounts_TwoFactorAuthentication_By_Email_Enabled": "Engedélyezze a kétfaktoros hitelesítést e-mailben", "Accounts_TwoFactorAuthentication_Enabled": "Kétlépcsős hitelesítés engedélyezése", + "Accounts_TwoFactorAuthentication_Enforce_Password_Fallback": "Jelszó-visszavonás kényszerítése", "Accounts_TwoFactorAuthentication_MaxDelta": "Legnagyobb delta", "Accounts_TwoFactorAuthentication_MaxDelta_Description": "A legnagyobb delta határozza meg, hogy hány token érvényes bármely adott időpontban. A tokeneket 30 másodpercenként állítják elő, és (30 × legnagyobb delta) másodpercig érvényesek.
      Példa: Ha a legnagyobb delta 10-re van állítva, akkor minden token legfeljebb 300 másodpercig használható az időbélyegük előtt vagy után. Ez akkor hasznos, ha az ügyfél órája nincs megfelelően szinkronizálva a kiszolgálóéval.", "Accounts_UseDefaultBlockedDomainsList": "Alapértelmezetten blokkolt tartományok listájának használata", @@ -224,6 +240,7 @@ "Accounts_UserAddedEmail_Default": "

      Üdvözli a(z) [Site_Name]

      Menjen a(z) [Site_URL] címre, és még ma próbálja ki az elérhető legjobb nyílt forráskódú csevegőmegoldást!

      Bejelentkezhet az e-mail-címe ([email]) és jelszava ([password]) használatával. Arra kérhetik, hogy változtassa meg az első bejelentkezés után.", "Accounts_UserAddedEmail_Description": "A következő helykitöltőket használhatja:

      • [name] a felhasználó teljes nevéhez, [lname] a felhasználó vezetéknevéhez és [fname] a felhasználó keresztnevéhez.
      • [email] a felhasználó e-mail-címéhez.
      • [password] a felhasználó jelszavához.
      • [Site_Name] az alkalmazás nevéhez és [Site_URL] az alkalmazás URL-jéhez.
      ", "Accounts_UserAddedEmailSubject_Default": "Hozzá lett adva a(z) [Site_Name] alkalmazáshoz", + "Accounts_Verify_Email_For_External_Accounts": "E-mail ellenőrzése külső fiókok esetén", "Action_required": "Beavatkozás szükséges", "Activate": "Aktiválás", "Active": "Aktív", @@ -241,7 +258,9 @@ "Add_user": "Felhasználó hozzáadása", "Add_User": "Felhasználó hozzáadása", "Add_users": "Felhasználók hozzáadása", + "Add_members": "Tagok hozzáadása", "add-livechat-department-agents": "Livechat ügyintézők hozzáadása a részlegekhez", + "add-livechat-department-agents_description": "Engedély az omnichannel ügynökök hozzáadására az osztályokhoz", "add-oauth-service": "Oauth szolgáltatás hozzáadása", "add-oauth-service_description": "Jogosultság új Oauth szolgáltatás hozzáadásához", "add-user": "Felhasználó hozzáadása", @@ -264,10 +283,13 @@ "Administration": "Adminisztráció", "Adult_images_are_not_allowed": "Felnőtt tartalmú képek nem engedélyezettek", "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "Az OAuth2 hitelesítés után a felhasználók át lesznek irányítva egy ezen a listán lévő URL-re. Soronként egy URL-t adhat hozzá.", - "Agent": "Ügyintéző", - "Agent_added": "Ügyintéző hozzáadva", + "Agent": "Ügynök", + "Agent_added": "Ügynök hozzáadva", "Agent_Info": "Ügyintéző információk", + "Agent_messages": "Ügynök üzenetek", + "Agent_Name_Placeholder": "Kérjük, adja meg az ügynök nevét...", "Agent_removed": "Ügyintéző eltávolítva", + "Agent_deactivated": "Az ügynököt deaktiválták", "Agents": "Ügyintézők", "Alerts": "Riasztások", "Alias": "Álnév", @@ -288,10 +310,13 @@ "Allow_Invalid_SelfSigned_Certs": "Érvénytelen saját aláírású tanúsítványok engedélyezése", "Allow_Invalid_SelfSigned_Certs_Description": "Érvénytelen és saját aláírású SSL-tanúsítvány engedélyezése a hivatkozás-ellenőrzéshez és az előnézetekhez.", "Allow_Marketing_Emails": "Marketing e-mailek engedélyezése", + "Allow_Online_Agents_Outside_Business_Hours": "Online ügynökök engedélyezése nyitvatartási időn kívül", "Allow_Online_Agents_Outside_Office_Hours": "Munkaidőn kívül elérhető ügyintézők engedélyezése", + "Allow_Save_Media_to_Gallery": "Média mentésének engedélyezése a galériába", "Allow_switching_departments": "Lehetővé tétel a látogató számára, hogy részleget váltson", "Almost_done": "Majdnem kész", "Alphabetical": "Ábécé sorrend", + "Also_send_to_channel": "Szintén küldje el a csatornára", "Always_open_in_new_window": "Megnyitás mindig új ablakban", "Analytics": "Analitika", "Analytics_features_enabled": "Funkciók engedélyezve", @@ -348,6 +373,7 @@ "API_Personal_Access_Tokens_Regenerate_Modal": "Ha elvesztette vagy elfelejtette a tokenjét, akkor újra előállíthatja azt, de ne feledje, hogy a tokent használó összes alkalmazást frissíteni kell", "API_Personal_Access_Tokens_Remove_Modal": "Biztosan el szeretné eltávolítani ezt a személyes hozzáférési tokent?", "API_Personal_Access_Tokens_To_REST_API": "Személyes hozzáférési tokenek a REST API-hoz", + "API_Rate_Limiter": "API sebességkorlátozó", "API_Shield_Types": "Pajzstípusok", "API_Shield_Types_Description": "Az engedélyezendő pajzsok típusai vesszővel elválasztott listaként, válasszon „online”, „channel” vagy „*” (összes) közül", "API_Token": "API token", @@ -358,6 +384,7 @@ "API_User_Limit": "Felhasználókorlát a csatornához történő összes felhasználó hozzáadásánál", "API_Wordpress_URL": "WordPress URL", "api-bypass-rate-limit": "REST API gyakoriságkorlát megkerülése", + "api-bypass-rate-limit_description": "Engedély az API hívására sebességkorlátozás nélkül", "Apiai_Key": "Api.ai kulcs", "Apiai_Language": "Api.ai nyelv", "APIs": "API-k", @@ -381,17 +408,33 @@ "App_user_not_allowed_to_login": "Az alkalmazás felhasználóinak nem engedélyezett a közvetlen bejelentkezés.", "Appearance": "Megjelenés", "Application_added": "Alkalmazás hozzáadva", + "Application_delete_warning": "Ezt az alkalmazást nem fogod tudni visszaállítani!", "Application_Name": "Alkalmazás neve", "Application_updated": "Alkalmazás frissítve", "Apply": "Alkalmaz", "Apply_and_refresh_all_clients": "Alkalmaz és minden kliens frissítése", "Apps": "Alkalmazások", "Apps_Engine_Version": "Alkalmazás motorjának verziója", + "Apps_Essential_Alert": "Ez az alkalmazás elengedhetetlen a következő eseményekhez:", "Apps_Framework_Development_Mode": "Fejlesztői mód engedélyezése", "Apps_Framework_Development_Mode_Description": "A fejlesztői mód lehetővé teszi olyan alkalmazások telepítését, amelyek nem a Rocket.Chat alkalmazásboltjából származnak.", "Apps_Framework_enabled": "Az alkalmazás-keretrendszer engedélyezése", + "Apps_Framework_Source_Package_Storage_Type": "Alkalmazás forráscsomagjának tárolási típusa", + "Apps_Framework_Source_Package_Storage_FileSystem_Path": "Az alkalmazások forráscsomagjának tárolási könyvtára", "Apps_Game_Center": "Játék központ", "Apps_Game_Center_Back": "Vissza a játék központba", + "Apps_Game_Center_Invite_Friends": "Hívja meg barátait, hogy csatlakozzanak", + "Apps_Game_Center_Play_Game_Together": "@here Játsszunk együtt __name__!", + "Apps_Interface_IPreRoomCreatePrevent": "A szoba létrehozása előtt bekövetkező esemény", + "Apps_Interface_IPreRoomDeletePrevent": "A szoba törlése előtt bekövetkező esemény", + "Apps_License_Message_appId": "Ehhez az alkalmazáshoz nem adtak ki licenszet", + "Apps_License_Message_expire": "Az licensz már nem érvényes, meg kell újítani", + "Apps_License_Message_renewal": "Az licensz lejárt és meg kell újítani", + "Apps_Logs_TTL": "Az alkalmazások naplóinak tárolási napjainak száma", + "Apps_Logs_TTL_7days": "7 nap", + "Apps_Logs_TTL_14days": "14 nap", + "Apps_Logs_TTL_30days": "30 nap", + "Apps_Logs_TTL_Alert": "A Naplók méretétől függően a beállítás megváltoztatása néhány pillanatig lassúságot okozhat", "Apps_Marketplace_Deactivate_App_Prompt": "Valóban le szeretné tiltani ezt az alkalmazást?", "Apps_Marketplace_Login_Required_Description": "A Rocket.Chat alkalmazásboltból történő alkalmazások vásárlásához a munkaterület regisztrálása és bejelentkezés szükséges.", "Apps_Marketplace_Login_Required_Title": "Alkalmazásbolt bejelentkezés szükséges", @@ -400,12 +443,40 @@ "Apps_Marketplace_pricingPlan_monthly_perUser": "__price__ / hónap felhasználónként", "Apps_Marketplace_pricingPlan_startingAt_monthly": "__price__ / hónaptól", "Apps_Marketplace_pricingPlan_startingAt_monthly_perUser": "felhasználónként __price__ / hónaptól", + "Apps_Marketplace_pricingPlan_startingAt_yearly": "__price__ / évtől kezdődően", + "Apps_Marketplace_pricingPlan_startingAt_yearly_perUser": "Felhasználónként __price__ / évtől kezdődően", "Apps_Marketplace_pricingPlan_yearly": "__price__ / év", "Apps_Marketplace_pricingPlan_yearly_perUser": "__price__ / év felhasználónként", "Apps_Marketplace_Uninstall_App_Prompt": "Valóban el akarja távolítani ezt az alkalmazást?", "Apps_Marketplace_Uninstall_Subscribed_App_Anyway": "Eltávolítás mindenképp", "Apps_Marketplace_Uninstall_Subscribed_App_Prompt": "Ennek az alkalmazásnak aktív előfizetése van, és az eltávolítása nem fogja megszakítani azt. Ha ezt szeretné tenni, akkor módosítsa az előfizetését az eltávolítás előtt.", + "Apps_Permissions_Review_Modal_Title": "Szükséges engedélyek", + "Apps_Permissions_Review_Modal_Subtitle": "Ez az alkalmazás a következő engedélyekhez szeretne hozzáférni. Egyetértesz?", + "Apps_Permissions_No_Permissions_Required": "Az alkalmazás nem igényel további engedélyeket", + "Apps_Permissions_cloud_workspace-token": "Interakció a felhőszolgáltatásokkal a kiszolgáló nevében", + "Apps_Permissions_user_read": "Hozzáférés a felhasználói adatokhoz", + "Apps_Permissions_user_write": "Felhasználói adatok módosítása", + "Apps_Permissions_upload_read": "A szerverre feltöltött fájlok elérése", + "Apps_Permissions_upload_write": "Fájlok feltöltése a szerverre", + "Apps_Permissions_server-setting_read": "Hozzáférési beállítások a szerveren", + "Apps_Permissions_server-setting_write": "Beállítások módosítása a szerveren", + "Apps_Permissions_room_read": "Hozzáférés a szoba információihoz", + "Apps_Permissions_room_write": "Szobák létrehozása és módosítása", + "Apps_Permissions_message_read": "Hozzáférési üzenetek", + "Apps_Permissions_message_write": "Üzenetek küldése és módosítása", + "Apps_Permissions_livechat-status_read": "Hozzáférés a Livechat státusz információkhoz", + "Apps_Permissions_livechat-visitor_read": "Hozzáférés a Livechat látogatói információkhoz", + "Apps_Permissions_livechat-department_write": "Livechat részleg információk módosítása", + "Apps_Permissions_slashcommand": "Új slash parancsok regisztrálása", + "Apps_Permissions_api": "Új HTTP végpontok regisztrálása", + "Apps_Permissions_env_read": "Hozzáférés minimális információkhoz a kiszolgáló környezetéről", + "Apps_Permissions_networking": "Hozzáférés ehhez a szerverhálózathoz", + "Apps_Permissions_persistence": "Belső adatok tárolása az adatbázisban", + "Apps_Permissions_scheduler": "Ütemezett munkák nyilvántartása és karbantartása", + "Apps_Permissions_ui_interact": "Interakció a felhasználói felülettel", "Apps_Settings": "Alkalmazás beállításai", + "Apps_Manual_Update_Modal_Title": "Ez az alkalmazás már telepítve van", + "Apps_Manual_Update_Modal_Body": "Szeretné frissíteni?", "Apps_User_Already_Exists": "A(z) „__username__” felhasználónév már használatban van. Az alkalmazás telepítéshez nevezze át vagy távolítsa el az ezt használó felhasználót", "Apps_WhatIsIt": "Alkalmazások: mik ezek?", "Apps_WhatIsIt_paragraph1": "Új ikon az adminisztrációs területen! Mit jelent ez és mik azok az alkalmazások?", @@ -417,6 +488,7 @@ "archive-room_description": "Jogosultság egy csatorna archiválásához", "are_typing": "gépel", "Are_you_sure": "Biztos benne?", + "Are_you_sure_you_want_to_close_this_chat": "Biztos, hogy be akarod zárni ezt a csevegést?", "Are_you_sure_you_want_to_delete_this_record": "Biztosan törölni szeretné ezt a rekordot?", "Are_you_sure_you_want_to_delete_your_account": "Biztosan törölni szeretné a fiókját?", "Are_you_sure_you_want_to_disable_Facebook_integration": "Biztosan le szeretné tiltani a Facebook integrációt?", @@ -502,13 +574,25 @@ "Back_to_Manage_Apps": "Vissza az alkalmazások kezeléséhez", "Back_to_permissions": "Vissza a jogosultságokhoz", "Back_to_room": "Vissza szobához", + "Back_to_threads": "Vissza a témákhoz", "Backup_codes": "Kódok biztonsági mentése", "ban-user": "Felhasználó kitiltása", "ban-user_description": "Jogosultság egy felhasználó kitiltásához egy csatornáról", + "BBB_End_Meeting": "Megbeszélés vége", + "BBB_Enable_Teams": "Engedélyezés a csapatok számára", + "BBB_Join_Meeting": "Csatlakozz a megbeszéléshez", + "BBB_Start_Meeting": "Megbeszélés kezdése", + "BBB_Video_Call": "BBB videóhívás", + "BBB_You_have_no_permission_to_start_a_call": "Nincs engedélye hívás indítására", + "Belongs_To": "Tartozik", "Best_first_response_time": "Legjobb első válasz idő", "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "Béta funkció. Attól függ, hogy a videokonferencia engedélyezve van-e.", "Better": "Jobb", + "Bio": "Adatlap", + "Bio_Placeholder": "Adatlap helyfoglaló", + "Block_Multiple_Failed_Logins_Enabled": "Bejelentkezési adatok gyűjtésének engedélyezése", "Block_Multiple_Failed_Logins_Ip_Whitelist": "IP fehérlista", + "Block_Multiple_Failed_Logins_Notify_Failed": "Értesítés a sikertelen bejelentkezési kísérletekről", "Block_User": "Felhasználó blokkolása", "Blockchain": "Blokklánc", "Blockstack_Auth_Description": "Hitelesítés leírása", @@ -549,22 +633,34 @@ "by": "–", "cache_cleared": "Gyorsítótár törölve", "Call": "Hívás", + "Call_declined": "Hívás elutasítva!", + "Call_provider": "Hívás Szolgáltató", + "Call_Already_Ended": "A hívás már befejeződött", "call-management": "Híváskezelés", + "call-management_description": "Engedély a megbeszélés megkezdésére", "Caller": "Hívó", "Cancel": "Mégse", "Cancel_message_input": "Mégse", "Canceled": "Megszakítva", + "Canned_Response_Created": "Válasz sablon létrehozva", + "Canned_Response_Updated": "Válasz sablon frissítve", + "Canned_Response_Delete_Warning": "A válasz sablon törlése nem vonható vissza.", "Canned_Response_Removed": "Válasz sablon eltávolítva", + "Canned_Response_Sharing_Public_Description": "Ehhez a válasz sablonhoz bárki hozzáférhet", "Canned_Responses": "Válasz sablonok", "Canned_Responses_Enable": "Válasz sablonok engedélyezése", + "Create_your_First_Canned_Response": "Az első válasz sablon létrehozása", "Cannot_invite_users_to_direct_rooms": "Nem hívhat meg felhasználókat a közvetlen szobákba", "Cannot_open_conversation_with_yourself": "Nem küldhet közvetlen üzenetet önmagának", + "Cannot_share_your_location": "Nem oszthatja meg a tartózkodási helyét...", "CAS_autoclose": "Bejelentkezési felugró ablak automatikus bezárása", "CAS_base_url": "SSO alap URL", "CAS_base_url_Description": "A külső SSO szolgáltatás alap URL-je, például: https://sso.example.undef/sso/", "CAS_button_color": "Bejelentkezés gomb háttérszíne", "CAS_button_label_color": "Bejelentkezés gomb szövegszíne", "CAS_button_label_text": "Bejelentkezés gomb felirata", + "CAS_Creation_User_Enabled": "Felhasználó létrehozásának engedélyezése", + "CAS_Creation_User_Enabled_Description": "CAS-felhasználó létrehozásának engedélyezése a CAS-jegy által megadott adatokból.", "CAS_enabled": "Engedélyezve", "CAS_Login_Layout": "CAS bejelentkezés elrendezése", "CAS_login_url": "SSO bejelentkezési URL", @@ -574,7 +670,8 @@ "CAS_Sync_User_Data_Enabled": "Mindig szinkronizálja a felhasználói adatokat", "CAS_Sync_User_Data_Enabled_Description": "Mindig szinkronizálja a külső CAS felhasználói adatokat a rendelkezésre álló attribútumokkal bejelentkezéskor. Megjegyzés: Az attribútumok mindig szinkronizálódnak a fiók létrehozásakor.", "CAS_Sync_User_Data_FieldMap": "Tulajdonság térkép", - "CAS_Sync_User_Data_FieldMap_Description": "Használja ezt a JSON bemenetet belső attribútumok (kulcs) külső attribútumok (érték) létrehozására. A (z) \"%\" verziójú külső attribútumnevek interpolálják az érték-karakterláncokat.
      Példa, \"{email\": \"% email%\", \"név\": \"% firstname%,% utónév%\"} `

      Az attribútum térkép mindig interpolált. A CAS 1.0-ban csak a `username` attribútum áll rendelkezésre. Az elérhető belső jellemzők: felhasználónév, név, e-mail, szobák; szobák a vesszõvel elválasztott szobák listája, amelyek a felhasználók létrehozásához csatlakoznak, például: {\"rooms\": \"% team%,% department%\"} csatlakozhat a CAS felhasználókhoz a létrehozásukhoz csapatuk és osztályuk csatornájához.", + "CAS_Sync_User_Data_FieldMap_Description": "Használja ezt a JSON bemenetet belső attribútumok (kulcs) külső attribútumok (érték) létrehozására. A (z) \"%\" verziójú külső attribútumnevek interpolálják az érték-karakterláncokat.
      Példa, `{\"email\":\"%email%\", \"name\":\"%firstname%, %lastname%\"}`

      Az attribútum térkép mindig interpolált. A CAS 1.0-ban csak a `username` attribútum áll rendelkezésre. Az elérhető belső jellemzők: felhasználónév, név, e-mail, szobák; szobák a vesszővel elválasztott szobák listája, amelyek a felhasználók létrehozásához csatlakoznak, például: {\"rooms\": \"%team%,%department%\"} csatlakozhat a CAS felhasználókhoz a létrehozásukhoz csapatuk és osztályuk csatornájához.", + "CAS_trust_username": "Trust CAS felhasználónév", "CAS_version": "CAS verzió", "CAS_version_Description": "Csak CAS által támogatott CAS-verziót használjon.", "Categories": "Kategóriák", @@ -598,13 +695,24 @@ "Channel_to_listen_on": "Csatorna figyelő", "Channel_Unarchived": "A `#%s` nevű csatorna sikeresen visszaállítva", "Channels": "Csatornák", + "Channels_added": "Channel hozzáadva", "Channels_are_where_your_team_communicate": "Csatorna, ahol a csapatod kommunikálhat", "Channels_list": "Nyilvános csatornák listája", + "Chart": "Diagram", "Chat_button": "Chat gomb", + "Chat_close": "Csevegés bezárása", "Chat_closed": "Chat lezárva", "Chat_closed_by_agent": "Chat lezárva az operátor által", "Chat_closed_successfully": "Chat sikeresen lezárult", + "Chat_History": "Csevegés története", "Chat_Now": "Csevegj most", + "Chat_On_Hold": "Várakozó csevegés", + "Chat_On_Hold_Successfully": "Ez a csevegés sikeresen várakoztatásra került", + "Chat_queued": "Csevegés várakoztatva", + "Chat_removed": "Chat eltávolítva", + "Chat_resumed": "Csevegés folytatódik", + "Chat_start": "Csevegés kezdete", + "Chat_started": "Csevegés megkezdve", "Chat_window": "Chat ablak", "Chatops_Enabled": "Chatops engedélyezése", "Chatops_Title": "Chatops felület", @@ -658,11 +766,13 @@ "Chatpal_Welcome": "Élvezze a keresést!", "Chatpal_Window_Size": "Index ablakméret", "Chatpal_Window_Size_Description": "Az index ablakok mérete órában (bootstrapoláskor)", + "Chats_removed": "Csevegések eltávolítva", "Check_All": "Összes kijelölése", "Choose_a_room": "Válasszon egy szobát", "Choose_messages": "Üzenetek kiválasztása", "Choose_the_alias_that_will_appear_before_the_username_in_messages": "Válassza Alias ​​előtt jelenik meg a felhasználónevét üzeneteket.", "Choose_the_username_that_this_integration_will_post_as": "Válassza ki a felhasználónevét, hogy ez az integráció utáni mint.", + "Choose_users": "Válasszon felhasználókat", "Clean_Usernames": "Felhasználó nevek törlése", "clean-channel-history": "Tiszta csatornaelőzmények", "clean-channel-history_description": "Engedély a csatornák történetének törlésére", @@ -677,17 +787,20 @@ "Click_here_to_view_and_copy_your_password": "Kattints ide a jelszó megtekintéséhez és másolásához.", "Click_the_messages_you_would_like_to_send_by_email": "Kattints az üzenetekre, amiket szeretnél e-mailben elküldeni", "Click_to_join": "Kattints a csatlakozáshoz!", + "Click_to_load": "Kattints a betöltéshez", "Client_ID": "ügyfél-azonosító", "Client_Secret": "Client Secret", "Clients_will_refresh_in_a_few_seconds": "Az ügyfelek néhány másodpercen belül frissülnek", "close": "bezárás", "Close": "Bezárás", + "Close_chat": "Csevegés bezárása", "close-livechat-room": "Livechat szoba bezárása", "close-livechat-room_description": "Engedély az aktuális LiveChat csatorna bezárásához", "Close_menu": "Menü bezárása", "close-others-livechat-room": "Livechat szoba bezárása", "close-others-livechat-room_description": "Engedély az egyéb LiveChat csatornák bezárására", "Closed": "Zárva", + "Closed_At": "Lezárva", "Closed_by_visitor": "Látogató által bezárva", "Closing_chat": "Beszélgetés bezáráse", "Cloud": "Felhő", @@ -695,6 +808,7 @@ "Cloud_console": "Felhő konzol", "Cloud_error_code": "Kód: __errorCode__", "Cloud_error_in_authenticating": "Hiba a hitelesítés során:", + "Cloud_Info": "Felhő információ", "Cloud_login_to_cloud": "Belépés a Rocket.Chat Felhőbe", "Cloud_logout": "Kilépés a Rocket.Chat Felhőből", "Cloud_manually_input_token": "Add meg kézzel a tokent, ami a Felhő regisztrációs e-mailben szerepel.", @@ -731,17 +845,22 @@ "Connectivity_Services": "Kapcsolódási szolgáltatások", "Consulting": "Tanácsadó", "Contact": "Kapcsolat", + "Contacts": "Kapcsolat", + "Contact_Name": "Kapcsolattartó neve", "Contains_Security_Fixes": "Biztonsági javításokat tartalmaz", "Content": "Tartalom", "Continue": "Folytatás", "Continuous_sound_notifications_for_new_livechat_room": "Folyamatos hang értesítések az új livechat szobához", "Conversation": "Beszélgetés", "Conversation_closed": "Beszélgetés zárva: __comment__.", + "Conversation_finished": "Beszélgetés befejeződött", "Conversation_finished_message": "Beszélgetés befejezése üzenet", "conversation_with_s": "a beszélgetés %s-val", "Conversations": "Beszélgetések", "Conversations_per_day": "Napi beszélgetések", + "Convert": "Átalakítás", "Convert_Ascii_Emojis": "ASCII hangulatjelek átalakítása", + "Converting_channel_to_a_team": "Ezt a Channel csapattá alakítod át. Minden tag megmarad.", "Copied": "Másolva", "Copy": "Másolás", "Copy_text": "Szöveg másolása", @@ -952,7 +1071,7 @@ "Country_Spain": "Spanyolország", "Country_Sri_Lanka": "Srí Lanka", "Country_Sudan": "Szudán", - "Country_Suriname": "Suriname", + "Country_Suriname": "Vezetéknév", "Country_Svalbard_and_Jan_Mayen": "Svalbard és Jan Mayen", "Country_Swaziland": "Szváziföld", "Country_Sweden": "Svédország", @@ -992,27 +1111,38 @@ "Country_Zimbabwe": "Zimbabwe", "Cozy": "Kényelmes", "Create": "Létrehoz", + "Create_Canned_Response": "Sablon válasz létrehozása", + "Create_channel": "Channel létrehozása", "Create_A_New_Channel": "Új csatorna létrehozása", "Create_new": "Új létrehozása", + "Create_new_members": "Új tagok létrehozása", "Create_unique_rules_for_this_channel": "Hozzon létre egyedi szabályokat ehhez a csatornához", "create-c": "Nyilvános csatornák létrehozása", "create-c_description": "Engedély nyilvános csatornák létrehozására", "create-d": "Közvetlen üzenetek létrehozása", "create-d_description": "Engedély a közvetlen üzenetek indításához", + "create-invite-links": "Meghívó linkek létrehozása", + "create-invite-links_description": "Engedély meghívó linkek létrehozására a csatornákhoz", "create-p": "Privát csatornák létrehozása", "create-p_description": "Privát csatornák létrehozásának engedélye", "create-personal-access-tokens": "Személyi hozzáférési token létrehozása", + "create-personal-access-tokens_description": "Személyes hozzáférési tokenek létrehozásának engedélyezése", "create-user": "Felhasználó létrehozása", "create-user_description": "Engedély a felhasználók létrehozásához", + "Created": "Létrehozva", + "Created_as": "Létrehozva mint", "Created_at": "Készült", "Created_at_s_by_s": "Létrehozva %s, %s által", "Created_at_s_by_s_triggered_by_s": "%s hatására %s által készítve %s időpontban.", + "Created_by": "Létrehozta", "CRM_Integration": "CRM integráció", "CROWD_Allow_Custom_Username": "Egyedi nevek engedélyezése a Rocket.Chat-en", "CROWD_Reject_Unauthorized": "Jogosulatlanok elutasítása", + "Crowd_Remove_Orphaned_Users": "Árva felhasználók eltávolítása", "Crowd_sync_interval_Description": "A szinkronizálás közötti idő. Példa \"24 óránként\" vagy \"a hét első napján\", további példák a [Cron szövegszerkesztőben] (http://bunkat.github.io/later/parsers.html#text)", "Current_Chats": "Jelenlegi beszélgetések", "Current_File": "Aktuális fájl", + "Current_Import_Operation": "Aktuális importálási művelet", "Current_Status": "Jelenlegi állapot", "Custom": "Egyedi", "Custom CSS": "Egyéni CSS", @@ -1029,6 +1159,9 @@ "Custom_Emoji_Info": "Egyedi hangulatjel információ", "Custom_Emoji_Updated_Successfully": "Az egyéni hangulatjel feltöltése sikeres", "Custom_Fields": "Egyedi mezők", + "Custom_Field_Removed": "Egyéni mező eltávolítva", + "Custom_Field_Not_Found": "Egyéni mező nem található", + "Custom_Integration": "Egyedi integráció", "Custom_oauth_helper": "Amikor beállítja OAuth Szolgáltató, akkor tájékoztatni visszahívás URL. Használat
       %s 
      .", "Custom_oauth_unique_name": "Egyedi OAuth egyedi név", "Custom_Script_Logged_In": "Egyedi szkript bejelentkezett felhasználóknak", @@ -1046,12 +1179,14 @@ "Custom_Sound_Info": "Egyedi hang információ", "Custom_Sound_Saved_Successfully": "Az egyedi hang sikeresen mentve", "Custom_Sounds": "Egyedi Hangok", + "Custom_Status": "Egyéni állapot", "Custom_Translations": "Egyedi fordítás", "Custom_Translations_Description": "Valódi JSON legyen, ahol a kulcsok olyan nyelvek, amelyek kulcsszót tartalmaznak és fordítások. Például:
      {\n \"en\": {\n \"Channels\": \"Rooms\"\n },\n\"pt\":{\n \"Channels\": \"Salas\"\n }\n}", "Custom_User_Status": "Egyedi felhasználó állapot", "Custom_User_Status_Add": "Egyedi felhasználó állapot hozzáadása", "Custom_User_Status_Added_Successfully": "Az egyedi felhasználó állapot sikeresen hozzáadva", "Custom_User_Status_Delete_Warning": "Az egyedi felhasználó állapot törlés nem vonható vissza.", + "Custom_User_Status_Edit": "Egyéni felhasználói státusz szerkesztése", "Custom_User_Status_Error_Invalid_User_Status": "Érvénytelen felhasználó állapot", "Custom_User_Status_Error_Name_Already_In_Use": "Az egyedi felhasználó állapot neve már használatban van.", "Custom_User_Status_Has_Been_Deleted": "Az egyedi felhasználó állapot törölve lett", @@ -1059,6 +1194,7 @@ "Custom_User_Status_Updated_Successfully": "Az egyedi felhasználó állapot sikeresen frissítve", "Customize": "Testreszabás", "CustomSoundsFilesystem": "Egyedi hangok fájlrendszere", + "Daily_Active_Users": "Napi aktív felhasználók", "Dashboard": "Irányítópult", "Data_processing_consent_text": "Hozzájárulás adatfeldolgozáshoz szöveg", "Data_processing_consent_text_description": "Használja ezt a beállítás, ahol tájékoztatja a felhasználót, hogy gyűjti, tárolja és feldolgozza a felhasználó személyes adatait a beszélgetések mellett.", @@ -1066,6 +1202,7 @@ "Date_From": "Ból ből", "Date_to": "nak nek", "days": "nap", + "Days": "Napok", "DB_Migration": "Adatbázis migráció", "DB_Migration_Date": "Adatbázis migráció időpontja", "DDP_Rate_Limit_Connection_Enabled": "Korlátozás kapcsolat alapján: engedélyezve", @@ -1078,8 +1215,12 @@ "Default": "Alapértelmezett", "Default_value": "Alapértelmezett érték", "Delete": "Töröl", + "Deleting": "Törlés", + "Delete_all_closed_chats": "Minden lezárt csevegés törlése", + "Delete_File_Warning": "Egy fájl törlése véglegesen törli azt. Ezt nem lehet visszacsinálni.", "Delete_message": "Üzenet törlése", "Delete_my_account": "A fiókom törlése", + "Delete_Role_Warning": "Egy szerepkör törlése véglegesen törli azt. Ezt nem lehet visszacsinálni.", "Delete_Room_Warning": "Törlése szoba törli az összes üzenetet küldte a szobában. Ezt nem lehet visszacsinálni.", "Delete_User_Warning": "Törlése felhasználó összes üzenetet törölni, hogy a felhasználó is. Ezt nem lehet visszacsinálni.", "Delete_User_Warning_Delete": "Törlése felhasználó összes üzenetet törölni, hogy a felhasználó is. Ezt nem lehet visszacsinálni.", @@ -1091,16 +1232,20 @@ "delete-d_description": "Engedély a közvetlen üzenetek törlésére", "delete-message": "Üzenet törlése", "delete-message_description": "Engedély az üzenet törlésére egy szobában", + "delete-own-message": "Saját üzenet törlése", + "delete-own-message_description": "Saját üzenet törlésének engedélyezése", "delete-p": "Privát csatornák törlése", "delete-p_description": "Engedély privát csatornák törléséhez", "delete-user": "Felhasználó törlése", "delete-user_description": "Engedély a felhasználók törléséhez", "Deleted": "Törölve!", "Department": "Részleg", + "Department_name": "Részleg neve", "Department_not_found": "Az osztály nem található", "Department_removed": "Részleg eltávolítva", "Departments": "Részlegek", "Deployment_ID": "Telepítés azonosító", + "Deployment": "Telepítés", "Description": "Leírás", "Desktop": "Asztal", "Desktop_Notification_Test": "Asztali értesítés teszt", @@ -1135,12 +1280,14 @@ "Direct_Reply_Username": "Felhasználónév", "Direct_Reply_Username_Description": "Kérem, használja az abszolút e-mailt, a címkézés nem megengedett, túlzás lenne", "Directory": "Címtár", + "Disable": "Tilt", "Disable_Facebook_integration": "Facebook integráció tiltása", "Disable_Notifications": "Értesítések letiltása", "Disable_two-factor_authentication": "Kétlépcsős azonosítása tiltása", "Disabled": "Tiltva", "Disallow_reacting": "Reagálás tiltása", "Disallow_reacting_Description": "Hatástalanítja a reagálást", + "Discard": "Eldob", "Disconnect": "Megszakítás", "Discussion": "Beszélgetés", "Discussion_first_message_title": "Az Ön üzenete", @@ -1152,11 +1299,18 @@ "Discussion_title": "Új beszélgetés létrehozása", "discussion-created": "__message__", "Discussions": "Beszélgetések", + "Display": "Megjelenítés", + "Display_avatars": "Avatarok megjelenítése", + "Display_Avatars_Sidebar": "Avatárok megjelenítése az oldalsávban", "Display_chat_permissions": "Chat engedélyek megjelenítése", "Display_offline_form": "Offline űrlap megjelenítése", + "Display_setting_permissions": "A beállítások módosítására vonatkozó engedélyek megjelenítése", "Display_unread_counter": "Az olvasatlan üzenetek számának megjelenítése", "Displays_action_text": "Kijelzők akció szöveg", + "Do_It_Later": "Csináld később", "Do_not_display_unread_counter": "Ne jelenjen meg a csatorna számlálója", + "Do_not_provide_this_code_to_anyone": "Ne add át ezt a kódot senkinek.", + "Do_Nothing": "Ne csinálj semmit", "Do_you_want_to_accept": "Szeretné elfogadni?", "Do_you_want_to_change_to_s_question": "Szeretné megváltoztatni %s?", "Document_Domain": "Dokumentumtartomány", @@ -1168,7 +1322,10 @@ "Dont_ask_me_again": "Ne kérdezzen újra!", "Dont_ask_me_again_list": "Ne kérdezze meg újra lista", "Download": "Letöltés", + "Download_Info": "Letöltési információ", "Download_My_Data": "Adataim letöltése", + "Download_Pending_Avatars": "Függő avatárok letöltése", + "Download_Pending_Files": "Függő fájlok letöltése", "Download_Snippet": "Letöltés", "Downloading_file_from_external_URL": "Fájl letöltése külső URL címről", "Drop_to_upload_file": "Dobja ide a fájlt a feltöltéshez", @@ -1176,18 +1333,30 @@ "Dry_run_description": "Csak akkor küldünk egy e-mailt, hogy ugyanaz a cím, mint a From. Az e-mail kell tartozniuk felhasználó érvényes.", "Duplicate_archived_channel_name": "Archivált Channel névvel ' %s' létezik", "Duplicate_archived_private_group_name": "Archivált Private csoport név ' %s' létezik", - "Duplicate_channel_name": "A csatorna névvel '%s' létezik", + "Duplicate_channel_name": "A Channel névvel '%s' létezik", + "Duplicate_file_name_found": "Duplikált fájlnevet találtunk.", "Duplicate_private_group_name": "A Private csoport neve ' %s' létezik", "Duplicated_Email_address_will_be_ignored": "A duplikált e-mail címeket nem vesszük figyelembe.", "duplicated-account": "Megkettőzött fiók", "E2E Encryption": "E2E titkosítás", + "E2E_enable": "E2E engedélyezése", + "E2E_disable": "E2E kikapcsolása", "E2E_Enabled": "E2E engedélyezve", + "E2E_Encryption_Password_Change": "Titkosítási jelszó módosítása", + "ECDH_Enabled": "Második rétegű titkosítás engedélyezése az adatátvitelhez", "Edit": "Szerkesztés", + "Edit_Canned_Response": "Sablon válasz szerkesztése", + "Edit_Canned_Responses": "Sablon válaszok szerkesztése", "Edit_Custom_Field": "Egyedi mező szerkesztése", "Edit_Department": "Osztály szerkesztése", + "Edit_Invite": "Meghívó szerkesztése", "Edit_previous_message": "`%s` - Az előző üzenet szerkesztése", + "Edit_Priority": "Prioritás szerkesztése", "Edit_Status": "Állapot szerkesztése", + "Edit_Tag": "Címke szerkesztése", "Edit_Trigger": "Trigger szerkesztése", + "Edit_Unit": "Egység szerkesztése", + "Edit_User": "Felhasználó szerkesztése", "edit-message": "Üzenet szerkesztése", "edit-message_description": "Engedély az üzenet szerkesztésére egy szobában", "edit-other-user-active-status": "Egyéb felhasználói aktív állapot szerkesztése", @@ -1202,19 +1371,26 @@ "edit-privileged-setting_description": "Engedély a beállítások szerkesztéséhez", "edit-room": "Room szerkesztése", "edit-room_description": "Engedély a szoba név, téma, típus (privát vagy nyilvános státusz) és státusz módosítására (aktív vagy archivált)", + "edit-room-avatar": "Room profilkép módosítása", "edit-room-retention-policy": "A szoba megőrzési szabályainak szerkesztése", "edit-room-retention-policy_description": "Engedély a szoba fenntartásának szabályaihoz, hogy automatikusan törölje az üzeneteket", "edited": "szerkesztve", "Editing_room": "vágószobából", "Editing_user": "szerkesztés alatt", + "Editor": "Szerkesztő", "Education": "Oktatás", + "Email": "Email", "Email_address_to_send_offline_messages": "E-mail cím, ahova az offline üzenetek küldésre kerülnek", "Email_already_exists": "Az e-mail cím már létezik", "Email_body": "E-mail szövege", "Email_Change_Disabled": "A Rocket.Chat adminisztrátora letiltotta az e-mail cím változtatását", + "Email_Changed_Email_Subject": "[Site_Name] - Az e-mail cím megváltozott", + "Email_changed_section": "Az e-mail cím megváltozott", "Email_Footer_Description": "Használhatja a következő szimbólumokat:
      • [Site_Name] és [Site_URL] Az Alkalmazás neve és URL ill.
      ", "Email_from": "Feladó", "Email_Header_Description": "Használhatja a következő szimbólumokat:
      • [Site_Name] és [Site_URL] Az Alkalmazás neve és URL ill.
      ", + "Email_Inbox": "Email bejövő levelek", + "Email_Inboxes": "Email bejövő levelek", "Email_Notification_Mode": "Offline E-mail értesítések", "Email_Notification_Mode_All": "Minden említés / Közvetlen üzenet", "Email_Notification_Mode_Disabled": "Tiltva", @@ -1223,6 +1399,7 @@ "Email_or_username": "E-mail vagy felhasználónév", "Email_Placeholder": "Kérjük, adja meg e-mail címét...", "Email_Placeholder_any": "Kérjük, írja be az e-mail címeket ...", + "email_plain_text_only": "Csak egyszerű szöveges e-mailek küldése", "email_style_description": "Egymásba ágyazott kiválasztok mellőzése", "email_style_label": "E-mail stílusa", "Email_subject": "Tárgy", @@ -1233,7 +1410,9 @@ "Empty_title": "Üres címet", "Enable": "Engedélyezze", "Enable_Auto_Away": "Engedélyezze az automatikus kikapcsolást", + "Enable_CSP": "Tartalom-biztonsági házirend (CSP) engedélyezése", "Enable_Desktop_Notifications": "Asztali értesítések engedélyezése", + "Enable_Password_History": "Jelszótörténet engedélyezése", "Enable_Svg_Favicon": "Engedélyezze az SVG favicon szolgáltatást", "Enable_two-factor_authentication": "Kétlépcsős hitelesítés engedélyezése", "Enabled": "Engedélyezett", @@ -1243,6 +1422,9 @@ "Encryption_key_saved_successfully": "A titkosító kulcs mentése sikeres.", "End": "Vége", "End_OTR": "OTR vége", + "Enter": "Belépés", + "Enter_a_custom_message": "Egyéni üzenet megadása", + "Enter_a_department_name": "Adja meg a részleg nevét", "Enter_a_name": "Írja be a nevet", "Enter_a_regex": "Adjon meg egy regex-et", "Enter_a_room_name": "Írja be a szoba nevét", @@ -1252,11 +1434,13 @@ "Enter_authentication_code": "Írja be a hitelesítési kódot", "Enter_Behaviour": "Írja be a kulcsmódot", "Enter_Behaviour_Description": "Ez megváltozik, ha az Enter gomb elküldi az üzenetet vagy megszakítja a sort", + "Enter_E2E_password": "E2E jelszó megadása", "Enter_name_here": "Írd be a nevet", "Enter_Normal": "Normál mód (küldés az Enter billentyűvel)", "Enter_to": "Belépés a", "Enter_your_E2E_password": "Adja meg E2E jelszavát", "Enterprise": "Vállalkozás", + "Enterprise_License": "Vállalati licensz", "Entertainment": "Szórakozás", "Error": "Hiba", "Error_404": "404-es hibakód", @@ -1266,25 +1450,32 @@ "Error_RocketChat_requires_oplog_tailing_when_running_in_multiple_instances_details": "Győződjön meg róla, hogy a MongoDB ReplicaSet módban van és a MONGO_OPLOG_URL környezeti változó helyesen van definiálva az alkalmazáskiszolgálón", "Error_sending_livechat_offline_message": "Hiba a Livechat offline üzenet küldésekor", "Error_sending_livechat_transcript": "Hiba a Livechat átirat küldésekor", + "Error_Site_URL": "Érvénytelen Site_Url", "error-action-not-allowed": "__action__ nem engedélyezett", + "error-agent-offline": "Az ügynök offline", "error-application-not-found": "Alkalmazás nem található", "error-archived-duplicate-name": "Van egy archivált csatorna neve '__room_name__ \"", "error-avatar-invalid-url": "Érvénytelen avatar URL: __url__", "error-avatar-url-handling": "Hiba kezelése avatar beállítás egy URL (__url__) az __username__", + "error-blocked-username": "__field__ blokkolva van és nem használható!", + "error-canned-response-not-found": "Sablon válasz nem található", "error-cant-invite-for-direct-room": "Nem lehet meghívni a felhasználó közvetlen szobák", "error-channels-setdefault-is-same": "A csatorna alapértelmezett beállítása megegyezik a változtatással.", "error-channels-setdefault-missing-default-param": "A bodyParam alapértelmezett beállítása szükséges", "error-could-not-change-email": "Nem sikerült megváltoztatni az e-mail", "error-could-not-change-name": "Nem sikerült megváltoztatni a nevét", "error-could-not-change-username": "Nem sikerült megváltoztatni a felhasználónevet", + "error-custom-field-name-already-exists": "Egyéni mezőnév már létezik", "error-delete-protected-role": "Nem lehet törölni a védett szerepet", "error-department-not-found": "Az osztály nem található", "error-direct-message-file-upload-not-allowed": "A fájlmegosztás nem engedélyezett a közvetlen üzenetekben", + "error-duplicate-channel-name": "Létezik egy csatorna '__channel_name__' névvel", "error-edit-permissions-not-allowed": "A jogosultságok szerkesztése nem engedélyezett", "error-email-domain-blacklisted": "Az e-mail domain feketelistára", "error-email-send-failed": "Hiba történt e-mail küldésénél: __message__", "error-field-unavailable": "__field__ név már használatban van :(", "error-file-too-large": "A fájl túl nagy", + "error-forwarding-chat": "A csevegés továbbítása közben hiba történt, kérjük, próbáld meg később újra.", "error-import-file-extract-error": "Az importált fájl kicsomagolása sikertelen.", "error-import-file-is-empty": "Az importált fájl üres.", "error-import-file-missing": "Az importálandó fájl nem található a megadott útvonalon.", @@ -1298,21 +1489,28 @@ "error-invalid-channel-start-with-chars": "Érvénytelen csatorna. Kezdje @ vagy a #", "error-invalid-custom-field": "Érvénytelen egyéni mező", "error-invalid-custom-field-name": "Érvénytelen egyéni mező nevét. Csak betűk, számok, kötőjel és aláhúzás.", + "error-invalid-custom-field-value": "Érvénytelen érték a __field__ mezőhöz", "error-invalid-date": "Érvénytelen dátum megadva.", "error-invalid-description": "érvénytelen leírás", "error-invalid-domain": "érvénytelen domain", "error-invalid-email": "Érvénytelen e-mail __email__", "error-invalid-email-address": "Érvénytelen e-mail cím", + "error-invalid-email-inbox": "Érvénytelen e-mail postafiók", "error-invalid-file-height": "Érvénytelen fájl magassága", "error-invalid-file-type": "Érvénytelen fájltípus", "error-invalid-file-width": "Érvénytelen fájl szélessége", "error-invalid-from-address": "Informálják érvénytelen feladó címét.", + "error-invalid-inquiry": "Érvénytelen lekérdezés", "error-invalid-integration": "érvénytelen integráció", "error-invalid-message": "érvénytelen üzenet", "error-invalid-method": "érvénytelen módszer", - "error-invalid-name": "érvénytelen név", + "error-invalid-name": "Érvénytelen név", "error-invalid-password": "Érvénytelen jelszó", + "error-invalid-param": "Érvénytelen paraméter", + "error-invalid-params": "Érvénytelen paraméterek", "error-invalid-permission": "Érvénytelen engedély", + "error-invalid-port-number": "Érvénytelen portszám", + "error-invalid-priority": "Érvénytelen prioritás", "error-invalid-redirectUri": "érvénytelen redirectUri", "error-invalid-role": "érvénytelen szerepe", "error-invalid-room": "érvénytelen szoba", @@ -1325,6 +1523,7 @@ "error-invalid-urls": "Érvénytelen URL-ek", "error-invalid-user": "Érvénytelen felhasználó", "error-invalid-username": "Érvénytelen felhasználónév", + "error-invalid-value": "Érvénytelen érték", "error-invalid-webhook-response": "A webhook URL nem 200-as HTTP státusszal válaszolt", "error-logged-user-not-in-room": "Nem vagy a `%s` szobában", "error-message-deleting-blocked": "Üzenet törlés blokkolva", @@ -1344,25 +1543,36 @@ "error-password-policy-not-met-oneUppercase": "A jelszó nem felel meg a szerver házirendjének legalább egy nagybetűs karakterének", "error-password-policy-not-met-repeatingCharacters": "A jelszó nem felel meg a kiszolgáló tiltott ismétlődő karaktereinek (túl sok azonos karakter van egymás mellett)", "error-personal-access-tokens-are-current-disabled": "A személyi hozzáférési tokenek jelenleg le vannak tiltva", + "error-pinning-message": "Az üzenetet nem lehetett kitűzni", "error-push-disabled": "Push le van tiltva", "error-remove-last-owner": "Ez az utolsó tulajdonos. Kérjük, állítsa be az új tulajdonost, mielőtt ezt eltávolítaná.", "error-role-in-use": "Nem lehet törölni a szerepet, mert használatban van", "error-role-name-required": "Szerep neve szükséges", + "error-role-already-present": "Ezzel a névvel már létezik egy szerepkör", "error-room-is-not-closed": "A szoba nincs zárva", + "error-starring-message": "Az üzenetet nem lehetett megcsillagozni", "error-the-field-is-required": "A mező __field__ szükséges.", "error-this-is-not-a-livechat-room": "Ez nem egy Livechat szoba", "error-token-already-exists": "Már létezik token ezen a néven", "error-token-does-not-exists": "A token nem létezik", "error-too-many-requests": "Hiba, túl sok kérés. Kérlek lassíts le. Meg kell várni, __seconds__ másodpercet, mielőtt újra próbálkozna.", + "error-transcript-already-requested": "Az átirat már kérvényezve", + "error-unpinning-message": "Az üzenet kitűzését nem lehetett feloldani", "error-user-has-no-roles": "A felhasználónak nincs szerepe", "error-user-is-not-activated": "Felhasználó nem aktív", + "error-user-is-not-agent": "A felhasználó nem Omnichannel ügynök", + "error-user-is-offline": "Felhasználó ha offline", "error-user-limit-exceeded": "A (z) #channel_name címre meghívott felhasználók száma meghaladja a rendszergazda által meghatározott értéket", + "error-user-not-belong-to-department": "A felhasználó nem tartozik ehhez az osztályhoz", "error-user-not-in-room": "A felhasználó nincs ebben a szobában", "error-user-registration-disabled": "Felhasználó regisztráció letiltva", "error-user-registration-secret": "Felhasználó regisztráció csak titkos URL segítségével lehetséges", + "error-no-owner-channel": "Csak a tulajdonosok adhatják hozzá ezt a csatornát a csapathoz", "error-you-are-last-owner": "Te vagy az utolsó tulajdonos. Kérjük, állítsd be az új tulajdonost, mielőtt elhagyod a szobát.", "Errors_and_Warnings": "Hibák és figyelmeztetések", "Esc_to": "Esc, hogy", + "Estimated_due_time": "Becsült esedékességi idő", + "Estimated_due_time_in_minutes": "Becsült esedékességi idő (percben kifejezve)", "Event_Trigger": "Esemény trigger", "Event_Trigger_Description": "Válassza ki, hogy az esemény típusa melyik kimenő webhook integrációt fogja indítani", "every_5_minutes": "5 percenként", @@ -1374,6 +1584,8 @@ "every_second": "Minden másodpercben", "every_six_hours": "6 óránként", "Everyone_can_access_this_channel": "Mindenki hozzáférhet ehhez a csatornához", + "Exact": "Egyező", + "Example_payload": "Példa payload", "Example_s": "Példa: %s", "except_pinned": "(kivéve azokat, amelyek rögzítettek)", "Exclude_Botnames": "Kizárja a botokat", @@ -1381,13 +1593,21 @@ "Exclude_pinned": "Kizárt rögzített üzenetek", "Execute_Synchronization_Now": "Végezze el a szinkronizálást most", "Exit_Full_Screen": "Kilépés a teljes képernyőből", + "Expand": "Kinyit", + "Expiration": "Lejárat", + "Expiration_(Days)": "Lejárat (napok)", + "Export_as_file": "Exportálás fájlként", + "Export_Messages": "Üzenetek exportálása", "Export_My_Data": "Adataim exportálása", "expression": "Kifejezés", "Extended": "Kiterjesztett", + "External": "Külső", "External_Domains": "Külső domainek", "External_Queue_Service_URL": "Külső sor szolgáltatás URL-je", "External_Service": "Külső szolgáltatás", "External_Users": "Külső felhasználók", + "Extremely_likely": "Rendkívül valószínű", + "Facebook": "Facebook", "Facebook_Page": "Facebook oldal", "Failed": "Sikertelen", "Failed_to_activate_invite_token": "Nem sikerült aktiválni a meghívó tokent", @@ -1399,8 +1619,17 @@ "Favorite_Rooms": "Engedélyezze Kedvenc szobák", "Favorites": "Kedvencek", "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "Ez a funkció a \"Látogatók navigálási előzményeinek küldése üzenetként való elküldéséhez\" tartozik.", + "Features": "Jellemzők", "Features_Enabled": "Jellemzők Enabled", + "Federation_Username": "Felhasználónév: myfriendsusername@anotherdomain.com", + "Federation_Email": "E-mail cím: joseph@remotedomain.com", + "Federation_Invite_User": "Felhasználó meghívása", + "Federation_Adding_users_from_another_server": "Felhasználók hozzáadása egy másik kiszolgálóról", + "Federation_Configure_DNS": "DNS konfigurálása", "Federation_Domain": "Domain", + "Federation_Fix_now": "Javítsd meg most!", + "Federation_Legacy_support": "Legacy támogatás", + "Federation_HTTP_instead_HTTPS": "Ha a HTTP protokollt használja a HTTPS helyett", "Federation_Public_key": "Publikus kulcs", "Federation_Discovery_method": "Felfedezési mód", "Federation_Protocol": "Protokoll", @@ -1413,13 +1642,19 @@ "Field": "Mező", "Field_removed": "Field eltávolított", "Field_required": "Szükséges mező", + "File": "Fájl", + "File_Downloads_Started": "Fájlok letöltése megkezdődött", "File_exceeds_allowed_size_of_bytes": "Fájl mérete meghaladja a megengedett méretet a __size__ bájt", "File_name_Placeholder": "Fájlok keresése ...", "File_not_allowed_direct_messages": "A fájlmegosztás nem engedélyezett a közvetlen üzenetekben.", + "File_Path": "Fájl elérési útvonal", "File_removed_by_automatic_prune": "A fájl eltávolítása automatikus prúzással történik", "File_removed_by_prune": "A fájl eltávolítása a prúzással történik", + "File_Type": "Fájltípus", "File_type_is_not_accepted": "A fájltípus nem fogadható el.", "File_uploaded": "Fájl feltöltve", + "File_uploaded_successfully": "Fájl sikeresen feltöltve", + "File_URL": "Fájl URL", "files": "fájlok", "Files": "Fájlok", "Files_only": "Csak távolítsa el a csatolt fájlokat, tartsa meg az üzeneteket", @@ -1428,6 +1663,7 @@ "FileSize_KB": "__fileSize__ KB", "FileSize_MB": "__fileSize__ MB", "FileUpload": "Fájlfeltöltés", + "FileUpload_Cannot_preview_file": "Nem lehet előnézetben megjeleníteni a fájlt", "FileUpload_Disabled": "A fájl feltöltések le vannak tiltva.", "FileUpload_Enabled": "Fájlfeltöltések engedélyezve", "FileUpload_Enabled_Direct": "Fájl feltöltés engedélyezése a közvetlen üzeneteknél", @@ -1446,11 +1682,14 @@ "FileUpload_GoogleStorage_Secret_Description": "Kérjük, kövesse ezeket az utasításokat, és illessze ide az eredményt.", "FileUpload_MaxFileSize": "Feltölthető legnagyobb fájlméret (bájtban)", "FileUpload_MaxFileSizeDescription": "Állítsa -1-re a fájlméret korlátozásának eltávolításához.", + "FileUpload_MediaType_NotAccepted__type__": "Nem elfogadott médiatípus: __type__", "FileUpload_MediaType_NotAccepted": "Média típusok Nem Elfogadva", + "FileUpload_MediaTypeBlackList": "Blokkolt médiatípusok", "FileUpload_MediaTypeWhiteList": "Elfogadott média típusok", "FileUpload_MediaTypeWhiteListDescription": "Vesszővel elválasztott listája média típusok. Hagyja üresen elfogadó minden média típus.", "FileUpload_ProtectFiles": "Feltöltött fájlok védelme", "FileUpload_ProtectFilesDescription": "Csak regisztrált felhasználók férhetnek", + "FileUpload_RotateImages_Description": "A beállítás engedélyezése a képminőség romlását okozhatja", "FileUpload_S3_Acl": "Amazon S3 acl", "FileUpload_S3_AWSAccessKeyId": "Amazon S3 AWSAccessKeyId", "FileUpload_S3_AWSSecretAccessKey": "Amazon S3 AWSSecretAccessKey", @@ -1477,7 +1716,10 @@ "FileUpload_Webdav_Upload_Folder_Path_Description": "A WebDAV mappák elérési útja, ahová a fájlokat fel kell tölteni", "FileUpload_Webdav_Username": "WebDAV felhasználónév", "Filter": "Szűrő", + "Filters": "Szűrők", "Financial_Services": "Pénzügyi szolgáltatások", + "Finish": "Befejezés", + "Finish_Registration": "Regisztráció befejezése", "First_Channel_After_Login": "Első csatorna bejelentkezés után", "First_response_time": "Első válasz idő", "Flags": "Zászlók", @@ -1492,9 +1734,12 @@ "For_your_security_you_must_enter_your_current_password_to_continue": "A folytatáshoz újra meg kell adnia jelenlegi jelszavát", "Force_Disable_OpLog_For_Cache": "Távolítsa el az OpLog gyorsítótárat", "Force_Disable_OpLog_For_Cache_Description": "Az OpLog nem használja a gyorsítótár szinkronizálását, még akkor sem, ha rendelkezésre áll", + "Force_Screen_Lock": "Képernyőzárolás kikényszerítése", + "Force_Screen_Lock_After": "Képernyőzárolás kikényszerítése után", "Force_SSL": "SSL kényszerítése", "Force_SSL_Description": "* Figyelem! * _Force SSL_ soha nem lehet fordított proxy. Ha van egy fordított proxy, meg kell tennie az átirányítást OTT. Ez a lehetőség fennáll telepítések mint Heroku, amely nem teszi lehetővé az átirányítás konfiguráció a fordított proxy.", "Force_visitor_to_accept_data_processing_consent": "Adatfeldolgozási hozzájárulás kényszerítése", + "Force_visitor_to_accept_data_processing_consent_description": "A látogatók nem kezdhetnek csevegésbe beleegyezés nélkül.", "force-delete-message": "Törölje az üzenet törlését", "force-delete-message_description": "Engedély az üzenetek törléséhez, az összes korlátozás megkerülésével", "Forgot_password": "Elfelejtetted a jelszavad?", @@ -1506,16 +1751,27 @@ "Forward_chat": "Chat továbbítása", "Forward_to_department": "Továbbítás a részlegnek", "Forward_to_user": "Továbbítás a felhasználónak", + "Forwarding": "Továbbítás", "Free": "Szabad", "Frequently_Used": "Gyakran használt", "Friday": "Péntek", "From": "Feladó", "From_Email": "E-mail feladója", "From_email_warning": "Figyelmeztetés: A mező van kitéve az e-mail szerver beállításait.", + "Full_Name": "Teljes név", "Full_Screen": "Teljes képernyő", "Gaming": "Szerencsejáték", "General": "Általános", + "Generate_new_key": "Új kulcs generálása", + "Generate_New_Link": "Új link generálása", + "Generating_key": "Kulcs generálása", "Get_link": "Hivatkozás", + "get-password-policy-forbidRepeatingCharacters": "A jelszó nem tartalmazhat ismétlődő karaktereket", + "get-password-policy-forbidRepeatingCharactersCount": "A jelszó nem tartalmazhat több mint __forbidRepeatingCharactersCount__ ismétlődő karaktert", + "get-password-policy-maxLength": "A jelszónak legfeljebb __maxLength__ karakter hosszúságúnak kell lennie", + "get-password-policy-minLength": "A jelszónak legalább __minLength__ karakter hosszúságúnak kell lennie", + "get-password-policy-mustContainAtLeastOneLowercase": "A jelszónak legalább egy kisbetűt kell tartalmaznia", + "get-password-policy-mustContainAtLeastOneNumber": "A jelszónak legalább egy számot kell tartalmaznia", "github_no_public_email": "Nincs egy email címed se publikusként megadva a GitHub fiókodban", "Give_a_unique_name_for_the_custom_oauth": "Adj egy egyedi nevet az egyéni OAuth", "Give_the_application_a_name_This_will_be_seen_by_your_users": "Adjon az alkalmazás nevét. Ez látható lesz az Ön számára.", @@ -1529,14 +1785,19 @@ "Government": "Kormány", "Graphql_CORS": "GraphQL CORS", "Graphql_Enabled": "GraphQL engedélyezve", + "Group_by": "Csoportosítás", "Group_by_Type": "Csoportosítás típus szerint", "Group_discussions": "Csoportos beszélgetések", "Group_favorites": "Kedvencek csoportosítása", "Group_mentions_disabled_x_members": "A \"@ all\" és a \"@ itt\" csoport megjegyzi, hogy több mint __total__ taggal rendelkeznek.", "Group_mentions_only": "A csoport csak említi", + "Grouping": "Csoportosítás", + "Guest": "Vendég", "Hash": "hash", "Header": "Fejléc", "Header_and_Footer": "Fejléc és lábléc", + "Pharmaceutical": "Gyógyszeripari", + "Healthcare": "Egészségügy", "Helpers": "Segítők", "Hex_Color_Preview": "Hex színek elölnézet", "Hi": "Szia", @@ -1553,6 +1814,7 @@ "Hide_Room_Warning": "Biztosan el szeretné rejteni a szobában \"%s\"?", "Hide_Unread_Room_Status": "Az olvasatlan állapot helyének elrejtése", "Hide_usernames": "Felhasználói nevek elrejtése", + "Hide_video": "Videó elrejtése", "Highlights": "Kiemelések", "Highlights_How_To": "Értesítést, ha valaki megemlíti a szót vagy kifejezést, add ide. Akkor külön szavak és kifejezések vesszővel. Kiemelés szavak nem érzékenyek.", "Highlights_List": "Kiemelés szavak", @@ -1566,6 +1828,7 @@ "How_responsive_was_the_chat_agent": "Mennyire volt készséges az operátor?", "How_satisfied_were_you_with_this_chat": "Mennyire volt elégedett ezzel a beszélgetéssel?", "How_to_handle_open_sessions_when_agent_goes_offline": "Hogyan kell kezelni az Open Sessions-t, amikor az ügynök offline állapotban van", + "I_Saved_My_Password": "Elmentettem a jelszavamat", "Idle_Time_Limit": "Tétlenségi időkorlát", "Idle_Time_Limit_Description": "Időtartam, amíg a státusz nem változik el. Az értéknek másodpercben kell lennie.", "if_they_are_from": "(ha %s-ból származik)", @@ -1583,6 +1846,7 @@ "Iframe_Integration_send_enable_Description": "Események küldése a szülőablakba", "Iframe_Integration_send_target_origin": "Cél származás küldése", "Iframe_Integration_send_target_origin_Description": "A protokoll előtaggal való származás, mely parancsokat elküldjük pl. 'https: // localhost', vagy * a küldés bárhová történő küldéséhez.", + "Iframe_Restrict_Access": "A hozzáférés korlátozása bármelyik Iframe-en belül", "Ignore": "Figyelmen kívül hagyni", "Ignored": "Figyelmen kívül hagyva", "Images": "Képek", @@ -1619,18 +1883,28 @@ "Importer_Slack_Users_CSV_Information": "A feltöltött fájlnak laza felhasználók exportfájlja, amely CSV fájl. További információk itt találhatók:", "Importer_Source_File": "Forrásfájl kiválasztása", "importer_status_done": "Sikeresen befejezve", + "importer_status_downloading_file": "Fájl letöltése", + "importer_status_file_loaded": "Fájl betöltve", "importer_status_finishing": "Majdnem kész", "importer_status_import_cancelled": "Megszakítva", "importer_status_import_failed": "Hiba", "importer_status_importing_channels": "Csatornák importálása", + "importer_status_importing_files": "Fájlok importálása", "importer_status_importing_messages": "Üzenetek importálása", + "importer_status_importing_started": "Adatok importálása", "importer_status_importing_users": "Felhasználók importálása", "importer_status_new": "Nem megkezdett", + "importer_status_preparing_channels": "Csatorna fájl olvasása", + "importer_status_preparing_messages": "Üzenetfájlok olvasása", + "importer_status_preparing_started": "Fájlok olvasása", + "importer_status_uploading": "Fájl feltöltése", "Importer_Upload_FileSize_Message": "A szerver beállítások a fájlok feltöltését a __maxFileSize__ méretig engedélyezik.", "Importer_Upload_Unlimited_FileSize": "A szerver beállítások bármilyen méretű fájl feltöltését engedélyezik.", "Importing_channels": "Csatornák importálása", + "Importing_Data": "Adatok importálása", "Importing_messages": "Üzenetek importálása", "Importing_users": "Felhasználók importálása", + "Inactivity_Time": "Inaktivitási idő", "In_progress": "Folyamatban", "Inclusive": "befogadó", "Incoming_Livechats": "Bejövő Livechats", @@ -1653,9 +1927,11 @@ "Instance_Record": "Példány nyilvántartás", "Instructions": "Utasítások", "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Útmutató a látogató töltse ki az űrlapot, hogy küldjön egy üzenetet", + "Insert_Contact_Name": "Írja be a kapcsolattartó nevét", "Insurance": "Biztosítás", "Integration_added": "Integráció hozzáadásra került", "Integration_Advanced_Settings": "Haladó beállítások", + "Integration_Delete_Warning": "Az integráció törlése nem vonható vissza.", "Integration_disabled": "Integráció letiltva", "Integration_History_Cleared": "Az integrációs történet sikeresen törölve", "Integration_Incoming_WebHook": "Bejövő WebHook integráció", @@ -1721,12 +1997,13 @@ "Invitation_Email_Description": "Használhatja a következő szimbólumokat:
      • [email] A címzett e-mail.
      • [Site_Name] és [Site_URL] Az Alkalmazás neve és URL ill.
      ", "Invitation_HTML": "Meghívó HTML", "Invitation_HTML_Default": "

      Ön meghívást kapott [Site_Name] oldalra

      Tovább a [Site_URL], és próbálja ki a ma elérhető legjobb nyílt forráskódú chat megoldást!

      ", - "Invitation_Subject": "Meghívó Tárgy", + "Invitation_Subject": "Meghívó tárgya", "Invitation_Subject_Default": "Ön meghívást kaptak [Site_Name]", "Invite_user_to_join_channel": "Kérj meg egy felhasználó számára, hogy csatlakozzon ehhez a csatornához", "Invite_user_to_join_channel_all_from": "Hívja meg a [#channel] összes felhasználóját, hogy csatlakozzon ehhez a csatornához", "Invite_user_to_join_channel_all_to": "A csatornáról minden felhasználó meghívja a [#channel]", "Invite_Users": "Felhasználók meghívása", + "IP": "IP cím", "IRC_Channel_Join": "A JOIN parancs kimenete.", "IRC_Channel_Leave": "A PART parancs kimenete.", "IRC_Channel_Users": "A NAMES parancs kimenete.", @@ -1755,9 +2032,11 @@ "italics": "dőlt", "Jitsi_Chrome_Extension": "Chrome-bővítmény Id", "Jitsi_Enable_Channels": "Engedélyezze a csatornák", + "Jitsi_Enable_Teams": "Engedélyezés a csapatok számára", "Jitsi_Enabled_TokenAuth": "JWT hitelesítés engedélyezése", "Job_Title": "Munka megnevezése", "join": "Csatlakozás", + "Join_call": "Csatlakozz a híváshoz", "Join_audio_call": "Csatlakozás audio hívást", "Join_Chat": "Csatlakozás beszélgetéshez", "Join_default_channels": "Csatlakozz alapértelmezett csatornák", @@ -1797,17 +2076,37 @@ "Knowledge_Base": "Tudásbázis", "Label": "Címke", "Language": "Nyelv", + "Language_Bulgarian": "Bolgár", + "Language_Chinese": "Kínai", + "Language_Czech": "Cseh", + "Language_Danish": "Dán", "Language_Dutch": "Holland", "Language_English": "Angol", + "Language_Estonian": "Észt", + "Language_Finnish": "Finn", "Language_French": "Francia", "Language_German": "Német", + "Language_Greek": "Görög", + "Language_Hungarian": "Magyar", "Language_Italian": "Olasz", + "Language_Japanese": "Japán", + "Language_Latvian": "Lett", + "Language_Lithuanian": "Litván", "Language_Not_set": "Nincs megadva", "Language_Polish": "Lengyel", "Language_Portuguese": "Portugál", + "Language_Romanian": "Román", "Language_Russian": "Orosz", + "Language_Slovak": "Szlovák", + "Language_Slovenian": "Szlovén", "Language_Spanish": "Spanyol", + "Language_Swedish": "Svéd", "Language_Version": "Angol nyelvű verzió", + "Last_7_days": "Utolsó 7 nap", + "Last_30_days": "Utolsó 30 nap", + "Last_90_days": "Utolsó 90 nap", + "Last_active": "Utoljára aktív", + "Last_Chat": "Utolsó csevegés", "Last_login": "Utolsó bejelentkezés", "Last_Message": "Utolsó üzenet", "Last_Message_At": "Utolsó üzenet", @@ -1827,6 +2126,19 @@ "Layout_Sidenav_Footer_description": "Footer mérete 260 x 70 képpont", "Layout_Terms_of_Service": "Szolgáltatás feltételei", "LDAP": "LDAP", + "LDAP_Documentation": "LDAP dokumentáció", + "LDAP_Connection": "Kapcsolat", + "LDAP_Connection_Authentication": "Hitelesítés", + "LDAP_Connection_Encryption": "Titkosítás", + "LDAP_Connection_Timeouts": "Időtúllépések", + "LDAP_UserSearch": "Felhasználó keresés", + "LDAP_UserSearch_Filter": "Keresés szűrő", + "LDAP_UserSearch_GroupFilter": "Csoport szűrő", + "LDAP_DataSync_BackgroundSync": "Háttér szinkronizálás", + "LDAP_Server_Type": "Kiszolgáló típusa", + "LDAP_Server_Type_AD": "Active Directory", + "LDAP_Server_Type_Other": "Egyéb", + "LDAP_Name_Field": "Név mező", "LDAP_Advanced_Sync": "Haladó szinkronizálás", "LDAP_Authentication": "Engedélyezze", "LDAP_Authentication_Password": "Jelszó", @@ -1843,6 +2155,7 @@ "LDAP_BaseDN_Description": "A teljesen minősített elkülönítő neve (DN) LDAP részfa szeretne keresni felhasználókat és csoportokat. Akkor adjunk hozzá annyi, amennyit akar; azonban minden csoport kell meghatározni ugyanabban a tartományban bázis a felhasználók számára, hogy tartozik hozzá. Ha megadod korlátozott felhasználói csoportok, csak azok a felhasználók, hogy tartoznak azok a csoportok lesznek hatálya alá. Javasoljuk, hogy adja meg a felső szint a LDAP fa, mint a domain bázis és használja keresési szűrő a hozzáférés szabályozására.", "LDAP_CA_Cert": "CA Cert", "LDAP_Connect_Timeout": "Kapcsolódási idő (ms)", + "LDAP_DataSync_AutoLogout": "Inaktivált felhasználók automatikus kijelentkeztetése", "LDAP_Default_Domain": "Alapértelmezett Domain", "LDAP_Default_Domain_Description": "Ha rendelkezésre áll, akkor az Alapértelmezett tartományt egyedi e-mailek létrehozására használják azon felhasználók számára, ahol az e-maileket nem importálták az LDAP-ból. Az e-mailt \"username @ default_domain\" vagy \"unique_id @ default_domain\" néven kell telepíteni.
      Példa: `rocket.chat`", "LDAP_Enable": "Engedélyezve", @@ -1873,7 +2186,7 @@ "LDAP_Login_Fallback_Description": "Ha az LDAP bejelentkezés sikertelen, próbálja meg bejelentkezni az alapértelmezett / helyi számlázási rendszerben. Segít, ha valamilyen okból leáll az LDAP.", "LDAP_Merge_Existing_Users": "Meglévő felhasználók egyesítése", "LDAP_Merge_Existing_Users_Description": "* Figyelmeztetés! * Ha egy LDAP-ból importál egy felhasználót, és ugyanaz a felhasználónév felhasználó van, az LDAP-információ és -jelszó a meglévő felhasználóra lesz beállítva.", - "LDAP_Port": "Kikötő", + "LDAP_Port": "Port", "LDAP_Port_Description": "Port eléréséhez LDAP. pl: `` 389` vagy 636` az LDAPS", "LDAP_Reconnect": "Kösse vissza", "LDAP_Reconnect_Description": "Próbáljon újból csatlakozni, ha a kapcsolat valamilyen oknál fogva megszakad a műveletek végrehajtása közben", @@ -1884,7 +2197,7 @@ "LDAP_Search_Size_Limit": "Keresési méretkorlát", "LDAP_Search_Size_Limit_Description": "A visszaadandó bejegyzések maximális száma
      ** Figyelmeztetés ** Ez a szám nagyobb, mint ** Search Page Size **", "LDAP_Sync_Now": "Háttér szinkronizálása most", - "LDAP_Sync_Now_Description": "A ** Háttér-szinkronizálást ** ** futtatja, nem pedig a ** Szinkronizálási időtartamot ** akkor is, ha a ** Háttér-szinkronizálás ** hamis.
      Ez a művelet aszinkron, kérjük, olvassa el a naplókat a folyamat", + "LDAP_Sync_Now_Description": "Ez a **Háttér szinkronizálás** műveletet indítja el most, anélkül, hogy megvárná a következő ütemezett szinkronizálást.\nEz a művelet aszinkron, további információkért tekintse meg a naplókat.", "LDAP_Sync_User_Avatar": "Szinkronizálás Avatar", "LDAP_Sync_User_Data_Channels_Admin": "Channel Adminisztrátor", "LDAP_Sync_User_Data_Channels_Filter": "Felhasználói csoport szűrő", @@ -1899,8 +2212,9 @@ "LDAP_User_Search_Filter": "Szűrő", "LDAP_User_Search_Filter_Description": "Ha meg van adva, csak azok a felhasználók, amelyek megfelelnek a szűrőt tenni, hogy jelentkezzen be. Ha nincs szűrő megadva, az összes felhasználó körén belül a megadott tartomány bázis lesz képes bejelentkezni.
      Pl Active Directory `MemberOf = cn = ROCKET_CHAT, ou = Általános Groups`.
      Pl OpenLDAP (bővíthető találat keresés) `ou: dn: = ROCKET_CHAT`.", "LDAP_User_Search_Scope": "terület", - "LDAP_Username_Field": "felhasználónév mező", + "LDAP_Username_Field": "Felhasználónév mező", "LDAP_Username_Field_Description": "Mely területen kerül felhasználásra * felhasználónév * az új felhasználók számára. Hagyja üresen használni a felhasználónevét tájékoztatni bejelentkezési oldalon.
      Használhatja sablon címkéket is, mint a `#{givenName}.#{sn}`.
      Az alapértelmezett érték `sAMAccountName`.", + "LDAP_Username_To_Search": "Felhasználónév a kereséshez", "Lead_capture_email_regex": "Lead capture email regex", "Lead_capture_phone_regex": "Lead capture phone regex", "Leave": "Elhagy", @@ -1919,7 +2233,10 @@ "Livechat_agents": "Livechat operátorok", "Livechat_Agents": "Operátorok", "Livechat_AllowedDomainsList": "Livechat engedélyezett domainek", + "Livechat_Appearance": "Livechat megjelenés", + "Livechat_close_chat": "Csevegés bezárása", "Livechat_Dashboard": "GYIK Portál", + "Livechat_enable_message_character_limit": "Üzenet karakterkorlátozás engedélyezése", "Livechat_enabled": "LiveChat engedélyezve", "Livechat_Facebook_API_Key": "OmniChannel API kulcs", "Livechat_Facebook_API_Secret": "OmniChannel API Secret", @@ -1928,16 +2245,20 @@ "Livechat_forward_open_chats_timeout": "Várakozási idő (másodpercben), a beszélgetések továbbításához", "Livechat_guest_count": "Guest Counter", "Livechat_Inquiry_Already_Taken": "A Livechat vizsgálat már megtörtént", + "Livechat_Installation": "Livechat telepítés", "Livechat_managers": "LiveChat kezelők", "Livechat_Managers": "Kezelők", + "Livechat_message_character_limit": "Livechat üzenet karakterkorlátozás", "Livechat_offline": "LiveChat nem elérhető", "Livechat_offline_message_sent": "Livechat offline üzenet elküldve", + "Livechat_OfflineMessageToChannel_enabled": "Livechat offline üzenetek küldése egy csatornára", "Livechat_online": "LiveChat elérhető", "Livechat_Queue": "Livechat sor", "Livechat_registration_form": "Regisztrációs űrlap", "Livechat_registration_form_message": "Regisztrációs űrlap üzenet", "Livechat_room_count": "GYIK szobák száma", "Livechat_Routing_Method": "Livechat útválasztási módszer", + "Livechat_status": "Livechat állapot", "Livechat_Take_Confirm": "Szeretne elvenni ezt az ügyfelet?", "Livechat_title": "LiveChat cím", "Livechat_title_color": "GYIK cím háttérszíne", @@ -1954,6 +2275,7 @@ "Livestream_url_incorrect": "Az élőszöveg URL-címe helytelen", "Load_Balancing": "Terhelés elosztás", "Load_more": "Továbbiak betöltése", + "Loading": "Betöltés", "Loading_more_from_history": "Továbbiak betöltése a történelemből", "Loading_suggestion": "Javaslatok betöltése...", "Loading...": "Betöltés...", @@ -1974,6 +2296,8 @@ "Log_View_Limit": "Naplónézetben Limit", "Logged_out_of_other_clients_successfully": "Kijelentkezett a más ügyfelek sikeresen", "Login": "Bejelentkezés", + "Login_Logs": "Bejelentkezési naplók", + "Login_Logs_Username": "Felhasználónév megjelenítése a sikertelen bejelentkezési kísérletek naplóiban", "Login_with": "Bejelentkezés %s segítségével", "Logistics": "Logisztika", "Logout": "Kijelentkezés", @@ -2039,13 +2363,18 @@ "Markdown_Parser": "Markdown Parser", "Markdown_SupportSchemesForLink": "Árleszállítás támogatási rendszereket link", "Markdown_SupportSchemesForLink_Description": "Vesszővel elválasztott listája az engedélyezett programok", + "Marketplace": "Piactér", "Marketplace_view_marketplace": "Piactér megtekintése", "Max_length_is": "A maximális hossza%s", "Max_number_incoming_livechats_displayed": "A várakozási sorban megjelenő elemet maximális száma", "Max_number_incoming_livechats_displayed_description": "(Opcionális) A Livechat várakozási sorban megjelenő elemek maximális száma.", + "Max_number_of_chats_per_agent": "Egyidejű csevegések maximális száma", + "Max_number_of_chats_per_agent_description": "Az ügynökök által egyidejűleg felvehető csevegések maximális száma", + "Max_number_of_uses": "A felhasználások maximális száma", "Maximum": "Maximális", "Media": "Média", "Medium": "Közepes", + "Members": "Tagok", "Members_List": "Tagok", "mention-all": "Mindent említ", "mention-all_description": "A @all említés engedélyezése", @@ -2066,7 +2395,7 @@ "Message_AllowEditing": "Engedélyezés Message szerkesztése", "Message_AllowEditing_BlockEditInMinutes": "Blokk Üzenet szerkesztése után (n) a jegyzőkönyvet", "Message_AllowEditing_BlockEditInMinutesDescription": "Írja 0 letiltja blokkoló.", - "Message_AllowPinning": "Engedélyezés Message rögzítéssel", + "Message_AllowPinning": "Üzenet kitűzésének engedélyezése", "Message_AllowPinning_Description": "Az üzenetek tűzve bármelyik csatornán.", "Message_AllowSnippeting": "Az üzenetek lekérdezésének engedélyezése", "Message_AllowStarring": "Engedélyezés Message Szereplők", @@ -2076,12 +2405,19 @@ "Message_Attachments": "Üzenet mellékletek", "Message_Attachments_GroupAttach": "Csoportos csatlakozó gombok", "Message_Attachments_GroupAttachDescription": "Ez csoportosítja az ikonokat egy kibontható menü alatt. Kevesebb képernyőteret vesz fel.", + "Message_Attachments_Thumbnails_Height": "A miniatűr maximális magassága (pixelben)", + "Message_Attachments_Strip_Exif": "EXIF metaadatok eltávolítása a támogatott fájlokból", "Message_Audio": "Hangüzenet", "Message_Audio_bitRate": "Hangüzenet bitráta", "Message_AudioRecorderEnabled": "Audio Recorder Enabled", "Message_AudioRecorderEnabled_Description": "Az \"audio / mp3\" fájlok elfogadott médiatípusnak kell lennie a \"Fájl feltöltése\" beállításai között.", + "Message_auditing": "Üzenet ellenőrzés", + "Message_auditing_log": "Üzenet ellenőrzési napló", "Message_BadWordsFilterList": "Add rossz szó a feketelistára", "Message_BadWordsFilterListDescription": "Add listája vesszővel elválasztott listája rossz szó, hogy kiszűrje", + "Message_BadWordsWhitelist": "Szavak eltávolítása a feketelistáról", + "Message_Characther_Limit": "Üzenet karakter limit", + "Message_Code_highlight": "Kódkiemelő nyelvek listája", "message_counter": "__counter__ üzenet", "message_counter_plural": "__counter__ üzenetek", "Message_DateFormat": "Dátum formátum", @@ -2098,6 +2434,7 @@ "Message_GroupingPeriodDescription": "Üzenetek lesznek csoportosítva a korábbi üzenetet, ha mindkettő ugyanannak a felhasználónak és az eltelt idő kevesebb volt, mint a tájékozott időt másodpercben.", "Message_HideType_au": "A \"Felhasználó hozzáadva\" üzenetek elrejtése", "Message_HideType_mute_unmute": "A \"Felhasználó elnémítva / letiltott\" üzenetek elrejtése", + "Message_HideType_r": "\"Room név megváltozott\" üzenetek elrejtése", "Message_HideType_ru": "A \"Felhasználó eltávolítva\" üzenetek elrejtése", "Message_HideType_uj": "A \"Felhasználói Csatlakozás\" üzenetek elrejtése", "Message_HideType_ul": "A \"Felhasználó szabadság\" üzenet elrejtése", @@ -2114,7 +2451,7 @@ "Message_Read_Receipt_Store_Users_Description": "Megjeleníti az egyes felhasználók olvasott beérkezéseit", "Message_removed": "üzenet eltávolított", "Message_sent_by_email": "Az e-mailben küldött üzenet", - "Message_ShowDeletedStatus": "Törölt állapota", + "Message_ShowDeletedStatus": "Törölt állapota megjelenítése", "Message_ShowEditedStatus": "Mutasd Szerkesztette állapota", "Message_ShowFormattingTips": "Itt található Formázási tippek", "Message_starring": "üzenet főszereplésével", @@ -2131,6 +2468,7 @@ "messages": "Üzenetek", "Messages": "Üzenetek", "messages_pruned": "üzenetek metszenek", + "Messages_sent": "Üzenetek elküldve", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Küldött üzenetek a Bejövő WebHook felteszik itt.", "Meta": "meta", "Meta_custom": "Egyéni metacímke", @@ -2147,30 +2485,40 @@ "meteor_status_try_now_offline": "Csatlakozzon újra", "meteor_status_try_now_waiting": "Próbálja most", "meteor_status_waiting": "Várakozás a szerver csatlakozásra,", + "Method": "Módszer", "Min_length_is": "A minimális hossza%s", "Minimum": "Minimális", "Minimum_balance": "Minimális egyenleg", + "minute": "perc", "minutes": "perc", "Mobex_sms_gateway_from_number": "Ból ből", "Mobex_sms_gateway_password": "Jelszó", "Mobex_sms_gateway_username": "Felhasználónév", "Mobile": "Mobil", + "mobile-download-file": "Fájlok letöltésének engedélyezése mobileszközökön", + "mobile-upload-file": "Fájlfeltöltés engedélyezése mobileszközökön", "Mobile_Push_Notifications_Default_Alert": "Mobil értesítések alapértelmezett figyelmeztetés", "Monday": "hétfő", "Mongo_storageEngine": "Mongo tároló motor", "Mongo_version": "Mongo verzió", + "MongoDB": "MongoDB", "MongoDB_Deprecated": "A MongoDB elavult", + "MongoDB_version_s_is_deprecated_please_upgrade_your_installation": "A MongoDB %s verziója elavult, kérjük frissítse a telepítést.", "Monitor_history_for_changes_on": "Figyelmeztetési előzmények a változásokhoz", + "Monthly_Active_Users": "Havi aktív felhasználók", "More": "Több", "More_channels": "Még több csatorna", "More_direct_messages": "Több közvetlen üzenetek", "More_groups": "Több privát csoport", - "More_unreads": "több unreads", + "More_unreads": "További olvasatlanok", + "Most_popular_channels_top_5": "Legnépszerűbb csatornák (Top 5)", "Move_beginning_message": "`%s` - Áthelyezés az üzenet elejére", "Move_end_message": "`%s` - Áthelyezés az üzenet végére", + "Move_queue": "Mozgatás a várakozási sorba", "Msgs": "Üzik", "multi": "több", "multi_line": "többsoros", + "Mute": "Némítás", "Mute_all_notifications": "Az összes értesítés elnémítása", "Mute_Focused_Conversations": "Némítás célzott beszélgetések", "Mute_Group_Mentions": "@all és @here említések némítása", @@ -2186,11 +2534,16 @@ "N_new_messages": "%s új üzenet", "Name": "Név", "Name_cant_be_empty": "A név nem lehet üres", - "Name_of_agent": "Az anyag neve", + "Name_of_agent": "Az operátor neve", "Name_optional": "Név (kötelező)", "Name_Placeholder": "Kérem írja be a nevét...", "Navigation_History": "navigációs története", + "Next": "Következő", + "Never": "Soha", + "New": "Új", "New_Application": "új alkalmazás", + "New_Canned_Response": "Új válasz sablon", + "New_Contact": "Új kapcsolat", "New_Custom_Field": "Új egyéni mező", "New_Department": "új Osztály", "New_discussion": "Új beszélgetés", @@ -2203,25 +2556,36 @@ "New_messages": "Új üzenet", "New_password": "Új jelszó", "New_Password_Placeholder": "Adjon meg új jelszót ...", + "New_Priority": "Új prioritás", "New_role": "Új szerep", "New_Room_Notification": "Új szoba értesítés", + "New_Tag": "Új címke", "New_Trigger": "Új trigger", + "New_Unit": "Új egység", + "New_users": "Új felhasználók", "New_version_available_(s)": "Új verzió elérhető (%s)", "New_videocall_request": "Új videohívási kérelem", "New_visitor_navigation": "Új navigáció: __history__", "Newer_than": "Újabbak mint", "Newer_than_may_not_exceed_Older_than": "\"Újabbak, mint\" nem haladhatja meg a \"Régebbiek\"", + "Nickname": "Becenév", + "Nickname_Placeholder": "Add meg a beceneved...", "No": "Nem", "No_available_agents_to_transfer": "Nem áll rendelkezésre átruházható anyagok", + "No_Canned_Responses": "Nincsenek válasz sablonok", + "No_Canned_Responses_Yet": "Még nincsenek válasz sablonok", "No_channel_with_name_%s_was_found": "Nem található \"%s\" nevű csatorna!", "No_channels_yet": "Még nem vagy tagja egy csatornának sem.", + "No_data_found": "Nem találhatóak adatok", "No_direct_messages_yet": "Még nem kezdeményeztél beszélgetést", "No_discussions_yet": "Még nincsenek beszélgetések", "No_emojis_found": "Nem találhatóak hangulatjelek", "No_Encryption": "Nincs titkosítás", + "No_files_left_to_download": "Nincs letölthető fájl", "No_group_with_name_%s_was_found": "Nem található \"%s\" nevű privát csoport!", "No_groups_yet": "Még nincsenek privát csoportjaid", "No_integration_found": "Nincs integráció a megadott id alapján.", + "No_Limit": "Nincs korlát", "No_livechats": "Nincsenek livechats.", "No_mentions_found": "Nincs említés talált", "No_messages_yet": "Nincs üzenet még", @@ -2256,7 +2620,9 @@ "Notifications_Preferences": "Értesítések beállításai", "Notifications_Sound_Volume": "Értesítések hangereje", "Notify_active_in_this_room": "Értesítsen aktív felhasználókat ebben a szobában", - "Notify_all_in_this_room": "Értesíti az összes ebben a szobában", + "Notify_all_in_this_room": "Értesítse az összes tagot ebben a szobában", + "Default_Server_Timezone": "Szerver időzóna", + "Default_Custom_Timezone": "Egyéni időzóna", "Num_Agents": "# Ügynök", "Number_of_events": "Események száma", "Number_of_messages": "Üzenetek száma", @@ -2279,6 +2645,8 @@ "Offline_Mention_All_Email": "Az összes e-mail tárgyának említése", "Offline_Mention_Email": "Megemlítés e-mail tárgya", "Offline_message": "Offline üzenet", + "Offline_Message": "Offline üzenet", + "Offline_messages": "Offline üzenetek", "Offline_success_message": "Offline siker üzenet", "Offline_unavailable": "Offline nem érhető el", "Old Colors": "Régi színek", @@ -2288,7 +2656,9 @@ "online": "online", "Online": "Elérhető", "Only_authorized_users_can_write_new_messages": "Csak az engedélyezett felhasználók írhatnak új üzeneteket", + "Only_authorized_users_can_react_to_messages": "Csak a bejelentkezett felhasználók reagálhatnak az üzenetekre", "Only_from_users": "Csak szúrja be ezeket a felhasználók tartalmát (üresen hagyja az összes felhasználót)", + "Only_Members_Selected_Department_Can_View_Channel": "Csak a kiválasztott részleg tagjai láthatják a csatornán zajló csevegéseket", "Only_On_Desktop": "Asztali mód (csak az asztali gépen lévő Enter billentyűvel küldi el)", "Only_you_can_see_this_message": "Csak akkor látni ezt az üzenetet", "Oops_page_not_found": "Hoppá, az oldal nem található", @@ -2323,32 +2693,47 @@ "OS_Uptime": "Operációs rendszer indítása óta eltelt idő", "Other": "Más", "others": "mások", + "Others": "Egyebek", "OTR": "OTR", "OTR_is_only_available_when_both_users_are_online": "OTR csak ha mindkét online", + "Out_of_seats": "Nincs több hely", "Outgoing_WebHook": "Kimenő webhook", "Outgoing_WebHook_Description": "Készítsen adatokat a Rocket.Chatból valós időben.", + "Output_format": "Kimeneti formátum", "Override_URL_to_which_files_are_uploaded_This_url_also_used_for_downloads_unless_a_CDN_is_given": "Felülírása URL, amelyre fájlok feltöltése. Ez az URL is használható letöltések hacsak CDN kap", "Page_title": "Lap cím", "Page_URL": "Az oldal URL-je", + "Pages": "Oldalak", "Parent_channel_doesnt_exist": "A Channel nem létezik.", + "Participants": "Résztvevők", "Password": "Jelszó", "Password_Change_Disabled": "Az Rocket.Chat rendszergazda letiltotta a változó jelszavakat", + "Password_Changed_Email_Subject": "[Site_Name] - A jelszó megváltozott", + "Password_changed_section": "A jelszó megváltozott", "Password_changed_successfully": "Jelszó sikeresen módosítva", + "Password_History": "Jelszó előzmények", + "Password_History_Amount": "Jelszó előzmények hossza", "Password_Policy": "Jelszószabályzat", + "Password_to_access": "Jelszó a hozzáféréshez", + "Passwords_do_not_match": "A jelszavak nem egyeznek", "Past_Chats": "Korábbi beszélgetések", + "Paste_here": "Beillesztés ide...", + "Paste": "Beillesztés", "Payload": "Hasznos teher", + "PDF": "PDF", "Peer_Password": "Peer jelszó", "People": "Emberek", "Permalink": "Permalink", "Permissions": "Engedélyek", - "Personal_Access_Tokens": "Személyes hozzférési token", + "Personal_Access_Tokens": "Személyes hozzáférési token", + "Phone": "Telefon", "Phone_number": "Telefonszám", "Pin": "Pin", "Pin_Message": "Rögzít", "pin-message": "Rögzít", "pin-message_description": "Engedély a csatorna üzenetének beillesztésére", "Pinned_a_message": "Üzenet rögzítve:", - "Pinned_Messages": "Rögzített üzenetek", + "Pinned_Messages": "Kitűzött üzenetek", "PiwikAdditionalTrackers": "További Piwik webhelyek", "PiwikAdditionalTrackers_Description": "Adj meg további Piwik webhely URL-eket és webhelyazonosítóit a következő formátumban, ha követheti szeretnéd ugyanazokat az adatokat a különböző webhelyekhez: [{ \"trackerURL\" : \"https://my.piwik.domain2/\", \"siteId\" : 42 }, { \"trackerURL\" : \"https://my.piwik.domain3/\", \"siteId\" : 15 }]", "PiwikAnalytics_cookieDomain": "Minden aldomain", @@ -2397,15 +2782,20 @@ "Preparing_list_of_messages": "Az üzenetek listájának előkészítése", "Preparing_list_of_users": "A felhasználó listájának előkészítése", "Presence": "Jelenlét", + "Preview": "Előnézet", "preview-c-room": "Nyilvános csatorna megtekintése", "preview-c-room_description": "Engedély a nyilvános csatorna tartalmának megtekintéséhez a csatlakozás előtt", "Previous_month": "Előző hónap", "Previous_week": "Előző hét", + "Priorities": "Prioritások", + "Priority": "Prioritás", + "Priority_removed": "Prioritás eltávolítva", "Privacy": "Adatvédelem", "Privacy_Policy": "Adatvédelmi irányelvek", "Private": "Magán", "Private_Channel": "Privát csatorna", - "Private_Group": "Private Csoport", + "Private_Chats": "Privát csevegések", + "Private_Group": "Privát csoport", "Private_Groups": "Privát csoportok", "Private_Groups_list": "List of Private Groups", "Private_Team": "Privát csapat", @@ -2415,6 +2805,9 @@ "Profile_picture": "Profilkép", "Profile_saved_successfully": "Profil sikeresen mentve", "Prometheus": "Prométheusz", + "Prometheus_API_User_Agent": "API: User Agent követése", + "Prometheus_Garbage_Collector": "NodeJS GC gyűjtése", + "Prometheus_Reset_Interval": "Visszaállítási időköz (ms)", "Protocol": "Protokoll", "Prune": "Aszalt szilva", "Prune_finished": "Prune kész", @@ -2429,10 +2822,12 @@ "Public": "Nyilvános", "Public_Channel": "Nyilvános csatorna", "Public_Community": "Nyilvános közösség", + "Public_URL": "Nyilvános URL", "Purchase_for_free": "Vedd meg INGYEN", "Purchase_for_price": "Vedd", "Purchased": "Megvásárolt", "Push": "Nyom", + "Push_Notifications": "Push értesítések", "Push_apn_cert": "APN Cert", "Push_apn_dev_cert": "APN Dev Cert", "Push_apn_dev_key": "APN Dev Key", @@ -2450,7 +2845,8 @@ "Push_show_username_room": "-Es csatorna / csoport / felhasználónév az értesítési", "Push_test_push": "Teszt", "Query": "kérdés", - "Query_description": "További feltételek meghatározására, hogy mely felhasználók küldeni a levelet. Feliratkozott felhasználók automatikusan eltávolításra kerülnek a lekérdezés. Meg kell egy érvényes JSON. Példa: \"{\" CreatedAt \": {\" $ gt \": {\" $ date \":\" 2015-01-01T00: 00: 00.000Z \"}}}\"", + "Query_description": "További feltételek meghatározására, hogy mely felhasználók küldeni a levelet. Feliratkozott felhasználók automatikusan eltávolításra kerülnek a lekérdezés. Meg kell egy érvényes JSON. Példa: \"{\"createdAt\":{\"$gt\":{\"$date\": \"2015-01-01T00:00:00.000Z\"}}}\"", + "Query_is_not_valid_JSON": "A lekérdezés nem érvényes JSON", "Queue": "sorban áll", "quote": "idézet", "Quote": "Idézet", @@ -2473,7 +2869,7 @@ "Receive_alerts": "Értesítések fogadása", "Receive_Group_Mentions": "Fogadja a @all és a @here utalásokat", "Recent_Import_History": "Legutóbbi importálás történet", - "Record": "Rekord", + "Record": "Bejegyzés", "Redirect_URI": "Átirányítás URI", "Refresh": "Frissítés", "Refresh_keys": "Frissítés kulcsok", @@ -2502,7 +2898,7 @@ "Reload": "Reload", "Reload_Pages": "Oldalak újratöltése", "Remove": "Eltávolít", - "Remove_Admin": "Távolítsuk Admin", + "Remove_Admin": "Admin eltávolítása", "Remove_as_leader": "Vegye le a vezetőt", "Remove_as_moderator": "Távolítsuk el a moderátor", "Remove_as_owner": "Távolítsuk el a tulajdonos", @@ -2529,6 +2925,7 @@ "Report_sent": "Report küldött", "Report_this_message_question_mark": "Üzenet jelentése?", "Reporting": "Jelentés", + "required": "szükséges", "Require_all_tokens": "Szükséges minden token", "Require_any_token": "Kérjen minden tokenet", "Require_password_change": "Igényel jelszócsere", @@ -2574,6 +2971,7 @@ "Return_to_home": "Vissza a főoldalra", "Return_to_previous_page": "Vissza az előző oldalra", "Robot_Instructions_File_Content": "Robots.txt fájl tartalma", + "No_Referrer": "Nincs ajánló", "Rocket_Chat_Alert": "Rockat.Chat figyelmeztetés", "Role": "Szerep", "Role_Editing": "szerep szerkesztése", @@ -2607,7 +3005,7 @@ "Room_type_of_default_rooms_cant_be_changed": "Ez az alapértelmezett hely, és a típus nem módosítható, kérjük, forduljon a rendszergazdájához.", "Room_unarchived": "szoba archivált", "Room_uploaded_file_list": "Fájlok", - "Room_uploaded_file_list_empty": "Nincs fájl is elérhető.", + "Room_uploaded_file_list_empty": "Nincsenek elérhető fájlok.", "Rooms": "Szobák", "Routing": "Útválasztás", "Run_only_once_for_each_visitor": "Csak egyszer fusson látogatónként", @@ -2647,10 +3045,14 @@ "SAML_Default_User_Role_Description": "Több szerepkört is megadhat, vesszővel elválasztva.", "SAML_Role_Attribute_Name": "Szerepkör attribútum neve", "SAML_Section_1_User_Interface": "Felhasználói felület", + "SAML_Section_2_Certificate": "Tanúsítvány", + "SAML_Section_3_Behavior": "Viselkedés", + "SAML_Section_4_Roles": "Szerepkörök", + "SAML_Section_6_Advanced": "Haladó", "Saturday": "szombat", "Save": "Mentés", "Save_changes": "Változtatások mentése", - "Save_Mobile_Bandwidth": "Sávszélesség kímélő mód", + "Save_Mobile_Bandwidth": "Mobil sávszélesség kímélő mód", "Save_to_enable_this_action": "Mentse, hogy ezt a műveletet", "Save_To_Webdav": "Mentés a WebDAV-ba", "save-others-livechat-room-info": "Mások mentése a Livechat szobapiacon", @@ -2677,10 +3079,14 @@ "seconds": "másodperc", "Secret_token": "titkos token", "Security": "Biztonság", - "Select_a_department": "Válasszon egy osztály", + "See_full_profile": "Teljes profil megtekintése", + "Select_a_department": "Válasszon részleget", + "Select_a_room": "Válasszon ki egy szobát", "Select_a_user": "Válasszon ki egy felhasználót", "Select_an_avatar": "Válassz képet", "Select_an_option": "Válassz egy lehetőséget", + "Select_at_least_one_user": "Válasszon legalább egy felhasználót", + "Select_at_least_two_users": "Válasszon legalább két felhasználót", "Select_department": "Válasszon egy osztály", "Select_file": "File kiválasztása", "Select_role": "Válasszon egy szerepkört", @@ -2690,7 +3096,7 @@ "Select_users": "felhasználók kiválasztása", "Selected_agents": "Válogatott szerek", "Selecting_users": "Felhasználók kiválasztása", - "Send": "Elküld", + "Send": "Küldés", "Send_a_message": "Üzenetet küldeni", "Send_a_test_mail_to_my_user": "Küldj egy teszt mailt a használati", "Send_a_test_push_to_my_user": "Küldj egy teszt push a használati", @@ -2716,6 +3122,7 @@ "Sent_an_attachment": "Küldött egy mellékletet", "Served_By": "Szolgált", "Server": "Kiszolgáló", + "Server_File_Path": "Szerver fájl elérési útvonal", "Server_Info": "Szerverinformáció", "Server_Type": "Szerver típusa", "Service": "Szolgáltatás", @@ -2737,7 +3144,10 @@ "Setup_Wizard": "Beállítási varázslót", "Setup_Wizard_Info": "Útmutatunk az első adminisztrátor felhasználójának beállításához, a szervezet konfigurálásához és a szerver regisztrációjához, hogy ingyenes push-értesítéseket kapjunk és így tovább.", "Share_Location_Title": "Megosztás helye?", + "Share_screen": "Képernyő megosztása", + "Sharing": "Megosztás", "Shared_Location": "Megosztott hely", + "Shared_Secret": "Megosztott titkos kulcs", "Should_be_a_URL_of_an_image": "Kell egy kép URL-je.", "Should_exists_a_user_with_this_username": "A felhasználó már léteznie kell.", "Show_agent_email": "Az ügynök e-mailjének megjelenítése", @@ -2757,7 +3167,9 @@ "Show_room_counter_on_sidebar": "Mutasd a helyiségszámlálót az oldalsávon", "Show_Setup_Wizard": "A telepítővarázsló megjelenítése", "Show_the_keyboard_shortcut_list": "Mutassa be a billentyűparancsok listáját", + "Show_video": "Videó megjelenítése", "Showing_archived_results": "

      A következő %s archivált eredmények

      ", + "Showing_online_users": null, "Showing_results": "

      %s eredmény megjelenítve

      ", "Sidebar": "Oldalsáv", "Sidebar_list_mode": "Oldalsáv csatornalista mód", @@ -2799,6 +3211,7 @@ "Smarsh_MissingEmail_Email_Description": "Az e-mail, amelyet egy felhasználói fiók megjelenítéséhez használnak, amikor az e-mail címük hiányzik, általában a bot-fiókokkal történik.", "Smarsh_Timezone": "Smarsh időzóna", "Smileys_and_People": "Hangulatjelek és emberek", + "SMS": "SMS", "SMS_Enabled": "SMS-ek bekapcsolva", "SMTP": "SMTP", "SMTP_Host": "SMTP Host", @@ -2815,6 +3228,7 @@ "Social_Network": "Közösségi háló", "Sorry_page_you_requested_does_not_exist_or_was_deleted": "Sajnáljuk, a kért oldal nem létezik, vagy törölték!", "Sort": "Rendezés", + "Sort_By": "Rendezés", "Sort_by_activity": "Rendezés tevékenység szerint", "Sound": "Hang", "Sound_File_mp3": "Hangfájl (mp3)", @@ -2838,7 +3252,8 @@ "Statistics": "Statisztika", "Statistics_reporting": "Küldje statisztikák Rocket.Chat", "Statistics_reporting_Description": "Elküldi a statisztikát, akkor segítenek azonosítani, hogy hány példányban Rocket.Chat telepítettek, valamint, hogy milyen jó a rendszer úgy viselkedik, így tudjuk tovább javítani. Ne aggódj, a felhasználói információ nem küldött, és az összes információt kapunk bizalmasan kezeljük.", - "Stats_Active_Users": "aktív felhasználók", + "Stats_Active_Users": "Aktivált felhasználók", + "Stats_App_Users": "Rocket.Chat alkalmazás felhasználók", "Stats_Avg_Channel_Users": "Átlagos Channel Felhasználók", "Stats_Avg_Private_Group_Users": "Átlagos Private Csoport Felhasználók", "Stats_Away_Users": "Idegenben felhasználók", @@ -2863,11 +3278,11 @@ "Stats_Total_Messages_Livechat": "Total Messages in Livechats", "Stats_Total_Messages_PrivateGroup": "Összes üzenet privát csoportokban", "Stats_Total_Outgoing_Integrations": "Összes kimenő integráció", - "Stats_Total_Private_Groups": "Összesen Private Groups", + "Stats_Total_Private_Groups": "Összes privát csoport", "Stats_Total_Rooms": "Összesen szobák", "Stats_Total_Uploads": "Összes feltöltés", "Stats_Total_Uploads_Size": "Összes feltöltés mérete", - "Stats_Total_Users": "felhasználók", + "Stats_Total_Users": "Összes felhasználó", "Status": "Állapot", "StatusMessage": "Állapot üzenet", "StatusMessage_Change_Disabled": "A Rockat.Chat adminisztrátor letiltotta az állapot üzenetek módosítását", @@ -2891,7 +3306,7 @@ "Sunday": "vasárnap", "Support": "Támogatás", "Survey": "Felmérés", - "Survey_instructions": "Szavazz minden kérdésre az Ön elégedettsége, 1 ami azt jelenti, teljesen elégedetlen, és 5 ami azt jelenti, hogy teljesen elégedett.", + "Survey_instructions": "Értékelje a kérdéseket elégedettségétől függően. (1: teljesen elégedetlen, 5: teljesen elégedett)", "Symbols": "Szimbólumok", "Sync": "Szinkronizálás", "Sync / Import": "Szinkronizálás / Importálás", @@ -2901,19 +3316,39 @@ "Sync_Users": "Szinkronizálás felhasználók", "System_messages": "Rendszerüzenetek", "Tag": "Címke", + "Tags": "Címkék", + "Tag_removed": "Címke eltávolítva", + "Tag_already_exists": "A címke már létezik", "Take_it": "Vedd el!", "Target user not allowed to receive messages": "A célfelhasználó nem fogadhat üzeneteket", "TargetRoom": "Target szoba", "TargetRoom_Description": "Az a hely, ahol az üzenetek elküldésre kerülnek, amelyek ennek az eseménynek a következményei. Csak egy célteret engedélyezhet és léteznie kell.", + "Team_Add_existing": "Meglévő hozzáadása", + "Team_Channels": "Csapat Channel", + "Team_Delete_Channel_modal_content_danger": "Ez nem vonható vissza.", + "Team_Info": "Csapat információ", + "Team_Remove_from_team": "A csapatból való eltávolítás", "Team": "Csapat", + "Teams": "Csapatok", + "Teams_Errors_Already_exists": "A `__name__` csapat már létezik.", + "Teams_Errors_team_name": "Nem használhatod a \"__name__\" szót csapatnévként.", "Teams_New_Name_Label": "Név", + "Teams_leave": "Csapat elhagyása", + "Teams_left_team_successfully": "Sikeresen elhagyta a csapatot", + "Teams_members": "Csapatok tagjai", + "Teams_New_Add_members_Label": "Tagok hozzáadása", "Teams_New_Broadcast_Description": "Csak az engedélyezett felhasználók írhatnak új üzeneteket, de a többi felhasználó is tud válaszolni", "Teams_New_Description_Label": "Téma", "Teams_New_Encrypted_Label": "Titkosított", "Teams_New_Private_Label": "Magán", + "Teams_Public_Team": "Nyilvános csapat", "Teams_Private_Team": "Privát csapat", + "Teams_removing__username__from_team": "Ön eltávolítja __username__-t ebből a csapatból", + "Teams_Select_a_team": "Válasszon egy csapatot", + "Teams_Search_teams": "Keresés a csapatok között", "Teams_New_Read_only_Label": "Csak olvasható", "Technology_Services": "Technológiai szolgáltatások", + "Terms": "Feltételek", "Test_Connection": "kapcsolat tesztelése", "Test_Desktop_Notifications": "Asztali értesítések tesztelése", "Texts": "Szövegek", @@ -2921,7 +3356,8 @@ "Thank_you_for_your_feedback": "Köszönjük a visszajelzést", "The_application_name_is_required": "Az alkalmazás neve szükséges", "The_channel_name_is_required": "A csatorna neve szükséges", - "The_emails_are_being_sent": "Az e-mailek küldésére is.", + "The_emails_are_being_sent": "Az e-maileket elküldtük.", + "The_empty_room__roomName__will_be_removed_automatically": "Az üres szoba __roomName__ automatikusan eltávolításra kerül.", "The_field_is_required": "A %s mezőben van szükség.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "A kép átméretezés nem fog működni, mert nem tudjuk észlelni ImageMagick vagy GraphicsMagick szerveren telepítve.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "Az üzenet egy beszélgetés, amit nem fogsz tudni visszaállítani!", @@ -2960,6 +3396,7 @@ "theme-color-rc-color-error-light": "Hiba jelzőfény", "theme-color-rc-color-link-active": "Active link", "theme-color-rc-color-primary": "Elsődleges", + "theme-color-rc-color-primary-background": "Elsődleges háttér", "theme-color-rc-color-primary-dark": "Elsődleges sötét", "theme-color-rc-color-primary-darkest": "Elsődleges sötét", "theme-color-rc-color-primary-light": "Elsődleges fény", @@ -2993,6 +3430,7 @@ "There_are_no_users_in_this_role": "Nincsenek felhasználók ebben a szerepben.", "There_is_one_or_more_apps_in_an_invalid_state_Click_here_to_review": "Egy vagy több alkalmazás érvénytelen állapotban van. Kattintson ide a felülvizsgálathoz.", "This_agent_was_already_selected": "Az operátort már kiválasztották", + "This_cant_be_undone": "Ez nem visszavonható.", "This_conversation_is_already_closed": "Ez a beszélgetés már lezárult.", "This_email_has_already_been_used_and_has_not_been_verified__Please_change_your_password": "Ez az e-mail már felhasználták, és még nem igazolták. Kérjük, változtassa meg a jelszavát.", "This_is_a_desktop_notification": "Ez egy asztali értesítés", @@ -3006,8 +3444,10 @@ "Thread_message": "A (z) * __username__ * üzenetéhez kommentált: _ __msg__ _", "Threads": "Témák", "Thursday": "csütörtök", + "Time_in_minutes": "Idő percben", "Time_in_seconds": "Az idő másodpercben", "Timeouts": "Szünetek", + "Timezone": "Időzóna", "Title": "Cím", "Title_bar_color": "Cím sáv színe", "Title_bar_color_offline": "Cím sáv színe nem elérhető", @@ -3019,6 +3459,7 @@ "To_users": "Ahhoz, hogy a felhasználók", "Today": "Ma", "Toggle_original_translated": "Váltás az eredeti / lefordítva", + "toggle-room-e2e-encryption_description": "Engedély az e2e titkosítási szoba átkapcsolására", "Token": "Token", "Token_Access": "Token hozzáférés", "Token_Controlled_Access": "Token ellenőrzött hozzáférés", @@ -3034,14 +3475,19 @@ "Tokens_Required_Input_Placeholder": "Jelvények névsora", "Topic": "Téma", "Total": "Összes", + "Total_abandoned_chats": "Összes elhagyott csevegés", "Total_conversations": "Összes beszélgetés", "Total_Discussions": "Összes beszélgetés", "Total_messages": "Összes üzenet", "Total_Threads": "Összes téma", "Total_visitors": "Össze látogató", + "TOTP Invalid [totp-invalid]": "Kód vagy jelszó érvénytelen", + "totp-invalid": "Kód vagy jelszó érvénytelen", + "Transcript": "Átirat", "Transcript_Enabled": "Kérdezd meg a látogatót, hogy szeretne-e egy átiratát lezárt csevegés után", "Transcript_message": "Üzenet a mutatóhoz, amikor a transzkriptről kérdez", "Transcript_of_your_livechat_conversation": "Az élő chat-beszélgetés átiratai.", + "Transcript_Request": "Átirat kérés", "transfer-livechat-guest": "Livechat vendégek továbbítása", "Translate": "Fordítás", "Translated": "Lefordított", @@ -3058,10 +3504,14 @@ "Two Factor Authentication": "Kétlépcsős azonosítás", "Two-factor_authentication": "Kétlépcsős azonosítás", "Two-factor_authentication_disabled": "Kétlépcsős azonosítás letiltva", + "Two-factor_authentication_email": "Kétfaktoros hitelesítés e-mailben", + "Two-factor_authentication_email_is_currently_disabled": "Az e-mailen keresztüli kétfaktoros hitelesítés jelenleg le van tiltva", "Two-factor_authentication_enabled": "Kétlépcsős azonosítás engedélyezve", "Two-factor_authentication_is_currently_disabled": "A kétlépcsős azonosítás jelenleg letiltott", "Two-factor_authentication_native_mobile_app_warning": "FIGYELMEZTETÉS: Miután engedélyezte ezt, a natív mobilalkalmazásokra (Rocket.Chat +) nem tud bejelentkezni a jelszó használatával, amíg nem hajtják végre a 2FA-t.", "Type": "Típus", + "typing": "ír", + "Types": "Típusok", "Type_your_email": "Írja be az e-mail", "Type_your_job_title": "Írja be a munkakört", "Type_your_message": "Írja be az üzenetet", @@ -3082,15 +3532,19 @@ "unarchive-room": "Unarchive szoba", "unarchive-room_description": "A csatornák unarchiválásának engedélyezése", "Unblock_User": "Felhasználó feloldása", + "Uncheck_All": "Összes kijelölés eltávolítása", "Unfavorite": "Eltávolítás a kedvencekből", "Unfollow_message": "Üzenet követés leállítása", "Unignore": "mellőzésének", "Uninstall": "Eltávolítás", + "Unknown_Import_State": "Ismeretlen importálási állapot", + "Unlimited": "Korlátlan", + "Unmute": "Némítás feloldása", "Unmute_someone_in_room": "Unmute valaki a szobában", "Unmute_user": "Felhasználó némításának visszavonása", "Unnamed": "Névtelen", "Unpin": "Rögzítés feloldása", - "Unpin_Message": "Unpin Message", + "Unpin_Message": "Kitűzött üzenet eltávolítása", "Unread": "Nem olvasott", "Unread_Count": "Olvasatlan gróf", "Unread_Count_DM": "Olvasatlan számlálás a közvetlen üzenetekhez", @@ -3100,11 +3554,16 @@ "Unread_Rooms_Mode": "Olvasatlan szobák Mode", "Unread_Tray_Icon_Alert": "Nem olvasott tálca ikon figyelmeztetés", "Unstar_Message": "Csillag eltávolítása", + "Unmute_microphone": "Mikrofon némítás feloldása", "Update": "Frissítés", + "Update_EnableChecker": "A Frissítésellenőrző engedélyezése", + "Update_every": "Frissítés minden", "Update_LatestAvailableVersion": "Frissítés a legújabb elérhető verzióra", "Update_to_version": "Frissítés __version__ verzióra", "Update_your_RocketChat": "Frissítsd a Rocket.Chat-et", "Updated_at": "Frissítve:", + "Upload": "Feltöltés", + "Uploads": "Feltöltések", "Upload_app": "Alkalmazás feltöltése", "Upload_file_description": "Fájl leírása", "Upload_file_name": "Fájl név", @@ -3115,13 +3574,19 @@ "Uploading_file": "Fájl feltöltése ...", "Uptime": "Indítás óta eltelt idő", "URL": "URL", + "URL_room_hash": "Szobanév hash engedélyezése", "URL_room_prefix": "URL szoba előtag", + "Usage": "Használat", + "Use": "Használ", "Use_account_preference": "Használja a fiók beállításokat", "Use_Emojis": "Hangulatjelek használata", "Use_Global_Settings": "Használja a globális beállításokat", "Use_initials_avatar": "Használd a felhasználónév kezdőbetűi", "Use_minor_colors": "Használjon kisebb színpalettát (az alapértelmezések örökölnek a nagyobb színeket)", + "Use_Server_configuration": "Szerver konfiguráció használata", "Use_service_avatar": "%s profilkép használata", + "Use_this_response": "Használja ezt a választ", + "Use_response": "Válasz használata", "Use_this_username": "Használd ezt a felhasználónevet", "Use_uploaded_avatar": "Feltöltött profilkép használata", "Use_url_for_avatar": "Avatar URL megadása", @@ -3132,6 +3597,7 @@ "User__username__is_now_a_leader_of__room_name_": "__username__ mostantól a __room_name__ csatorna vezetője", "User__username__is_now_a_moderator_of__room_name_": "__username__ mostantól moderátor a __room_name__ csatornán", "User__username__is_now_a_owner_of__room_name_": "__username__ mostantól tulajdonosa a __room_name__ csatornának", + "User__username__muted_in_room__roomName__": "__username__ felhasználó elnémítva a __roomName__ szobában", "User__username__removed_from__room_name__leaders": "__username__ már nem vezetője a __room_name__ csatornának", "User__username__removed_from__room_name__moderators": "__username__ már nem moderátora a __room_name__ csatornának", "User__username__removed_from__room_name__owners": "__username__ már nem tulajdonosa a __room_name__ csatornának", @@ -3177,6 +3643,7 @@ "User_sent_a_message_to_you": "__username__üzenetet küldött neked", "user_sent_an_attachment": "__user__ csatoltat küldött", "User_Settings": "felhasználói beállítások", + "User_started_a_new_conversation": "__username__ új beszélgetést indított", "User_unmuted_by": "Felhasználói __user_unmuted__ hangjának újbóli által __user_by__.", "User_unmuted_in_room": "A felhasználó elnémítása megszüntetve a szobában", "User_updated_successfully": "Felhasználó sikeresen frissítve", @@ -3218,8 +3685,13 @@ "Users must use Two Factor Authentication": "A felhasználóknak kétlépcsős azonosítást kell használniuk", "Users_added": "A felhasználók hozzáadásra kerültek", "Users_in_role": "Felhasználók szerepe", + "Uses": "Felhasználások", + "Uses_left": "Maradék felhasználások", "UTF8_Names_Slugify": "UTF8 nevek Slugify", + "Videocall_enabled": "Videohívás engedélyezve", "Validate_email_address": "Érvényesítse az e-mail címet", + "Value_messages": "__value__ üzenetek", + "Value_users": "__value__ felhasználók", "Verification": "Igazolás", "Verification_Description": "A következő helyőrzőket használhatja:
      • [Verification_Url] az ellenőrző URL-hez.
      • [név], [fname], [lname] a felhasználó teljes neve, utóneve vagy vezetékneve számára.
      • [e-mail] a felhasználó e-mailje számára.
      • [Site_Name] és [Site_URL] az Alkalmazás neve és URL címekhez.
      ", "Verification_Email": "Kattintson a ittfiókjának ellenőrzésére.", @@ -3230,12 +3702,13 @@ "Verify": "Ellenőrizze", "Verify_your_email": "Ellenőrizd az e-mail címet", "Version": "Változat", + "Version_version": "Verzió __version__", "Video Conference": "Videó konferencia", "Video_Chat_Window": "Videó Chat", "Video_Conference": "Videó konferencia", "Video_message": "Videóüzenet", "Videocall_declined": "Videóhívás visszautasítva.", - "Videocall_enabled": "Videohívás engedélyezve", + "Video_and_Audio_Call": "Videó- és hanghívás", "Videos": "Videók", "View_All": "Összes megtekintése", "View_Logs": "Naplók megtekintése", @@ -3245,8 +3718,10 @@ "view-broadcast-member-list": "View Member List a Broadcast Room-ben", "view-c-room": "Nyilvános csatorna megtekintése", "view-c-room_description": "Engedély a nyilvános csatornák megtekintéséhez", + "view-canned-responses": "Sablon válaszok megtekintése", "view-d-room": "Közvetlen üzenetek megtekintése", "view-d-room_description": "Engedély a közvetlen üzenetek megtekintéséhez", + "View_full_conversation": "Teljes beszélgetés megtekintése", "view-full-other-user-info": "Teljes többi felhasználói információ megtekintése", "view-full-other-user-info_description": "Engedély a többi felhasználó teljes profiljának megtekintésére, beleértve a fiók létrehozásának dátumát, az utolsó bejelentkezést stb.", "view-history": "Nézetelőzmények", @@ -3264,6 +3739,8 @@ "view-livechat-queue": "Livechat várólista megtekintése", "view-livechat-rooms": "Tekintse meg a Livechat szobákat", "view-livechat-rooms_description": "Engedély az egyéb élő csatornák megtekintéséhez", + "view-livechat-webhooks_description": "Az livechat webhook-ok megtekintésének engedélyezése", + "view-livechat-unit": "Livechat egységek megtekintése", "view-logs": "Naplók megtekintése", "view-logs_description": "Engedély a kiszolgáló naplóinak megtekintéséhez", "view-other-user-channels": "Egyéb felhasználói csatornák megtekintése", @@ -3285,7 +3762,11 @@ "Visible": "Látható", "Visit_Site_Url_and_try_the_best_open_source_chat_solution_available_today": "Látogasson el a __Site_URL__ oldalra, és próbálja ki a ma elérhető legjobb nyílt forráskódú chat megoldást! ", "Visitor": "Látogató", + "Visitor_Email": "Látogató E-mail", "Visitor_Info": "Látogatói információ", + "Visitor_message": "Látogatói üzenetek", + "Visitor_Name": "Látogató neve", + "Visitor_Name_Placeholder": "Kérjük, adja meg a látogató nevét...", "Visitor_Navigation": "visitor Navigation", "Visitor_page_URL": "Látogatói oldal URL", "Visitor_time_on_site": "Látogató az oldalon eltöltött idő", @@ -3303,6 +3784,7 @@ "Webdav_Server_URL": "WebDAV-kiszolgáló elérési URL-je", "Webdav_Username": "WebDAV felhasználónév", "webdav-account-saved": "WebDAV felhasználói fiók mentve", + "Webhook_Details": "WebHook részletek", "Webhook_URL": "Webhook URL", "Webhooks": "Webhooks", "WebRTC_direct_audio_call_from_%s": "Közvetlen hanghívás%s-ról", @@ -3317,16 +3799,22 @@ "WebRTC_Servers_Description": "A listát a STUN és TURN szerverek vesszővel elválasztva.
      Felhasználónév, jelszó és a port engedélyezett a formában `felhasználónév: jelszó @ stun: host: port` vagy` felhasználónév: jelszó @ fordulat: host: port`.", "Website": "Weboldal", "Wednesday": "szerda", + "Weekly_Active_Users": "Heti aktív felhasználók", "Welcome": "Üdvözöllek %s", "Welcome_to": " Üdvözöljük a(z) __Site_Name__ oldalon", "Welcome_to_the": "Üdvözöllek a", + "Where_are_the_messages_being_sent?": "Hová küldik az üzeneteket?", "Why_do_you_want_to_report_question_mark": "Miért akar jelenteni?", "will_be_able_to": "képes lesz", + "Will_be_available_here_after_saving": "A mentés után itt lesz elérhető.", + "Without_priority": "Prioritás nélkül", "Worldwide": "Világszerte", "Would_you_like_to_return_the_inquiry": "Szeretné visszaadni a vizsgálatot?", + "Would_you_like_to_place_chat_on_hold": "Szeretné várakoztatni ezt a csevegést?", "Yes": "Igen", "Yes_archive_it": "Igen, archiváld!", "Yes_clear_all": "Igen, tiszta minden!", + "Yes_deactivate_it": "Igen, kapcsolja ki!", "Yes_delete_it": "Igen, töröld!", "Yes_hide_it": "Igen, rejtsd el!", "Yes_leave_it": "Igen, hagyjuk!", @@ -3347,10 +3835,12 @@ "You_can_use_an_emoji_as_avatar": "Ön is használja a hangulatjel, mint egy avatar.", "You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM": "Használhatja webhooks könnyen integrálható LiveChat a CRM.", "You_cant_leave_a_livechat_room_Please_use_the_close_button": "Nem hagyhatja a LiveChat szobában. Kérjük, használja a bezárás gombot.", + "You_followed_this_message": "Követted ezt az üzenetet.", + "You_have_a_new_message": "Új üzeneted van.", "You_have_been_muted": "Akkor került lezárásra, és nem tud beszélni ebben a szobában", "You_have_n_codes_remaining": "Nincsenek __number__ kódok.", "You_have_not_verified_your_email": "Ön még nem igazolta az e-mail.", - "You_have_successfully_unsubscribed": "Sikeresen leiratkozott a Mailling List.", + "You_have_successfully_unsubscribed": "Sikeresen leiratkozott a levelező listáról.", "You_have_to_set_an_API_token_first_in_order_to_use_the_integration": "Először be kell állítania egy API tokenet az integráció használatához.", "You_must_join_to_view_messages_in_this_channel": "Csatlakozni kell ahhoz, hogy az ebben a csatornában megjelenő üzeneteket megnézhesse", "You_need_confirm_email": "Kérlek hitelesítsd az email címed a bejelntkezéshez", @@ -3362,18 +3852,28 @@ "You_need_to_write_something": "Be kell írni valamit!", "You_should_inform_one_url_at_least": "Meg kell határozni legalább egy URL-t.", "You_should_name_it_to_easily_manage_your_integrations": "Meg kell neveznie azt könnyedén kezelheti a integrációk.", + "You_unfollowed_this_message": "Már nem követed ezt az üzenetet.", "You_will_not_be_able_to_recover": "Ön nem lesz képes visszaállítani ezt az üzenetet!", "You_will_not_be_able_to_recover_file": "Ön nem lesz képes visszaállítani a fájlt!", "You_wont_receive_email_notifications_because_you_have_not_verified_your_email": "Nem fog kapni e-mailben értesítést, mert nem ellenőrizte az e-mail.", + "Your_e2e_key_has_been_reset": "Az e2e kulcsod visszaállításra került.", + "Your_email_address_has_changed": "Az e-mail címed megváltozott.", "Your_email_has_been_queued_for_sending": "Az e-mail várólistára került küldő", - "Your_entry_has_been_deleted": "A bejegyzés törölve lett.", + "Your_entry_has_been_deleted": "A bejegyzésed törölve lett.", "Your_file_has_been_deleted": "A fájl törölve lett.", + "Your_invite_link_will_expire_after__usesLeft__uses": "A meghívó linkje __usesLeft__ használat után lejár.", + "Your_invite_link_will_expire_on__date__": "A meghívó linkje lejár: __date__.", + "Your_invite_link_will_never_expire": "A meghívó linkje soha nem fog lejárni.", "Your_mail_was_sent_to_s": "A mail-ben küldött %s", "your_message": "az üzeneted", "your_message_optional": "az üzeneted (opcionális)", + "Your_new_email_is_email": "Az új e-mail címed: [email]", "Your_password_is_wrong": "A jelszó rossz!", + "Your_password_was_changed_by_an_admin": "Egy adminisztrátor megváltoztatta a jelszavad.", "Your_push_was_sent_to_s_devices": "Push küldték %s eszközök", "Your_question": "Kérdésed", "Your_server_link": "A szerver linkje", + "Your_temporary_password_is_password": "Az ideiglenes jelszavad: [password]", + "Your_TOTP_has_been_reset": "A kétfaktoros TOTP-d visszaállításra került.", "Your_workspace_is_ready": "A munkaterület készen áll a 🎉 használatára" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/id.i18n.json b/packages/rocketchat-i18n/i18n/id.i18n.json index e7cb8ccb9315..a6187ba783e0 100644 --- a/packages/rocketchat-i18n/i18n/id.i18n.json +++ b/packages/rocketchat-i18n/i18n/id.i18n.json @@ -555,6 +555,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Pemberitahuan suara berkelanjutan untuk ruang livechat baru", "Conversation": "Percakapan", "Conversation_closed": "Percakapan ditutup: __comment__.", + "Conversation_finished": "percakapan selesai", "Conversation_finished_message": "Pesan Selesai Percakapan", "conversation_with_s": "percakapan dengan %s", "Convert_Ascii_Emojis": "Ubah ASCII ke Emoji", @@ -2310,6 +2311,7 @@ "Show_Setup_Wizard": "Tampilkan Wizard Pengaturan", "Show_the_keyboard_shortcut_list": "Tampilkan daftar jalan pintas keyboard", "Showing_archived_results": "

      Menampilkan%s hasil diarsipkan

      ", + "Showing_online_users": null, "Showing_results": "

      Menampilkan %s hasil

      ", "Sidebar": "Sidebar", "Sidebar_list_mode": "Mode Daftar Saluran Sidebar", @@ -2689,6 +2691,7 @@ "Users_added": "Pengguna telah ditambahkan", "Users_in_role": "Pengguna dalam peran", "UTF8_Names_Slugify": "UTF8 Nama slugify", + "Videocall_enabled": "Panggilan Video Diaktifkan", "Validate_email_address": "Validasi alamat email", "Verification": "Verifikasi", "Verification_Description": "Anda dapat menggunakan placeholder berikut:
      • [Verification_Url] untuk URL verifikasi.
      • [nama], [fname], [lname] untuk nama lengkap pengguna, nama depan, atau nama belakang masing-masing.
      • [email] untuk email pengguna.
      • [Site_Name] dan [Site_URL] untuk Nama Aplikasi dan URL masing-masing.
      ", @@ -2703,7 +2706,6 @@ "Video_Conference": "Konferensi video", "Video_message": "Pesan video", "Videocall_declined": "Panggilan Video Ditolak.", - "Videocall_enabled": "Panggilan Video Diaktifkan", "View_All": "Tampilkan Semua", "View_Logs": "Lihat Log", "View_mode": "Lihat modus", @@ -2825,4 +2827,4 @@ "Your_push_was_sent_to_s_devices": "push dikirim ke%s perangkat", "Your_server_link": "Tautan server Anda", "Your_workspace_is_ready": "Ruang kerja Anda siap digunakan 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/it.i18n.json b/packages/rocketchat-i18n/i18n/it.i18n.json index 03ce86cb6d4d..1fb3c11791cd 100644 --- a/packages/rocketchat-i18n/i18n/it.i18n.json +++ b/packages/rocketchat-i18n/i18n/it.i18n.json @@ -299,7 +299,7 @@ "API_Upper_Count_Limit": "Numero massimo del registro", "API_Upper_Count_Limit_Description": "Qual è il numero massimo di record della REST API che può ritornare (quando non è illimitata)?", "API_User_Limit": "Limite utente per aggiungere tutti gli utenti al canale", - "API_Wordpress_URL": "URL WordPress", + "API_Wordpress_URL": "WordPress URL", "Apiai_Key": "Chiave Api.ai", "Apiai_Language": "Lingua Api.ai", "App_author_homepage": "homepage dell'autore", @@ -570,6 +570,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Notifiche continue del suono per la nuova stanza livechat", "Conversation": "Conversazione", "Conversation_closed": "Conversazione chiusa: __comment__.", + "Conversation_finished": "Conversazione terminata", "Conversation_finished_message": "Messaggio di conversazione terminato", "conversation_with_s": "la conversazione con %s", "Convert_Ascii_Emojis": "Converti gli ASCII in Emoji", @@ -918,7 +919,7 @@ "Desktop_Notifications_Enabled": "Le notifiche desktop sono abilitate", "Different_Style_For_User_Mentions": "Stile diverso per le menzioni dell'utente", "Direct_message_someone": "Invia un messaggio diretto", - "Direct_Messages": "Messaggi diretti", + "Direct_Messages": "Messaggi privati", "Direct_Reply": "Risposta diretta", "Direct_Reply_Debug": "Debug Direct Reply", "Direct_Reply_Debug_Description": "[Attenzione] L'attivazione della modalità di debug mostrerebbe la tua 'password di testo normale' nella Console di amministrazione.", @@ -2237,7 +2238,7 @@ "Room_unarchived": "Canale disarchiviato", "Room_uploaded_file_list": "Elenco dei file", "Room_uploaded_file_list_empty": "Nessun file disponibile.", - "Rooms": "Canali", + "Rooms": "Stanze", "run-import": "Esegui importazione", "run-import_description": "Autorizzazione a gestire gli importatori", "run-migration": "Esegui migrazione", @@ -2291,7 +2292,7 @@ "seconds": "secondi", "Secret_token": "Token segreto", "Security": "Sicurezza", - "Select_a_department": "Seleziona un reparto", + "Select_a_department": "Seleziona un dipartimento", "Select_a_user": "Seleziona un utente", "Select_an_avatar": "Seleziona l'avatar", "Select_an_option": "Seleziona un'opzione", @@ -2366,6 +2367,7 @@ "Show_Setup_Wizard": "Mostra procedura guidata di installazione", "Show_the_keyboard_shortcut_list": "Mostra l'elenco delle scorciatoie per la tastiera", "Showing_archived_results": "

      Mostra %s risultati archiviati

      ", + "Showing_online_users": null, "Showing_results": "

      Visualizzati %s risultati

      ", "Sidebar": "Sidebar", "Sidebar_list_mode": "Modalità Elenco canali laterale", @@ -2504,7 +2506,7 @@ "The_emails_are_being_sent": "Le email verranno inviate.", "The_field_is_required": "Il campo %s è richiesto.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Il ridimensionamento dell'immagine non funzionerà se non rileva ImageMagick or GraphicsMagick installato sul tuo server.", - "The_redirectUri_is_required": "Il redirectUri è richiesto", + "The_redirectUri_is_required": "Il redirectUri é richiesto", "The_server_will_restart_in_s_seconds": "Il server si riavvierà in %s secondi", "The_setting_s_is_configured_to_s_and_you_are_accessing_from_s": "L'impostazione%s è configurata su %s e stai accedendo da%s!", "The_user_will_be_removed_from_s": "L'utente sarà rimosso da %s", @@ -2620,7 +2622,7 @@ "Type_your_email": "Inserisci la tua email", "Type_your_job_title": "Digita il titolo del tuo lavoro", "Type_your_message": "Inserisci la tua messaggio", - "Type_your_name": "Inserisci il tuo nome", + "Type_your_name": "Inserire il proprio nome", "Type_your_new_password": "Inserisci la nuova password", "Type_your_password": "Digita la tua password", "Type_your_username": "Inserisci il tuo nome utente", @@ -2651,7 +2653,7 @@ "Unread_Messages": "Messaggi non letti", "Unread_on_top": "Non letti sopra", "Unread_Rooms": "Canali non letti", - "Unread_Rooms_Mode": "Modalità canali non letti", + "Unread_Rooms_Mode": "Modalità Stanze Non Letta", "Unread_Tray_Icon_Alert": "Avviso icona vassoio non letto", "Unstar_Message": "Non evidenziare messaggio", "Update_your_RocketChat": "Aggiorna il tuo Rocket.Chat", @@ -2761,6 +2763,7 @@ "Users_added": "L'utente è stato aggiunto", "Users_in_role": "Utenti nel ruolo", "UTF8_Names_Slugify": "UTF8 Names Slugify", + "Videocall_enabled": "Chiamata video abilitata", "Validate_email_address": "Verifica indirizzo email", "Verification": "Verifica", "Verification_Description": "Puoi usare i seguenti segnaposti:
      • [Forgot_Password_Url] per la URL del recupero password.
      • [name], [fname], [lname] rispettivamente per il nome completo dell'utente, nome or cognome.
      • [email] per la email dell'utente.
      • [Site_Name] e [Site_URL] rispettivamente per il nome della applicazione e la URL.
      ", @@ -2778,7 +2781,6 @@ "Video_Conference": "Video conferenza", "Video_message": "Messaggio video", "Videocall_declined": "Chiamata video rifiutata", - "Videocall_enabled": "Chiamata video abilitata", "View_All": "Vedi tutto", "View_Logs": "Visualizza log", "View_mode": "Aspetto", @@ -2903,4 +2905,4 @@ "Your_push_was_sent_to_s_devices": "La tua richiesta è stata inviata ai %s dispositivi.", "Your_server_link": "Il tuo collegamento al server", "Your_workspace_is_ready": "Il tuo spazio di lavoro è pronto per l'uso 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/ja.i18n.json b/packages/rocketchat-i18n/i18n/ja.i18n.json index 62cc14bf3154..15ee9cec1c16 100644 --- a/packages/rocketchat-i18n/i18n/ja.i18n.json +++ b/packages/rocketchat-i18n/i18n/ja.i18n.json @@ -892,7 +892,7 @@ "Conversation_closed": "会話が閉じました: __comment__", "Conversation_closing_tags": "会話終了タグ", "Conversation_closing_tags_description": "終了タグは、終了時に会話に自動的に割り当てられます。", - "Conversation_finished": "会話終了", + "Conversation_finished": "チャット終了", "Conversation_finished_message": "会話終了時のメッセージ", "Conversation_finished_text": "会話終了時のメッセージ", "conversation_with_s": "%sとの会話", @@ -3601,7 +3601,7 @@ "Sunday": "日曜日", "Support": "サポート", "Survey": "アンケート", - "Survey_instructions": "それぞれの設問にご満足度を、全く不満 1 〜 大変満足5 で評価してください。", + "Survey_instructions": "それぞれの設問について満足度を、不満 1 〜 満足 5 で評価してください。", "Symbols": "シンボル", "Sync": "同期する", "Sync / Import": "同期とインポート", @@ -3816,7 +3816,7 @@ "Type_your_email": "あなたのメールアドレスを入力してください", "Type_your_job_title": "職種を入力してください", "Type_your_message": "メッセージを入力", - "Type_your_name": "あなたの名前を入力します", + "Type_your_name": "名前を入力してください", "Type_your_new_password": "新しいパスワードを入力", "Type_your_password": "パスワードを入力してください", "Type_your_username": "あなたのユーザー名を入力", @@ -3999,6 +3999,7 @@ "Uses": "使用", "Uses_left": "残り", "UTF8_Names_Slugify": "UTF8形式名をスラグ化する", + "Videocall_enabled": "ビデオ通話が有効", "Validate_email_address": "電子メールアドレスの検証", "Validation": "検証", "Value_messages": "__value__ メッセージ", @@ -4020,7 +4021,6 @@ "Video_Conference": "ビデオ会議", "Video_message": "ビデオメッセージ", "Videocall_declined": "ビデオ通話が辞退しました。", - "Videocall_enabled": "ビデオ通話が有効", "Videos": "ビデオ", "View_All": "すべて表示", "View_Logs": "ログ表示", diff --git a/packages/rocketchat-i18n/i18n/ka-GE.i18n.json b/packages/rocketchat-i18n/i18n/ka-GE.i18n.json index 4c3105cdd5ea..403028e1f582 100644 --- a/packages/rocketchat-i18n/i18n/ka-GE.i18n.json +++ b/packages/rocketchat-i18n/i18n/ka-GE.i18n.json @@ -2888,6 +2888,7 @@ "Room_archivation_state_false": "აქტიური", "Room_archivation_state_true": "დაარქივებულია", "Room_archived": "ოთახი დაარქივებულია", + "room_changed_privacy": null, "room_changed_topic": "ოთახის თემა შეიცვალა: __room_topic__ __user_by__", "Room_default_change_to_private_will_be_default_no_more": "ეს არის დეფაულტ არხი და პირად ჯგუფად გადაკეთების შემთხვევაში აღარ იქნება დეფაულტ არხი.გსურთ გაგრძელება?", "Room_description_changed_successfully": "ოთახის აღწერა წარმატებით შეიცვალა", @@ -3578,6 +3579,7 @@ "Uses": "იყენებს", "Uses_left": "იყენებს მარცხნივ", "UTF8_Names_Slugify": "UTF8 სახელების Slugify", + "Videocall_enabled": "ვიდეო ზარი ჩართულია", "Validate_email_address": "ელ.ფოსტის მისამართების დამოწმება", "Validation": "დამოწმება", "Value_messages": "__value__ შეტყობინებები", @@ -3599,7 +3601,6 @@ "Video_Conference": "ვიდეო კონფერენცია", "Video_message": "ვიდეო შეტყობინება", "Videocall_declined": "ვიდეო ზარი უარყოფილია", - "Videocall_enabled": "ვიდეო ზარი ჩართულია", "Videos": "ვიდეოები", "View_All": "იხილეთ ყველა წევრი", "View_Logs": "ლოგების ნახვა", @@ -3747,4 +3748,4 @@ "Your_server_link": "თქვენი სერვერის მისამართი", "Your_temporary_password_is_password": "თქვენი დროებითი პაროლია არის [password]", "Your_workspace_is_ready": "თქვენი სამუშაო გარემო მზად არის სამუშაოდ 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/km.i18n.json b/packages/rocketchat-i18n/i18n/km.i18n.json index 2a136459f14e..8435fc2afe79 100644 --- a/packages/rocketchat-i18n/i18n/km.i18n.json +++ b/packages/rocketchat-i18n/i18n/km.i18n.json @@ -1530,7 +1530,7 @@ "hours": "ម៉ោង", "Hours": "ម៉ោង", "How_friendly_was_the_chat_agent": "តើធ្វើដូចម្តេចមិត្តភាពភ្នាក់ងារជជែកកំសាន្តនោះ?", - "How_knowledgeable_was_the_chat_agent": "តើចំណេះភ្នាក់ងារជជែកកំសាន្តនោះ?", + "How_knowledgeable_was_the_chat_agent": "តើចំណេះដឹងដែលភ្នាក់ងារជជែកកំសាន្តផ្តល់ឲ្យគ្រប់គ្រាន់ឬទេ?", "How_long_to_wait_after_agent_goes_offline": "តើត្រូវរង់ចាំប៉ុន្មានពេលដែលភ្នាក់ងារដើរនៅក្រៅអ៊ីនធឺណិត", "How_responsive_was_the_chat_agent": "តើធ្វើដូចម្តេចឆ្លើយតបភ្នាក់ងារជជែកកំសាន្តនោះ?", "How_satisfied_were_you_with_this_chat": "តើអ្នកពេញចិត្តនាក់អ្នកជាមួយនឹងការជជែកនេះ?", @@ -1985,6 +1985,7 @@ "Me": "ខ្ញុំ", "Media": "ប្រព័ន្ធផ្សព្វផ្សាយ", "Medium": "មធ្យម", + "Members": "សាមាជិក", "Members_List": "បញ្ជី​សមាជិក", "mention-all": "និយាយទាំងអស់", "mention-all_description": "ការអនុញ្ញាតប្រើប្រាស់ @all mention", @@ -2625,6 +2626,7 @@ "Show_Setup_Wizard": "បង្ហាញអ្នកជំនួយការដំឡើង", "Show_the_keyboard_shortcut_list": "បង្ហាញបញ្ជីផ្លូវកាត់ក្តារចុច", "Showing_archived_results": "

      បង្ហាញពីលទ្ធផលទុកក្នុងប័ណ្ណសារ %s បាន

      ", + "Showing_online_users": null, "Showing_results": "

      កំពុង​បង្ហាញ %s លទ្ធផល

      ", "Sidebar": "របារចំហៀង", "Sidebar_list_mode": "របៀបបញ្ជីឆានែលរបារចំហៀង", @@ -3031,6 +3033,7 @@ "Users_added": "អ្នកប្រើត្រូវបានបន្ថែម", "Users_in_role": "អ្នកប្រើនៅក្នុងតួនាទី", "UTF8_Names_Slugify": "ឈ្មោះក្រៅ UTF8 Slugify", + "Videocall_enabled": "បានបើកការហៅជាវីដេអូ", "Validate_email_address": "ធ្វើឱ្យមានអាសយដ្ឋានអ៊ីមែលមានសុពលភាព", "Verification": "ការផ្ទៀងផ្ទាត់", "Verification_Description": "អ្នកអាចប្រើកន្លែងដាក់ខាងក្រោម:
      • [Verification_Url] សម្រាប់ URL ផ្ទៀងផ្ទាត់។
      • [ឈ្មោះ], [fname], [lname] សម្រាប់ឈ្មោះអ្នកប្រើនាមត្រកូលឬនាមត្រកូលរៀងៗខ្លួន។
      • [អ៊ីមែល] សម្រាប់អ៊ីម៉ែលរបស់អ្នកប្រើ។
      • [Site_Name] និង [Site_URL] សម្រាប់ឈ្មោះកម្មវិធីនិង URL រៀងៗខ្លួន។
      ", @@ -3047,7 +3050,6 @@ "Video_Conference": "ស​ន្និ​សិ​ទ​វីដេអូ", "Video_message": "សារវីដេអូ", "Videocall_declined": "ការហៅវីដេអូបានបដិសេធ។", - "Videocall_enabled": "បានបើកការហៅជាវីដេអូ", "View_All": "មើល​ទាំង​អស់", "View_Logs": "មើលកំណត់ហេតុ", "View_mode": "របៀបមើល", @@ -3177,4 +3179,4 @@ "Your_push_was_sent_to_s_devices": "ការជំរុញរបស់អ្នកត្រូវបានបញ្ជូនទៅកាន់ឧបករណ៍ %s បាន", "Your_server_link": "តំណភ្ជាប់ម៉ាស៊ីនមេរបស់អ្នក", "Your_workspace_is_ready": "កន្លែងធ្វើការរបស់អ្នករួចរាល់ដើម្បីប្រើ🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/ko.i18n.json b/packages/rocketchat-i18n/i18n/ko.i18n.json index 361509ac5949..0f9342ef8918 100644 --- a/packages/rocketchat-i18n/i18n/ko.i18n.json +++ b/packages/rocketchat-i18n/i18n/ko.i18n.json @@ -3927,6 +3927,7 @@ "Uses": "용도", "Uses_left": "남은 사용량", "UTF8_Names_Slugify": "UTF8 이름 Slugify", + "Videocall_enabled": "화상 통화 사용", "Validate_email_address": "이메일 주소 검증", "Validation": "검증", "Value_messages": "__value__ 메시지", @@ -3948,7 +3949,6 @@ "Video_Conference": "화상 회의", "Video_message": "화상 메시지", "Videocall_declined": "화상 통화가 거부되었습니다.", - "Videocall_enabled": "화상 통화 사용", "Videos": "동영상", "View_All": "모든 사용자 보기", "View_Logs": "로그 보기", diff --git a/packages/rocketchat-i18n/i18n/ku.i18n.json b/packages/rocketchat-i18n/i18n/ku.i18n.json index 679dfe2e0f6d..663f9dd27c72 100644 --- a/packages/rocketchat-i18n/i18n/ku.i18n.json +++ b/packages/rocketchat-i18n/i18n/ku.i18n.json @@ -554,6 +554,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Agahiyên deng ên berdewam ji bo odeya zindî ya nû ya nû", "Conversation": "گفتوگۆ", "Conversation_closed": "Axaftina: __comment__.", + "Conversation_finished": "conversation qedand", "Conversation_finished_message": "Gotûbêja Peyama Dawîn", "conversation_with_s": "axaftina bi %s", "Convert_Ascii_Emojis": "Convert ASCII ji bo Emoji", @@ -1680,6 +1681,7 @@ "Max_length_is": "Mezinahiya%s e", "Media": "Medya", "Medium": "Medya", + "Members": "ئەندامان", "Members_List": "لیستی ئەندامان", "mention-all": "Mention All", "mention-all_description": "Destûra karûbarê @allê bikar bînin", @@ -2294,6 +2296,7 @@ "Show_Setup_Wizard": "Vebijêrk Setup", "Show_the_keyboard_shortcut_list": "Lîsteya kurteya klavyeyê nîşan bide", "Showing_archived_results": "

      Rûpela results trendê %s

      ", + "Showing_online_users": null, "Showing_results": "

      نیشاندانی %s ئەنجام

      ", "Sidebar": "Sidebar", "Sidebar_list_mode": "Mîhengên Channel Lîsteya Sidebar", @@ -2673,6 +2676,7 @@ "Users_added": "Bikarhênerên nû hatine zêdekirin", "Users_in_role": "Bikarhêner li rola", "UTF8_Names_Slugify": "نازناو بۆ ناوی UTF8", + "Videocall_enabled": "Call Call Enabled", "Validate_email_address": "Navnîşana Navnîşa Navnîşankirî", "Verification": "Tesdîq", "Verification_Description": "Hûn dikarin liverên jêrîn bikar bînin:
      • [Verification_Url] ji bo pejirandinê URL.
      • navê [[name], [fname], [lname] ji bo navê bikarhênerê, first name an navê paşîn, paşê.
      • [email] bo e-nameya bikarhêner.
      • [Site_Name] û [Site_URL] ji bo navê navekî û navnîşê ya serî.
      ", @@ -2687,7 +2691,6 @@ "Video_Conference": "Konferansa Video", "Video_message": "Peyamê Video", "Videocall_declined": "Call Call Declined.", - "Videocall_enabled": "Call Call Enabled", "View_All": "هەموو ببینە", "View_Logs": "View Têketin", "View_mode": "mode View", @@ -2809,4 +2812,4 @@ "Your_push_was_sent_to_s_devices": "push xwe ji bo cîhazên %s hate şandin", "Your_server_link": "Girêdana serverê", "Your_workspace_is_ready": "Karên te yên amadekar e amade ye" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/lo.i18n.json b/packages/rocketchat-i18n/i18n/lo.i18n.json index 93254d103888..a12e2ad65684 100644 --- a/packages/rocketchat-i18n/i18n/lo.i18n.json +++ b/packages/rocketchat-i18n/i18n/lo.i18n.json @@ -572,6 +572,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "ການແຈ້ງເຕືອນກ່ຽວກັບສຽງຕໍ່ເນື່ອງສໍາລັບຫ້ອງດໍາລົງຊີວິດໃຫມ່", "Conversation": "ການສົນທະນາ", "Conversation_closed": "ການສົນທະນາປິດ: __comment__.", + "Conversation_finished": "ການສົນທະນາໄດ້ສໍາເລັດ", "Conversation_finished_message": "ການສົນທະນາສິ້ນສຸດຂໍ້ຄວາມ", "conversation_with_s": "ການສົນທະນາທີ່ມີ %s", "Convert_Ascii_Emojis": "ແປງ ASCII ກັບ Emoji", @@ -994,6 +995,7 @@ "Editing_room": "ຫ້ອງການແກ້ໄຂ", "Editing_user": "ຜູ້ໃຊ້ການແກ້ໄຂ", "Education": "ການສຶກສາ", + "Email": "Email", "Email_address_to_send_offline_messages": "ທີ່ຢູ່ອີເມວການສົ່ງຂໍ້ຄວາມອອຟໄລ", "Email_already_exists": "Email ຢູ່ແລ້ວ", "Email_body": "ຮ່າງກາຍອີເມລ໌", @@ -2335,6 +2337,7 @@ "Show_Setup_Wizard": "ສະແດງຕົວຊ່ວຍສ້າງການຕັ້ງຄ່າ", "Show_the_keyboard_shortcut_list": "ສະແດງລາຍະການທາງລັດແປ້ນພິມ", "Showing_archived_results": "

      ສະແດງໃຫ້ເຫັນຜົນໄດ້ຮັບທີ່ເກັບ %s

      ", + "Showing_online_users": null, "Showing_results": "

      ສະແດງໃຫ້ເຫັນຜົນໄດ້ຮັບ %s

      ", "Sidebar": "Sidebar", "Sidebar_list_mode": "ໂຫມດບັນທັດຂອງແຖບ Sidebar", @@ -2714,6 +2717,7 @@ "Users_added": "ຜູ້ໃຊ້ໄດ້ຖືກເພີ່ມ", "Users_in_role": "ຜູ້ຊົມໃຊ້ໃນພາລະບົດບາດ", "UTF8_Names_Slugify": "UTF8 Names Slugify", + "Videocall_enabled": null, "Validate_email_address": "ຢືນຢັນທີ່ຢູ່ອີເມວ", "Verification": "ການຢັ້ງຢືນ", "Verification_Description": "ທ່ານອາດຈະນໍາໃຊ້ບ່ອນວາງສະຖານດັ່ງຕໍ່ໄປນີ້:
      • [Verification_Url] ສໍາລັບ URL ການຢືນຢັນ.
      • [ຊື່], [fname], [lname] ສໍາລັບຊື່ເຕັມ, ຊື່ຫຼືນາມສະກຸນຂອງຜູ້ໃຊ້.
      • [ອີເມວ] ສໍາລັບອີເມວຂອງຜູ້ໃຊ້.
      • [Site_Name] ແລະ [Site_URL] ສໍາລັບຊື່ແອັບຯແລະ URL ຕາມລໍາດັບ.
      ", @@ -2728,7 +2732,6 @@ "Video_Conference": "Video Conference", "Video_message": "ຂໍ້ຄວາມວິດີໂອ", "Videocall_declined": "ການໂທວິດີໂອໄດ້ຫຼຸດລົງ.", - "Videocall_enabled": "Video Call Enabled", "View_All": "ເບິ່ງ​ທັງ​ຫມົດ", "View_Logs": "ເບິ່ງຂໍ້ມູນບັນທຶກ", "View_mode": "ຮູບແບບການເບິ່ງ", @@ -2850,4 +2853,4 @@ "Your_push_was_sent_to_s_devices": "ການຊຸກຍູ້ຂອງທ່ານໄດ້ຖືກສົ່ງໄປອຸປະກອນ %s", "Your_server_link": "ເຊື່ອມຕໍ່ເຊີຟເວີຂອງທ່ານ", "Your_workspace_is_ready": "ພື້ນທີ່ເຮັດວຽກຂອງທ່ານແມ່ນພ້ອມທີ່ຈະໃຊ້🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/lt.i18n.json b/packages/rocketchat-i18n/i18n/lt.i18n.json index 707daa69125b..b202e2a98e4c 100644 --- a/packages/rocketchat-i18n/i18n/lt.i18n.json +++ b/packages/rocketchat-i18n/i18n/lt.i18n.json @@ -201,9 +201,12 @@ "Accounts_RequireNameForSignUp": "Reikalauti registracijos vardo", "Accounts_RequirePasswordConfirmation": "Reikalauti slaptažodžio patvirtinimo", "Accounts_SearchFields": "Laukai, kuriuos reikia apsvarstyti paieškoje", + "Accounts_Send_Email_When_Activating": "Siųsti naudotojui el. laišką, kai naudotojas aktyvuojamas", + "Accounts_Send_Email_When_Deactivating": "Siųsti naudotojui el. laišką, kai naudotojas deaktyvuojamas", "Accounts_SetDefaultAvatar": "Nustatyti numatytąjį įvaizdį", "Accounts_SetDefaultAvatar_Description": "Bando nustatyti numatytąją piktogramą, pagrįstą \"OAuth\" sąskaita arba \"Gravatar\"", "Accounts_ShowFormLogin": "Rodyti prisijungimą pagal formą", + "Accounts_TwoFactorAuthentication_By_Email_Enabled": "Įgalinti dviejų veiksnių autentiškumo patvirtinimą el. paštu", "Accounts_TwoFactorAuthentication_Enabled": "Įgalinti dviejų veiksnių autentifikavimą", "Accounts_TwoFactorAuthentication_MaxDelta": "Didžiausia deltė", "Accounts_TwoFactorAuthentication_MaxDelta_Description": "Maksimalus deltas nustato, kiek žetonų galioja bet kuriuo metu. Žetonai generuojami kas 30 sekundžių ir galioja (30 * Maksimalus deltos) sekundes.
      Pavyzdys: kai maksimali deltos vertė yra 10, kiekvienas raktas gali būti naudojamas iki 300 sekundžių iki arba po laiko žymos. Tai naudinga, kai kliento laikrodis netinkamai sinchronizuojamas su serveriu.", @@ -212,8 +215,11 @@ "Accounts_UserAddedEmail_Default": "

      Sveiki atvykę į [Site_Name]

      Eikite į [Site_URL] ir išbandykite geriausią atviro kodo pokalbių sprendimą šiandien!

      Prisijungti galite naudodami savo el. pašto adresą: [email] Ir slaptažodį: [password]. Po pirmojo prisijungimo gali tekti jį pakeisti.", "Accounts_UserAddedEmail_Description": "Galite atitinkamai nurodyti naudotojo vardą, pavardę ar vardą, naudodami šiuos užpildytojus:

      • [name], [fname], [lname].
      • [el. Paštas] naudotojo el. Laiškui.
      • [slaptažodis] vartotojo slaptažodžiui.
      • > [Site_Name] ir [Site_URL] atitinkamai programos pavadinimui ir URL.
      ", "Accounts_UserAddedEmailSubject_Default": "Jūs įtraukėte į [Site_Name]", + "Action": "Veiksmas", + "Action_required": "Reikalingas veiksmas", "Activate": "aktyvinti", "Active": "Aktyvus", + "Active_users": "Aktyvūs naudotojai", "Activity": "Veikla", "Add": "Pridurti", "Add_agent": "Pridėti agentą", @@ -225,6 +231,7 @@ "Add_user": "Pridėti naudotoją", "Add_User": "Pridėti naudotoją", "Add_users": "Pridėti vartotojus", + "Add_members": "Pridėti narius", "add-oauth-service": "Pridėti \"Oauth\" paslaugą", "add-oauth-service_description": "Leidimas pridėti naują \"Oauth\" paslaugą", "add-user": "Pridėti naudotoją", @@ -343,9 +350,12 @@ "Apps_Game_Center_Back": "Grįžti į žaidimų centrą", "Apps_Game_Center_Invite_Friends": "Pakvieskite draugus prisijungti", "Apps_Game_Center_Play_Game_Together": "@here Žaiskime __name__ kartu!", + "Apps_License_Message_renewal": "Licencijos galiojimo laikas baigėsi ir ją reikia atnaujinti", "Apps_Logs_TTL_7days": "7 dienos", "Apps_Logs_TTL_14days": "14 dienų", "Apps_Logs_TTL_30days": "30 dienų", + "Apps_Marketplace_Deactivate_App_Prompt": "Ar tikrai norite išjungti šią programą?", + "Apps_Marketplace_Login_Required_Title": "Reikalingas prisijungimas prie Marketplace", "Apps_Marketplace_pricingPlan_monthly": "__price__ per mėnesį", "Apps_Marketplace_pricingPlan_monthly_perUser": "__price__ per mėnesį vienam naudotojui", "Apps_Marketplace_pricingPlan_startingAt_monthly": "nuo __price__ per mėnesį", @@ -356,6 +366,7 @@ "Apps_Marketplace_pricingPlan_yearly_perUser": "__price__ per metus vienam naudotojui", "Apps_Marketplace_Uninstall_App_Prompt": "Ar tikrai norite pašalinti šią programą?", "Apps_Marketplace_Uninstall_Subscribed_App_Anyway": "Vis tiek pašalinti", + "Apps_Permissions_Review_Modal_Title": "Reikalingi leidimai", "Apps_Settings": "Programos nustatymai", "Apps_WhatIsIt": "\"Apps\": kokie jie?", "Apps_WhatIsIt_paragraph1": "Nauja piktograma administravimo srityje! Ką tai reiškia ir kas yra \"Apps\"?", @@ -479,7 +490,7 @@ "CAS_Sync_User_Data_Enabled": "Visada sinchronizuoti naudotojo duomenis", "CAS_Sync_User_Data_Enabled_Description": "Prisijungę, visada sinchronizuokite išorinius CAS vartotojo duomenis prie prieinamų atributų. Pastaba: bet kuriuo atveju atributai visada sinchronizuojami paskyros sukūrimo metu.", "CAS_Sync_User_Data_FieldMap": "Atributo žemėlapis", - "CAS_Sync_User_Data_FieldMap_Description": "Naudokite šį JSON įvestį, kad sukurtumėte vidinius atributus (raktą) iš išorinių atributų (vertė).
      Pavyzdžiui, `{\" el. \":\"% Email% \",\" name \":\"% firstname%,% lastname% \"}`

      Atributų žemėlapis visada interpoliuojamas. CAS 1.0 tik atributas `username` yra prieinamas. Galimi vidiniai atributai: vartotojo vardas, vardas, el. Paštas, kambariai; kambariai yra kableliais atskirtų kambarių sąrašas, skirtas prisijungti prie vartotojo sukūrimo, pvz .: (\"kambariai\": \"% team%,% department%\") prisijungs prie CAS vartotojų kūrimo savo komandos ir departamento kanale.", + "CAS_Sync_User_Data_FieldMap_Description": "Naudokite šį JSON įvestį, kad sukurtumėte vidinius atributus (raktą) iš išorinių atributų (vertė).
      Pavyzdžiui, `{\"email\":\"%email%\", \"name\":\"%firstname%, %lastname%\"}`

      Atributų žemėlapis visada interpoliuojamas. CAS 1.0 tik atributas `username` yra prieinamas. Galimi vidiniai atributai: vartotojo vardas, vardas, el. Paštas, kambariai; kambariai yra kableliais atskirtų kambarių sąrašas, skirtas prisijungti prie vartotojo sukūrimo, pvz .:{\"rooms\": \"%team%,%department%\"} prisijungs prie CAS vartotojų kūrimo savo komandos ir departamento kanale.", "CAS_version": "CAS versija", "CAS_version_Description": "Naudokite tik palaikomą CAS versiją, kurią palaiko jūsų CAS SSO paslauga.", "CDN_PREFIX": "CDN prefiksas", @@ -605,6 +616,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Nuolatiniai pranešimai apie garsą naujam \"Livechat\" kambariui", "Conversation": "Pokalbis", "Conversation_closed": "Pokalbis uždarytas: __comment__.", + "Conversation_finished": "Pokalbis baigtas", "Conversation_finished_message": "Pokalbis baigtas pranešimas", "conversation_with_s": "pokalbis su %s", "Convert_Ascii_Emojis": "Konvertuoti ASCII į Emoji", @@ -1617,7 +1629,7 @@ "LDAP_User_Search_Filter_Description": "Jei nurodyta, bus leista prisijungti tik šį filtrą atitinkantiems vartotojams. Jei filtro nenurodyta, visi naudotojai, kuriems taikoma konkretaus domeno bazė, galės prisijungti.
      Pvz. Active Directory \"memberOf = cn = ROCKET_CHAT, ou = Bendrosios grupės\".
      Pvz. OpenLDAP (extensible match search) \"ou: dn: = ROCKET_CHAT\".", "LDAP_User_Search_Scope": "Taikymo sritis", "LDAP_Username_Field": "Vartotojo vardas laukas", - "LDAP_Username_Field_Description": "Kuris laukas bus naudojamas kaip * vartotojo vardas * naujiems vartotojams. Palikite tuščią, kad naudotojo vardas būtų nurodytas prisijungimo puslapyje.
      Galite naudoti šablono žymes, pvz., \"# {GivenName}. # {Sn}\".
      Numatytoji reikšmė yra \"sAMAccountName\".", + "LDAP_Username_Field_Description": "Kuris laukas bus naudojamas kaip * vartotojo vardas * naujiems vartotojams. Palikite tuščią, kad naudotojo vardas būtų nurodytas prisijungimo puslapyje.
      Galite naudoti šablono žymes, pvz., \"#{givenName}. #{sn}\".
      Numatytoji reikšmė yra \"sAMAccountName\".", "Lead_capture_email_regex": "Švinas surenkite el. Pašto regex", "Lead_capture_phone_regex": "\"Lead\" užfiksuok telefoną regex", "Leave": "Palikite kambarį", @@ -2086,7 +2098,7 @@ "Push_show_username_room": "Rodyti kanalą / grupę / vartotojo vardą pranešime", "Push_test_push": "Testas", "Query": "Užklausa", - "Query_description": "Papildomos sąlygos nustatyti, kurie naudotojai turi siųsti el. Laišką. Neatsakytus naudotojus automatiškai pašalina iš užklausos. Tai turi galioti JSON. Pavyzdys: \"{\" createdAt \": {\" $ gt \": {\" $ date \":\" 2015-01-01T00: 00: 00.000Z \"}}}\"", + "Query_description": "Papildomos sąlygos nustatyti, kurie naudotojai turi siųsti el. laišką. Neatsakytus naudotojus automatiškai pašalina iš užklausos. Tai turi galioti JSON. Pavyzdys: \"{\"createdAt\":{\"$gt\":{\"$date\": \"2015-01-01T00:00:00.000Z\"}}}\"", "Queue": "Eilė", "quote": "citata", "Quote": "Citata", @@ -2732,6 +2744,7 @@ "Users_added": "Naudotojams buvo pridėta", "Users_in_role": "Vartotojai vaidmenyje", "UTF8_Names_Slugify": "UTF8 vardai Slugify", + "Videocall_enabled": "Vaizdo skambutis įjungtas", "Validate_email_address": "Patvirtinkite el. Pašto adresą", "Verification": "Patvirtinimas", "Verification_Description": "Galite naudoti šiuos užpildytojus:
      • > [Verification_Url], jei norite patvirtinimo URL.
      • [vardas], [fname], [lname] atitinkamai vartotojo vardas, pavardė arba vardas.
      • [el. Paštas] naudotojo el. Laiškui.
      • > [Site_Name] ir [Site_URL] atitinkamai programos pavadinimui ir URL.
      ", @@ -2746,7 +2759,6 @@ "Video_Conference": "Video konferencija", "Video_message": "Vaizdo pranešimas", "Videocall_declined": "Vaizdo skambutis atmesti.", - "Videocall_enabled": "Vaizdo skambutis įjungtas", "View_All": "Peržiūrėti visus narius", "View_Logs": "Žiūrėti žurnalus", "View_mode": "Peržiūrėti režimą", diff --git a/packages/rocketchat-i18n/i18n/lv.i18n.json b/packages/rocketchat-i18n/i18n/lv.i18n.json index 3e71499a0761..f01b99b21e3e 100644 --- a/packages/rocketchat-i18n/i18n/lv.i18n.json +++ b/packages/rocketchat-i18n/i18n/lv.i18n.json @@ -2159,6 +2159,7 @@ "Room_archivation_state_false": "Aktīvs", "Room_archivation_state_true": "Arhivēts", "Room_archived": "Istaba arhivēta", + "room_changed_privacy": null, "Room_default_change_to_private_will_be_default_no_more": "Šis ir noklusējuma kanāls, mainot to uz privātu grupu, tas vairs nebūs noklusējuma kanāls. Vai vēlaties turpināt?", "Room_description_changed_successfully": "Istabas apraksts ir veiksmīgi mainīts", "Room_has_been_archived": "Istaba ir arhivēta", @@ -2597,6 +2598,7 @@ "Use_url_for_avatar": "Izmantot URL kā avataru", "Use_User_Preferences_or_Global_Settings": "Izmantot lietotāja preferences vai globālos iestatījumus", "User": "Lietotājs", + "User__username__removed_from__room_name__leaders": null, "User_added": "Lietotājs pievienots", "User_added_successfully": "Lietotājs pievienots veiksmīgi", "User_and_group_mentions_only": "Lietotājs un grupa tikai pieminējumi", @@ -2663,6 +2665,7 @@ "Users_added": "Lietotāji ir pievienoti", "Users_in_role": "Lietotāji lomā", "UTF8_Names_Slugify": "UTF8 vārdi Slugify", + "Videocall_enabled": "Video zvans ir iespējots", "Validate_email_address": "Apstiprināt e-pasta adresi", "Verification": "Pārbaude", "Verification_Description": "Jūs varat izmantot šādus vietturus:
      • [Verification_Url] kā apstiprinājuma URL.
      • [vārds], [fname], [lname] lietotāja pilnam vārdam, attiecīgi vārds vai uzvārds.
      • [e-pasts] lietotāja e-pastam.
      • [Vietnes nosaukums] un [Site_URL] attiecīgi lietotnes nosaukums un URL.
      ", @@ -2677,7 +2680,6 @@ "Video_Conference": "Video konference", "Video_message": "Video ziņojums", "Videocall_declined": "Video zvans noraidīts.", - "Videocall_enabled": "Video zvans ir iespējots", "View_All": "Skatīt visus dalībniekus", "View_Logs": "Skatīt žurnālus", "View_mode": "Skatīt režīmu", @@ -2771,6 +2773,7 @@ "You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM": "Jūs varat izmantot webhoaks, lai viegli integrētu livechat ar savu CRM.", "You_cant_leave_a_livechat_room_Please_use_the_close_button": "Jūs nevarat atstāt livechat istabu. Lūdzu, izmantojiet aizvēršanas pogu.", "You_have_been_muted": "Jums ir liegts rakstīt un nevar runāt šajā istabā", + "You_have_n_codes_remaining": null, "You_have_not_verified_your_email": "Jūs neesat apstiprinājis savu e-pastu.", "You_have_successfully_unsubscribed": "Jūs esat veiksmīgi anulējis abonomentu no mūsu Sūtšanas saraksta.", "You_have_to_set_an_API_token_first_in_order_to_use_the_integration": "Vispirms ir jāiestata API žetons, lai izmantotu integrāciju.", @@ -2797,4 +2800,4 @@ "Your_push_was_sent_to_s_devices": "Jūsu push tika nosūtīts uz %s ierīcēm", "Your_server_link": "Jūsu servera saite", "Your_workspace_is_ready": "Jūsu darbastacija ir gatava lietošanai 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/mn.i18n.json b/packages/rocketchat-i18n/i18n/mn.i18n.json index cb7c6fe54869..c53483bee237 100644 --- a/packages/rocketchat-i18n/i18n/mn.i18n.json +++ b/packages/rocketchat-i18n/i18n/mn.i18n.json @@ -553,6 +553,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Шинэ livechat өрөөний тасралтгүй дууны мэдэгдэл", "Conversation": "Харилцаа холбоо", "Conversation_closed": "Хэлэлцүүлэг хаалттай: __comment__.", + "Conversation_finished": "Харилцаа дууссан", "Conversation_finished_message": "Хэлэлцүүлэг Дууссан зурвас", "conversation_with_s": "%s-тэй харилцсан", "Convert_Ascii_Emojis": "ASCII-г Emoji руу хөрвүүлэх", @@ -2666,6 +2667,7 @@ "Users_added": "Хэрэглэгчид нэмэгдсэн байна", "Users_in_role": "Үүрэг дэх хэрэглэгчид", "UTF8_Names_Slugify": "UTF8 нэрс Slugify", + "Videocall_enabled": "Видео дуудлага идэвхжсэн", "Validate_email_address": "Баталгаажуулсан имэйл хаяг", "Verification": "Баталгаажуулалт", "Verification_Description": "Та дараах байршлыг ашиглаж болно:
      • [Verification_Url] баталгаажуулах URL-д зориулж болно.
      • [нэр], [fname], [lname] хэрэглэгчийн бүтэн нэр, эхний нэр эсвэл овог нэр.
      • [имэйл] хэрэглэгчийн имэйлийн хувьд.
      • [Site_Name] болон [Site_URL] нь Програмын Нэр болон URL-ыг тус тусад нь зааж өгсөн.
      ", @@ -2680,7 +2682,6 @@ "Video_Conference": "Видео хурал", "Video_message": "Видео мэдээ", "Videocall_declined": "Видео дуудлага татгалзсан.", - "Videocall_enabled": "Видео дуудлага идэвхжсэн", "View_All": "Бүх гишүүдийг харах", "View_Logs": "Бүртгэлийг харах", "View_mode": "Горимыг харах", @@ -2775,6 +2776,7 @@ "You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM": "Та вэбсайтаа CRech-тай livechat-тай амархан холбох боломжтой.", "You_cant_leave_a_livechat_room_Please_use_the_close_button": "Та livechat өрөө орхиж болохгүй. Ойрхон товчлуурыг ашиглана уу.", "You_have_been_muted": "Та чимээгүй болж, энэ өрөөнд ярьж чадахгүй байна", + "You_have_n_codes_remaining": null, "You_have_not_verified_your_email": "Та өөрийн имэйлийг баталгаажуулаагүй байна.", "You_have_successfully_unsubscribed": "Та манай Майдансын жагсаалтаас амжилттай цуцалсан.", "You_have_to_set_an_API_token_first_in_order_to_use_the_integration": "Та интеграцыг ашиглахын тулд эхлээд API жетоныг тохируулах хэрэгтэй.", @@ -2801,4 +2803,4 @@ "Your_push_was_sent_to_s_devices": "Таны түлхэлт %s төхөөрөмж рүү илгээгдсэн", "Your_server_link": "Таны серверийн холбоос", "Your_workspace_is_ready": "Таны ажлын талбарыг ашиглахад бэлэн байна" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/ms-MY.i18n.json b/packages/rocketchat-i18n/i18n/ms-MY.i18n.json index 23b73b185a1e..746bd620fb56 100644 --- a/packages/rocketchat-i18n/i18n/ms-MY.i18n.json +++ b/packages/rocketchat-i18n/i18n/ms-MY.i18n.json @@ -1,6 +1,7 @@ { "403": "Larangan", "500": "Ralat Pelayan Dalaman", + "__username__was_set__role__by__user_by_": null, "@username": "@pengguna", "@username_message": "@username ", "#channel": "#channel", @@ -553,6 +554,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Pemberitahuan bunyi yang berterusan untuk bilik livechat baru", "Conversation": "Perbualan", "Conversation_closed": "Perbualan ditutup: __comment__.", + "Conversation_finished": "perbualan selesai", "Conversation_finished_message": "Mesej Selesai Perbualan", "conversation_with_s": "perbualan dengan %s", "Convert_Ascii_Emojis": "Menukar ASCII ke Emoji", @@ -1020,6 +1022,7 @@ "error-application-not-found": "Permohonan tidak dijumpai", "error-archived-duplicate-name": "Ada satu saluran yang diarkibkan dengan nama '__room_name__'", "error-avatar-invalid-url": "avatar URL tidak sah: __url__", + "error-avatar-url-handling": null, "error-cant-invite-for-direct-room": "tidak boleh menjemput pengguna ke bilik terus", "error-channels-setdefault-is-same": "Tetapan lalai saluran adalah sama dengan apa yang akan ditukar kepada.", "error-channels-setdefault-missing-default-param": "Diperlukan 'default' bodyParam", @@ -1681,6 +1684,7 @@ "Max_length_is": "Panjang maksimum ialah%s", "Media": "Media", "Medium": "Sederhana", + "Members": "Ahli", "Members_List": "Senarai Ahli", "mention-all": "Sebut semua", "mention-all_description": "Kebenaran untuk menggunakan sebutan @all", @@ -2305,6 +2309,7 @@ "Show_Setup_Wizard": "Tunjukkan Penyihir Persediaan", "Show_the_keyboard_shortcut_list": "Tunjukkan senarai pintasan papan kekunci", "Showing_archived_results": "

      Menunjukkan hasil yang diarkibkan %s

      ", + "Showing_online_users": null, "Showing_results": "

      Menunjukan %s keputusan

      ", "Sidebar": "Sidebar", "Sidebar_list_mode": "Mod Senarai Saluran Sidebar", @@ -2496,6 +2501,8 @@ "This_email_has_already_been_used_and_has_not_been_verified__Please_change_your_password": "E-mel ini telah digunakan dan tidak disahkan. Sila tukar kata laluan anda.", "This_is_a_desktop_notification": "Ini adalah pemberitahuan desktop", "This_is_a_push_test_messsage": "Ini adalah mesej supaya ujian push", + "This_room_has_been_archived_by__username_": "Bilik ini telah diarkibkan oleh __username__", + "This_room_has_been_unarchived_by__username_": "Bilik ini telah dinyaharkibkan oleh __username__", "Thursday": "Khamis", "Time_in_seconds": "Masa dalam saat", "Title": "Title", @@ -2599,7 +2606,10 @@ "Use_User_Preferences_or_Global_Settings": "Gunakan Keutamaan Pengguna atau Tetapan Global", "User": "Pengguna", "User__username__is_now_a_leader_of__room_name_": "Pengguna __username__ kini menjadi pemimpin __room_name__", + "User__username__is_now_a_moderator_of__room_name_": "__username__ pengguna kini merupakan moderator __room_name__", + "User__username__is_now_a_owner_of__room_name_": "__username__ pengguna kini merupakan pemilik __room_name__", "User__username__removed_from__room_name__leaders": "Pengguna __username__ dikeluarkan daripada pemimpin __room_name__", + "User__username__removed_from__room_name__moderators": "__username__ Pengguna dikeluarkan dari moderator __room_name__", "User_added": "Pengguna ditambah.", "User_added_by": "Pengguna __user_added__ ditambah oleh __user_by__.", "User_added_successfully": "Pengguna berjaya diletakkan", @@ -2666,6 +2676,7 @@ "Username_Change_Disabled": "pentadbir Rocket.Chat anda telah melumpuhkan perubahan nama pengguna", "Username_description": "Nama Pengguna adalah digunakan untuk membolehkan pengguna lain menyebut anda di mesej.", "Username_doesnt_exist": "Nama pengguna ' %s` tidak wujud.", + "Username_ended_the_OTR_session": "__username__ mengakhiri sesi OTR yang", "Username_invalid": "%s bukan nama pengguna yang sah,
      guna hanya huruf, nombor, titik dan pemisah", "Username_is_already_in_here": "`@%s` sudah berada di sini.", "Username_is_not_in_this_room": "Pengguna `#%s` tidak ada di dalam bilik ini.", @@ -2675,6 +2686,7 @@ "Users_added": "Pengguna telah ditambah", "Users_in_role": "Pengguna dalam peranan", "UTF8_Names_Slugify": "UTF8 Nama Slugify", + "Videocall_enabled": "Panggilan Video Dihidupkan", "Validate_email_address": "Mengesahkan Alamat E-mel", "Verification": "Pengesahan", "Verification_Description": "Anda boleh menggunakan pemegang tempat berikut:
      • [Verification_Url] untuk URL pengesahan.
      • [nama], [fname], [lname] untuk nama penuh pengguna, nama pertama atau nama belakangnya masing-masing.
      • [email] untuk e-mel pengguna.
      • [Site_Name] dan [Site_URL] untuk Nama Aplikasi dan URL masing-masing.
      ", @@ -2689,7 +2701,6 @@ "Video_Conference": "Persidangan Video", "Video_message": "Mesej video", "Videocall_declined": "Panggilan Video Ditolak.", - "Videocall_enabled": "Panggilan Video Dihidupkan", "View_All": "Lihat Semua", "View_Logs": "Lihat Balak", "View_mode": "mod lihat", @@ -2811,4 +2822,4 @@ "Your_push_was_sent_to_s_devices": "push anda telah dihantar ke peranti %s", "Your_server_link": "Pautan pelayan anda", "Your_workspace_is_ready": "Ruang kerja anda sedia untuk menggunakan 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/nl.i18n.json b/packages/rocketchat-i18n/i18n/nl.i18n.json index 97fd2aeee094..19acdbcb70c6 100644 --- a/packages/rocketchat-i18n/i18n/nl.i18n.json +++ b/packages/rocketchat-i18n/i18n/nl.i18n.json @@ -4,7 +4,7 @@ "__count__empty_rooms_will_be_removed_automatically": "__count__ lege kamers worden automatisch verwijderd.", "__count__empty_rooms_will_be_removed_automatically__rooms__": "__count__ lege kamers worden automatisch verwijderd:
      __rooms__", "__username__is_no_longer__role__defined_by__user_by_": "__username__ is niet langer __role__ door __user_by__", - "__username__was_set__role__by__user_by_": "__username__ is __role__ ingesteld door __user_by__", + "__username__was_set__role__by__user_by_": "__username__ werd ingesteld op __role__ door __user_by__", "This_room_encryption_has_been_enabled_by__username_": "De versleuteling van deze kamer werd ingeschakeld door __username__", "This_room_encryption_has_been_disabled_by__username_": "De versleuteling van deze kamer werd uitgeschakeld door __username__", "@username": "@gebruikersnaam", @@ -25,8 +25,8 @@ "Accept_with_no_online_agents": "Accepteer zonder online agenten", "Access_not_authorized": "Toegang niet toegestaan", "Access_Token_URL": "Toegangstoken URL", - "access-mailer": "Open het Mailer-scherm", - "access-mailer_description": "Toestemming voor het verzenden van massa-e-mail naar alle gebruikers.", + "access-mailer": "Toegang tot het Mailer-scherm", + "access-mailer_description": "Toestemming om massa-e-mail naar alle gebruikers te verzenden.", "access-permissions": "Open het toegangsrechten scherm", "access-permissions_description": "Wijzig de rechten voor verschillende rollen.", "access-setting-permissions": "Wijzig machtigingen op basis van instellingen", @@ -114,6 +114,8 @@ "Accounts_OAuth_Custom_Merge_Users": "Gebruikers samenvoegen", "Accounts_OAuth_Custom_Name_Field": "Naam veld", "Accounts_OAuth_Custom_Roles_Claim": "Rol / Groepen veldnaam", + "Accounts_OAuth_Custom_Roles_To_Sync": "Rollen die moeten worden gesynchroniseerd", + "Accounts_OAuth_Custom_Roles_To_Sync_Description": "Te synchroniseren OAuth rollen bij het inloggen en aanmaken van gebruikers (gescheiden door komma's).", "Accounts_OAuth_Custom_Scope": "Reikwijdte", "Accounts_OAuth_Custom_Secret": "Geheim", "Accounts_OAuth_Custom_Show_Button_On_Login_Page": "Knop weergeven op inlogpagina", @@ -370,6 +372,8 @@ "API_Enable_Rate_Limiter_Dev": "Schakel Rate Limiter in ontwikkeling in", "API_Enable_Rate_Limiter_Dev_Description": "Moet het aantal oproepen naar de eindpunten in de ontwikkelomgeving worden beperkt?", "API_Enable_Rate_Limiter_Limit_Calls_Default": "Standaardnummeroproepen naar de snelheidsbegrenzer", + "Rate_Limiter_Limit_RegisterUser": "Standaard aantal oproepen naar de snelheidsbegrenzer (rate limiter) voor het registreren van een gebruiker", + "Rate_Limiter_Limit_RegisterUser_Description": "Aantaal standaardoproepen voor gebruikersregistratie-eindpunten (REST en realtime API's), toegestaan binnen het tijdbereik dat is gedefinieerd in de sectie API Rate Limiter.", "API_Enable_Rate_Limiter_Limit_Calls_Default_Description": "Aantal standaardoproepen voor elk eindpunt van de REST API, toegestaan binnen het hieronder gedefinieerde tijdsbereik", "API_Enable_Rate_Limiter_Limit_Time_Default": "Standaard tijdslimiet voor de snelheidsbegrenzer (in ms)", "API_Enable_Rate_Limiter_Limit_Time_Default_Description": "Standaard time-out om het aantal oproepen op elk eindpunt van de REST API te beperken (in ms)", @@ -385,13 +389,14 @@ "API_Personal_Access_Tokens_Regenerate_Modal": "Als je de token kwijt bent of vergeet, dan kan je deze opnieuw genereren, maar vergeet niet dat alle applicaties die deze token gebruiken, moeten worden bijgewerkt", "API_Personal_Access_Tokens_Remove_Modal": "Weet u zeker dat u dit persoonlijke toegangstoken wilt verwijderen?", "API_Personal_Access_Tokens_To_REST_API": "Persoonlijke toegangstoken tot REST API", + "API_Rate_Limiter": "API Rate Limiter", "API_Shield_Types": "Schildtypen", "API_Shield_Types_Description": "Typen schilden die kunnen worden ingeschakeld als een door komma's gescheiden lijst, kies `online`, `kanaal` of `*` voor iedereen", "API_Shield_user_require_auth": "Verificatie vereisen voor gebruikersschilden", "API_Token": "API Token", "API_Tokenpass_URL": "Tokenpass-server-URL", "API_Tokenpass_URL_Description": "Voorbeeld: https://domain.com (exclusief trailing slash)", - "API_Upper_Count_Limit": "Max. Recordbedrag", + "API_Upper_Count_Limit": "Max. recordbedrag", "API_Upper_Count_Limit_Description": "Wat is het maximale aantal records dat de REST-API moet teruggeven (wanneer niet onbeperkt)?", "API_Use_REST_For_DDP_Calls": "Gebruik REST in plaats van websocket voor Meteor-oproepen", "API_User_Limit": "Gebruikerslimiet voor het toevoegen van alle gebruikers aan een kaneel", @@ -401,21 +406,21 @@ "Apiai_Key": "Api.ai Key", "Apiai_Language": "Api.ai Taal", "APIs": "API's", - "App_author_homepage": "startpagina van de auteur", + "App_author_homepage": "auteur homepage", "App_Details": "Applicatie details", "App_Information": "App-informatie", "App_Installation": "App-installatie", "App_status_auto_enabled": "Ingeschakeld", "App_status_constructed": "Gebouwd", "App_status_disabled": "Uitgeschakeld", - "App_status_error_disabled": "Uitgeschakeld: niet-afgevangen fout", + "App_status_error_disabled": "Uitgeschakeld: niet opgevangen fout", "App_status_initialized": "Geïnitialiseerd", "App_status_invalid_license_disabled": "Uitgeschakeld: ongeldige licentie", "App_status_invalid_settings_disabled": "Uitgeschakeld: configuratie vereist", "App_status_manually_disabled": "Uitgeschakeld: handmatig", "App_status_manually_enabled": "Ingeschakeld", "App_status_unknown": "Onbekend", - "App_support_url": "ondersteuning voor URL's", + "App_support_url": "ondersteunings-URL", "App_Url_to_Install_From": "Installeer vanaf URL", "App_Url_to_Install_From_File": "Installeer vanuit bestand", "App_user_not_allowed_to_login": "App-gebruikers mogen niet rechtstreeks inloggen.", @@ -425,7 +430,7 @@ "Application_Name": "Naam van de toepassing", "Application_updated": "Applicatie bijgewerkt", "Apply": "Toepassen", - "Apply_and_refresh_all_clients": "Toepassen en alle cliënten opnieuw laden", + "Apply_and_refresh_all_clients": "Toepassen en alle klanten opnieuw laden", "Apps": "Apps", "Apps_Engine_Version": "Apps Engine-versie", "Apps_Essential_Alert": "Deze app is essentieel voor de volgende evenementen:", @@ -550,10 +555,10 @@ "assign-roles": "Rollen toewijzen", "assign-roles_description": "Toestemming om rollen toe te wijzen aan andere gebruikers", "at": "op", - "At_least_one_added_token_is_required_by_the_user": "Ten minste één toegevoegd token is vereist door de gebruiker", + "At_least_one_added_token_is_required_by_the_user": "De gebruiker heeft ten minste één toegevoegde token nodig", "AtlassianCrowd": "Atlassian Crowd", "Attachment_File_Uploaded": "Bestand geüpload", - "Attribute_handling": "Behandeling van attributen", + "Attribute_handling": "Attribuut behandeling", "Audio": "Audio", "Audio_message": "Audiobericht", "Audio_Notification_Value_Description": "Kan elk aangepast geluid zijn of de standaardgeluiden: piep, chelle, ding, droplet, highbell, seizoenen", @@ -701,6 +706,9 @@ "By_author": "Door __author__", "cache_cleared": "Cache gewist", "Call": "Bel", + "Call_declined": "Oproep geweigerd!", + "Call_provider": "Oproepprovider", + "Call_Already_Ended": "Oproep al beëindigd", "call-management": "Oproepbeheer", "call-management_description": "Toestemming om een vergadering te starten", "Caller": "Beller", @@ -734,10 +742,10 @@ "CAS_login_url_Description": "De inlog-URL van uw externe SSO-service, bijvoorbeeld: https://sso.example.undef/sso/login", "CAS_popup_height": "Hoogte aanmeldingspop-up", "CAS_popup_width": "Breedte aanmeldingspop-up", - "CAS_Sync_User_Data_Enabled": "Synchroniseer altijd gebruikersgegevens", + "CAS_Sync_User_Data_Enabled": "Altijd gebruikersgegevens synchroniseren", "CAS_Sync_User_Data_Enabled_Description": "Synchroniseer altijd externe CAS-gebruikersgegevens naar beschikbare attributen bij het inloggen. Opmerking: Kenmerken worden sowieso altijd gesynchroniseerd bij het maken van een account.", "CAS_Sync_User_Data_FieldMap": "Kenmerkkaart", - "CAS_Sync_User_Data_FieldMap_Description": "Gebruik deze JSON-invoer om interne attributen (sleutel) te bouwen op basis van externe attributen (waarde). Externe attribuutnamen ingesloten met '%' worden geïnterpoleerd in waardestrings.
      Voorbeeld, `{\" email \":\"% email% \",\" name \":\"% firstname%,% lastname% \"}`

      De kenmerkkaart wordt altijd geïnterpoleerd. In CAS 1.0 is alleen het kenmerk `gebruikersnaam 'beschikbaar. Beschikbare interne kenmerken zijn: gebruikersnaam, naam, e-mail, kamers; rooms is een door komma's gescheiden lijst van kamers om deel te nemen na het maken van de gebruiker, bijvoorbeeld: {\"rooms\": \"% team%,% department%\"} zou CAS-gebruikers bij de creatie vergezellen naar hun team- en afdelingskanaal.", + "CAS_Sync_User_Data_FieldMap_Description": "Gebruik deze JSON-invoer om interne attributen (sleutel) te bouwen op basis van externe attributen (waarde). Externe attribuutnamen ingesloten met '%' worden geïnterpoleerd in waardestrings.
      Voorbeeld, `{\"email\":\"%email%\", \"name\":\"%firstname%, %lastname%\"}`

      De kenmerkkaart wordt altijd geïnterpoleerd. In CAS 1.0 is alleen het `username` attribuut beschikbaar. Beschikbare interne attributen zijn: username, name, email, rooms; rooms is een door komma's gescheiden lijst van kamers om lid van te worden bij het aanmaken van een gebruiker, bijvoorbeeld: {\"rooms\": \"%team%,%department%\"} zou CAS-gebruikers bij het aanmaken toevoegen aan hun team- en afdelingskanaal.", "CAS_trust_username": "Vertrouw op CAS-gebruikersnaam", "CAS_trust_username_description": "Indien ingeschakeld, vertrouwt Rocket.Chat erop dat elke gebruikersnaam van CAS toebehoort aan dezelfde gebruiker op Rocket.Chat.
      Dit kan nodig zijn als de naam van een gebruiker wordt gewijzigd op CAS, maar het kan mensen ook toestaan de controle over Rocket.Chat-accounts te nemen door hun eigen CAS-gebruikers te hernoemen.", "CAS_version": "CAS-versie", @@ -760,8 +768,8 @@ "Channel_created": "Kanaal `#%s` aangemaakt.", "Channel_doesnt_exist": "Het kanaal `#%s` bestaat niet.", "Channel_Export": "Kanaal exporteren", - "Channel_name": "Kanaal naam", - "Channel_Name_Placeholder": "Voer de kanaalnaam in...", + "Channel_name": "Kanaalnaam", + "Channel_Name_Placeholder": "Voer kanaalnaam in...", "Channel_to_listen_on": "Kanaal om op te luisteren", "Channel_Unarchived": "Kanaal met de naam `#%s` is succesvol uit het archief gehaald", "Channels": "Kanalen", @@ -802,7 +810,7 @@ "Chatpal_Batch_Size_Description": "De batchgrootte van indexdocumenten (bij bootstrapping)", "Chatpal_channel_not_joined_yet": "Kanaal is nog geen lid", "Chatpal_create_key": "Sleutel aanmaken", - "Chatpal_created_key_successfully": "API-sleutel is succesvol aangemaakt", + "Chatpal_created_key_successfully": "API-sleutel succesvol aangemaakt", "Chatpal_Current_Room_Only": "Zelfde kamer", "Chatpal_Default_Result_Type": "Standaard resultaattype", "Chatpal_Default_Result_Type_Description": "Bepaalt welk resultaattype wordt weergegeven per resultaat. Alles betekent dat een overzicht voor alle typen wordt gegeven.", @@ -815,7 +823,7 @@ "Chatpal_Get_more_information_about_chatpal_on_our_website": "Meer informatie over Chatpal op http://chatpal.io!", "Chatpal_go_to_message": "Springen", "Chatpal_go_to_room": "Springen", - "Chatpal_go_to_user": "Direct bericht sturen", + "Chatpal_go_to_user": "Direct bericht verzenden", "Chatpal_HTTP_Headers": "HTTP-headers", "Chatpal_HTTP_Headers_Description": "Lijst met HTTP-headers, één header per regel. Formaat: naam: waarde", "Chatpal_Include_All_Public_Channels": "Inclusief alle openbare kanalen", @@ -851,7 +859,7 @@ "Choose_the_username_that_this_integration_will_post_as": "Kies de gebruikersnaam die deze integratie zal posten.", "Choose_users": "Kies gebruikers", "Clean_Usernames": "Gebruikersnamen wissen", - "clean-channel-history": "Maak kanaalgeschiedenis schoon", + "clean-channel-history": "Kanaalgeschiedenis wissen", "clean-channel-history_description": "Toestemming om de geschiedenis van kanalen te wissen", "clear": "Wissen", "Clear_all_unreads_question": "Alle ongelezen berichten wissen?", @@ -1340,6 +1348,7 @@ "Days": "Dagen", "DB_Migration": "Database Migratie", "DB_Migration_Date": "Database Migratie Datum", + "DDP_Rate_Limit": "DDP Rate Limit", "DDP_Rate_Limit_Connection_By_Method_Enabled": "Beperking per verbinding per methode: ingeschakeld", "DDP_Rate_Limit_Connection_By_Method_Interval_Time": "Beperking door verbinding per methode: intervaltijd", "DDP_Rate_Limit_Connection_By_Method_Requests_Allowed": "Beperking door verbinding per methode: verzoeken toegestaan", @@ -1403,7 +1412,7 @@ "Desktop_Notifications_Enabled": "Desktopmeldingen zijn ingeschakeld", "Desktop_Notifications_Not_Enabled": "Bureaubladmeldingen zijn niet ingeschakeld", "Details": "Details", - "Different_Style_For_User_Mentions": "Verschillende stijl voor gebruikersvermeldingen", + "Different_Style_For_User_Mentions": "Andere stijl voor gebruikersvermeldingen", "Direct_Message": "Privébericht", "Direct_message_creation_description": "U staat op het punt een chat te starten met meerdere gebruikers. Voeg degene toe met wie u wilt praten, iedereen op dezelfde plaats, via directe berichten.", "Direct_message_someone": "Stuur iemand een privébericht", @@ -1427,7 +1436,7 @@ "Direct_Reply_Separator": "Scheidingsteken", "Direct_Reply_Separator_Description": "[Alleen wijzigen als u precies weet wat u aan het doen bent, raadpleeg de documentatie]
      Separator tussen basis- en taggedeelte van e-mail", "Direct_Reply_Username": "Gebruikersnaam", - "Direct_Reply_Username_Description": "Gebruik alstublieft absolute e-mail, tagging is niet toegestaan, het zou overschreven worden", + "Direct_Reply_Username_Description": "Gebruik absolute e-mail, tagging is niet toegestaan, het zou worden overschreven", "Directory": "Directory", "Disable": "Uitschakelen", "Disable_Facebook_integration": "Schakel Facebook-integratie uit", @@ -1609,6 +1618,8 @@ "Encryption_key_saved_successfully": "Uw coderingssleutel is succesvol opgeslagen.", "EncryptionKey_Change_Disabled": "U kunt geen wachtwoord instellen voor uw coderingssleutel omdat uw privésleutel niet aanwezig is op de client. Om een nieuw wachtwoord in te stellen, moet u uw privésleutel laden met uw bestaande wachtwoord of een client gebruiken waarop de sleutel al is geladen.", "End": "Einde", + "End_call": "Oproep beëindigen", + "Expand_view": "Weergave uitvouwen", "End_OTR": "Stop OTR", "Engagement_Dashboard": "Betrokkenheidsdashboard", "Enter": "Enter", @@ -1836,7 +1847,9 @@ "Favorite": "Favoriete", "Favorite_Rooms": "Schakel favoriete kamers in", "Favorites": "Favorieten", + "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "Deze functie is afhankelijk van de hierboven geselecteerde oproepprovider die moet worden ingeschakeld via de beheerinstellingen.", "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "Deze functie is afhankelijk van het feit of 'Navigatiegeschiedenis van bezoeker als bericht verzenden' is ingeschakeld.", + "Feature_Limiting": "Functiebeperking", "Features": "Functies", "Features_Enabled": "Ingeschakelde functies", "Feature_Disabled": "Functie uitgeschakeld", @@ -2012,7 +2025,7 @@ "force-delete-message_description": "Toestemming om een bericht te verwijderen waarbij alle beperkingen worden omzeild", "Forgot_password": "Wachtwoord vergeten?", "Forgot_Password_Description": "U kunt de volgende placeholders gebruiken:
      • [Forgot_Password_Url] voor de URL voor wachtwoordherstel.
      • [naam], [fname], [lname] voor de volledige naam, voornaam of achternaam van de gebruiker, respectievelijk.
      • [email] voor het e-mailadres van de gebruiker.
      • [Site_Name] en [Site_URL] voor respectievelijk de applicatienaam en URL.
      ", - "Forgot_Password_Email": "Klik hierom uw wachtwoord opnieuw in te stellen.", + "Forgot_Password_Email": "Klik hier om uw wachtwoord opnieuw in te stellen.", "Forgot_Password_Email_Subject": "[Site_Name] - Wachtwoordherstel", "Forgot_password_section": "Wachtwoord vergeten", "Forward": "Doorsturen", @@ -2089,6 +2102,7 @@ "Hide_System_Messages": "Verberg systeemberichten", "Hide_Unread_Room_Status": "Verberg ongelezen kamerstatus", "Hide_usernames": "Gebruikersnamen verbergen", + "Hide_video": "Video verbergen", "Highlights": "Hoogtepunten", "Highlights_How_To": "Als u een melding wilt ontvangen wanneer iemand een woord of zin noemt, voegt u deze hier toe. U kunt woorden of woordgroepen met komma's scheiden. Markeringswoorden zijn niet hoofdlettergevoelig.", "Highlights_List": "Highlight woorden", @@ -2100,7 +2114,7 @@ "Hours": "Uren", "How_friendly_was_the_chat_agent": "Hoe vriendelijk was de chatagent?", "How_knowledgeable_was_the_chat_agent": "Hoe deskundig was de chatagent?", - "How_long_to_wait_after_agent_goes_offline": "Hoelang gewacht moet worden nadat agent offline is gegaan", + "How_long_to_wait_after_agent_goes_offline": "Hoe lang te wachten nadat de agent offline gaat", "How_long_to_wait_to_consider_visitor_abandonment": "Hoe lang moet je wachten voordat je overweegt dat de bezoeker het heeft verlaten?", "How_long_to_wait_to_consider_visitor_abandonment_in_seconds": "Hoe lang moet je wachten voordat je overweegt dat de bezoeker het heeft verlaten?", "How_responsive_was_the_chat_agent": "Hoe responsief was de chatagent?", @@ -2148,16 +2162,16 @@ "Importer_finishing": "Afwerking van de import.", "Importer_From_Description": "Impoort __from__-gegevens in Rocket.Chat.", "Importer_HipChatEnterprise_BetaWarning": "Houd er rekening mee dat deze import nog steeds in uitvoering is, meld eventuele fouten die optreden op GitHub:", - "Importer_HipChatEnterprise_Information": "Het geüploade bestand moet een ontsleutelde tar.gz zijn, lees de documentatie voor meer informatie:", + "Importer_HipChatEnterprise_Information": "Het geüploade bestand moet een gedecodeerde tar.gz zijn, lees de documentatie voor meer informatie:", "Importer_import_cancelled": "Import geannuleerd.", "Importer_import_failed": "Er is een fout opgetreden tijdens het importeren.", - "Importer_importing_channels": "De kanalen importeren.", + "Importer_importing_channels": "Kanalen aan het importeren.", "Importer_importing_files": "Importeren van de bestanden.", "Importer_importing_messages": "De berichten importeren.", - "Importer_importing_started": "Starten van de import.", + "Importer_importing_started": "Het importeren starten.", "Importer_importing_users": "De gebruikers importeren.", "Importer_not_in_progress": "De importeur is momenteel niet actief.", - "Importer_not_setup": "De importeur is niet correct ingesteld, omdat er geen gegevens zijn geretourneerd.", + "Importer_not_setup": "De importeur is niet correct ingesteld, omdat hij geen gegevens heeft geretourneerd.", "Importer_Prepare_Restart_Import": "Start de import opnieuw", "Importer_Prepare_Start_Import": "Begin met importeren", "Importer_Prepare_Uncheck_Archived_Channels": "Haal het vinkje weg van gearchiveerde kanalen", @@ -2204,7 +2218,7 @@ "Install": "Installeren", "Install_Extension": "Installeer extensie", "Install_FxOs": "Installeer Rocket.Chat op uw Firefox", - "Install_FxOs_done": "Super! Je kunt Rocket.Chat nu gebruiken via het icoontje op je homescherm. Veel plezier met Rocket.Chat!", + "Install_FxOs_done": "Super! Je kunt nu Rocket.Chat gebruiken via het icoontje op je homescherm. Veel plezier met Rocket.Chat!", "Install_FxOs_error": "Sorry, dat werkte niet zoals de bedoeling! De volgende fout is opgetreden:", "Install_FxOs_follow_instructions": "Bevestig de installatie van de app op uw apparaat (druk op \"Installeren\" wanneer daar om gevraagd wordt).", "Install_package": "Installeer pakket", @@ -2214,7 +2228,7 @@ "Instance": "Instantie", "Instances": "Instanties", "Instances_health": "Gezondheid van instanties", - "Instance_Record": "Record instantie", + "Instance_Record": "Instantierecord", "Instructions": "Instructies", "Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Instructies voor uw bezoeker vul het formulier in om een bericht te verzenden", "Insert_Contact_Name": "Voer de naam van het contact in", @@ -2236,12 +2250,12 @@ "Integration_Outgoing_WebHook_History_Http_Response_Error": "HTTP-antwoordfout", "Integration_Outgoing_WebHook_History_Messages_Sent_From_Prepare_Script": "Berichten verzonden vanuit de voorbereidingsstap", "Integration_Outgoing_WebHook_History_Messages_Sent_From_Process_Script": "Berichten verzonden vanuit procesantwoordstap", - "Integration_Outgoing_WebHook_History_Time_Ended_Or_Error": "Tijd dat het eindigde of een fout maakte", + "Integration_Outgoing_WebHook_History_Time_Ended_Or_Error": "Tijd dat het eindigde of fout ging", "Integration_Outgoing_WebHook_History_Time_Triggered": "Tijdintegratie geactiveerd", "Integration_Outgoing_WebHook_History_Trigger_Step": "Laatste activeringsstap", "Integration_Outgoing_WebHook_No_History": "Deze uitgaande webhook-integratie heeft nog geen geschiedenis geregistreerd.", "Integration_Retry_Count": "Aantal pogingen", - "Integration_Retry_Count_Description": "Hoe vaak moet de integratie worden geprobeerd als de aanroep naar de url mislukt?", + "Integration_Retry_Count_Description": "Hoe vaak moet de integratie worden geprobeerd als de oproep naar de url mislukt?", "Integration_Retry_Delay": "Wachttijd nieuwe poging", "Integration_Retry_Delay_Description": "Welk vertragingsalgoritme moet de nieuwe poging gebruiken? 10^xof 2^x of x*2", "Integration_Retry_Failed_Url_Calls": "Probeer mislukte URL-oproepen opnieuw", @@ -2291,7 +2305,7 @@ "Invitation": "Uitnodiging", "Invitation_Email_Description": "U kunt de volgende variabels gebruiken:
      • [email] voor het e-mailadres van de ontvanger.
      • [Site_Name] en [Site_URL] voor respectievelijk de applicatienaam en URL.
      ", "Invitation_HTML": "Uitnodiging HTML", - "Invitation_HTML_Default": "

      Je bent uitgenodigd voor [Site_Name]

      Ga naar [Site_URL] en probeer de beste open source chat-oplossing die vandaag beschikbaar is!

      ", + "Invitation_HTML_Default": "

      Je bent uitgenodigd voor [Site_Name]

      Ga naar [Site_URL] en probeer de beste open source chatoplossing die vandaag beschikbaar is!

      ", "Invitation_Subject": "Uitnodiging onderwerp", "Invitation_Subject_Default": "Je bent uitgenodigd voor [Site_Name]", "Invite": "Nodig uit", @@ -2338,12 +2352,14 @@ "Jitsi_Limit_Token_To_Room": "Beperk het token tot Jitsi Room", "Job_Title": "Functietitel", "join": "Toetreden", + "Join_call": "Deelnemen aan gesprek", "Join_audio_call": "Deelnemen aan audiogesprek", "Join_Chat": "Met chat meedoen", "Join_default_channels": "Word lid van standaardkanalen", "Join_the_Community": "Word lid van de community", "Join_the_given_channel": "Word lid van het gegeven kanaal", "Join_video_call": "Deelnemen aan videogesprek", + "Join_my_room_to_start_the_video_call": "Word lid van mijn kamer om het videogesprek te starten", "join-without-join-code": "Word lid zonder deelnamecode", "join-without-join-code_description": "Toestemming om de deelnamecode te omzeilen in kanalen waarvoor join-code is ingeschakeld", "Joined": "Toegetreden", @@ -2352,7 +2368,7 @@ "Jump_to_first_unread": "Ga naar eerste ongelezen", "Jump_to_message": "Ga naar bericht", "Jump_to_recent_messages": "Ga naar recente berichten", - "Just_invited_people_can_access_this_channel": "Alleen uitgenodige mensen hebben toegang tot dit kanaal.", + "Just_invited_people_can_access_this_channel": "Alleen uitgenodigde mensen hebben toegang tot dit kanaal.", "Katex_Dollar_Syntax": "Dollar-syntaxis toestaan", "Katex_Dollar_Syntax_Description": "Sta het gebruik van $$katex block$$ en $inline katex$ syntaxis toe", "Katex_Enabled": "Katex ingeschakeld", @@ -2372,7 +2388,7 @@ "Keyboard_Shortcuts_Mark_all_as_read": "Markeer alle berichten (in alle kanalen) als gelezen", "Keyboard_Shortcuts_Move_To_Beginning_Of_Message": "Ga naar het begin van het bericht", "Keyboard_Shortcuts_Move_To_End_Of_Message": "Ga naar het einde van het bericht", - "Keyboard_Shortcuts_New_Line_In_Message": "Nieuwe regel in bericht opstellen", + "Keyboard_Shortcuts_New_Line_In_Message": "Nieuwe regel in invoer bericht opstellen", "Keyboard_Shortcuts_Open_Channel_Slash_User_Search": "Open kanaal / gebruiker zoeken", "Keyboard_Shortcuts_Title": "Toetsenbord sneltoetsen", "Knowledge_Base": "Kennis basis", @@ -2551,6 +2567,10 @@ "LDAP_Sync_User_Data_Roles_Filter_Description": "Het LDAP-zoekfilter dat wordt gebruikt om te controleren of een gebruiker deel uitmaakt van een groep.", "LDAP_Sync_User_Data_RolesMap": "User Data Group Map", "LDAP_Sync_User_Data_RolesMap_Description": "Wijs LDAP-groepen toe aan Rocket.Chat-gebruikersrollen
      Als voorbeeld zal `{\"rocket-admin\":\"admin\", \"tech-support\":\"support\"}` de rocket-admin LDAP-groep toewijzen aan Rocket's rol \"admin\".", + "LDAP_Teams_BaseDN": "LDAP-teams BaseDN", + "LDAP_Teams_BaseDN_Description": "De LDAP BaseDN gebruikt om gebruikersteams op te zoeken.", + "LDAP_Teams_Name_Field": "LDAP-teamnaam attribuut", + "LDAP_Teams_Name_Field_Description": "Het LDAP-attribuut dat Rocket.Chat moet gebruiken om de teamnaam te laden. Je kunt meer dan één mogelijke attribuutnaam opgeven als je ze scheidt met een komma.", "LDAP_Timeout": "Time-out (ms)", "LDAP_Timeout_Description": "Hoeveel milliseconden wachten op een zoekresultaat voordat een fout wordt geretourneerd", "LDAP_Unique_Identifier_Field": "Uniek identificatieveld", @@ -2579,7 +2599,7 @@ "Leave_Room_Warning": "Weet je zeker dat je het kanaal \"%s\" wilt verlaten?", "Leave_the_current_channel": "Verlaat het huidige kanaal", "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "Laat het beschrijvingsveld leeg als u de rol niet wilt weergeven", - "leave-c": "Verlaat kanalen", + "leave-c": "Kanalen verlaten", "leave-c_description": "Toestemming om kanalen te verlaten", "leave-p": "Verlaat privégroepen", "leave-p_description": "Toestemming om privégroepen te verlaten", @@ -2628,6 +2648,7 @@ "Livechat_Managers": "Managers", "Livechat_max_queue_wait_time_action": "Hoe om te gaan met chats in de wachtrij wanneer de maximale wachttijd is bereikt", "Livechat_maximum_queue_wait_time": "Maximale wachttijd in wachtrij", + "Livechat_maximum_queue_wait_time_description": "Maximale tijd (in minuten) om chats in de wachtrij te houden. -1 betekent onbeperkt", "Livechat_message_character_limit": "Tekenlimiet voor Livechat-bericht", "Livechat_monitors": "Livechat-monitoren", "Livechat_Monitors": "Monitoren", @@ -2663,6 +2684,7 @@ "Livechat_Triggers": "Livechat-triggers", "Livechat_user_sent_chat_transcript_to_visitor": "__agent__ heeft het chattranscript naar __guest__ gestuurd", "Livechat_Users": "Omnichannel-gebruikers", + "Livechat_Calls": "Livechat-oproepen", "Livechat_visitor_email_and_transcript_email_do_not_match": "E-mailadres van bezoeker en transcriptie-e-mailadres komen niet overeen", "Livechat_visitor_transcript_request": "__guest__ heeft het chattranscript aangevraagd", "LiveStream & Broadcasting": "LiveStream & Broadcasting", @@ -2799,6 +2821,7 @@ "Markdown_Parser": "Markdown Parser", "Markdown_SupportSchemesForLink": "Markdown-ondersteuningsregelingen voor Link", "Markdown_SupportSchemesForLink_Description": "Door komma's gescheiden lijst met toegestane schema's", + "Marketplace": "Marktplaats", "Marketplace_view_marketplace": "Marketplace bekijken", "MAU_value": "MAU __value__", "Max_length_is": "De maximale lengte is %s", @@ -2972,6 +2995,8 @@ "Mobex_sms_gateway_restful_address_desc": "IP of Host van uw Mobex REST API, bijv. `http://192.168.1.1:8080` of `https://www.example.com:8080`", "Mobex_sms_gateway_username": "Gebruikersnaam", "Mobile": "Mobiel", + "mobile-download-file": "Downloaden van bestanden op mobiele apparaten toestaan", + "mobile-upload-file": "Uploaden van bestanden op mobiele apparaten toestaan", "Mobile_Push_Notifications_Default_Alert": "Standaardwaarschuwing pushmeldingen", "Monday": "Maandag", "Mongo_storageEngine": "Mongo Storage Engine", @@ -2996,11 +3021,13 @@ "Msgs": "Berichten", "multi": "multi", "multi_line": "meerdere lijnen", + "Mute": "Dempen", "Mute_all_notifications": "Demp alle meldingen", "Mute_Focused_Conversations": "Demp gerichte gesprekken", "Mute_Group_Mentions": "Mute @all en @hier vermeldingen", "Mute_someone_in_room": "Demp iemand in de kamer", "Mute_user": "Gebruiker dempen", + "Mute_microphone": "Microfoon dempen", "mute-user": "Gebruiker dempen", "mute-user_description": "Toestemming om andere gebruikers in hetzelfde kanaal te dempen", "Muted": "Gedempt", @@ -3168,6 +3195,8 @@ "Omnichannel": "Omnichannel", "Omnichannel_Directory": "Omnichannel-directory", "Omnichannel_appearance": "Omnichannel-uiterlijk", + "Omnichannel_calculate_dispatch_service_queue_statistics": "Omnichannel-wachtrijstatistieken berekenen en verzenden", + "Omnichannel_calculate_dispatch_service_queue_statistics_Description": "Verwerken en verzenden van wachtrijstatistieken zoals positie en geschatte wachttijd. Als *Livechat-kanaal* niet in gebruik is, is het aan te raden om deze instelling uit te schakelen en te voorkomen dat de server onnodige processen uitvoert.", "Omnichannel_Contact_Center": "Omnichannel-contactcentrum", "Omnichannel_contact_manager_routing": "Wijs nieuwe gesprekken toe aan de contactmanager", "Omnichannel_contact_manager_routing_Description": "Deze instelling wijst een chat toe aan de toegewezen Contact Manager, zolang de Contact Manager online is wanneer de chat start", @@ -3178,6 +3207,7 @@ "Omnichannel_External_Frame_URL": "URL van externe frame", "On": "Aan", "On_Hold_Chats": "On-hold", + "On_Hold_conversations": "Gesprekken in de wacht", "online": "online", "Online": "Online", "Only_authorized_users_can_write_new_messages": "Alleen geautoriseerde gebruikers kunnen nieuwe berichten schrijven", @@ -3394,6 +3424,7 @@ "Query_description": "Aanvullende voorwaarden om te bepalen naar welke gebruikers de e-mail moet worden verzonden. Niet-geabonneerde gebruikers worden automatisch uit de zoekopdracht verwijderd. Het moet een geldige JSON zijn. Voorbeeld: \"{\"createdAt\":{\"$gt\":{\"$date\": \"2015-01-01T00:00:00.000Z\"}}}\"", "Query_is_not_valid_JSON": "Query is geen geldige JSON", "Queue": "Wachtrij", + "Queue_delay_timeout": "Wachtrij verwerking vertraging timeout", "Queue_Time": "Wachttijd", "Queue_management": "Wachtrijbeheer", "quote": "citaat", @@ -3822,6 +3853,7 @@ "Setup_Wizard": "Installatiewizard", "Setup_Wizard_Info": "We zullen u helpen bij het instellen van uw eerste admin-gebruiker, het configureren van uw organisatie en het registreren van uw server om gratis pushmeldingen en meer te ontvangen.", "Share_Location_Title": "Deel locatie?", + "Share_screen": "Scherm delen", "New_CannedResponse": "Nieuwe standaardreactie", "Edit_CannedResponse": "Standaardantwoord wijzigen", "Sharing": "Delen", @@ -3849,6 +3881,7 @@ "Show_room_counter_on_sidebar": "Toon de kamerteller op de zijbalk", "Show_Setup_Wizard": "Toon de installatiewizard", "Show_the_keyboard_shortcut_list": "Toon de lijst met sneltoetsen", + "Show_video": "Video weergeven", "Showing_archived_results": "

      Zichtbaar: %s gearchiveerde resultaten

      ", "Showing_online_users": "Toont: __total_showing__, online: __online__, totaal: __total__ gebruikers", "Showing_results": "

      Toon %s resultaten

      ", @@ -3897,7 +3930,7 @@ "Smileys_and_People": "Smileys & Mensen", "SMS": "sms", "SMS_Default_Omnichannel_Department": "Omnichannel-afdeling (standaard)", - "SMS_Default_Omnichannel_Department_Description": "Indien ingesteld, worden alle nieuwe inkomende chats die door deze integratie worden geïnitieerd, naar deze afdeling gestuurd.", + "SMS_Default_Omnichannel_Department_Description": "Indien ingesteld, worden alle nieuwe inkomende chats die door deze integratie worden gestart, naar deze afdeling gerouteerd.\nDeze instelling kan worden overschreven door de department query param in het verzoek door te geven.\nBijv. https:///api/v1/livechat/sms-incoming/twilio?department=.\nOpmerking: indien je afdelingsnaam gebruikt, moet de URL veilig zijn.", "SMS_Enabled": "SMS ingeschakeld", "SMTP": "SMTP", "SMTP_Host": "SMTP-host", @@ -3979,6 +4012,7 @@ "StatusMessage_Placeholder": "Wat doe je op dit moment?", "StatusMessage_Too_Long": "Statusbericht moet korter zijn dan 120 tekens.", "Step": "Stap", + "Stop_call": "Oproep stoppen", "Stop_Recording": "Stop opname", "Store_Last_Message": "Laatste bericht opslaan", "Store_Last_Message_Sent_per_Room": "Sla het laatste bericht op dat naar elke kamer is verzonden.", @@ -4270,6 +4304,8 @@ "Tuesday": "Dinsdag", "Turn_OFF": "Uitschakelen", "Turn_ON": "Aanzetten", + "Turn_on_video": "Video aanzetten", + "Turn_off_video": "Video uitschakelen", "Two Factor Authentication": "Twee-factorenauthenticatie", "Two-factor_authentication": "Tweefactorauthenticatie via TOTP", "Two-factor_authentication_disabled": "Tweefactorauthenticatie uitgeschakeld", @@ -4279,7 +4315,7 @@ "Two-factor_authentication_is_currently_disabled": "Tweefactorauthenticatie via TOTP is momenteel uitgeschakeld", "Two-factor_authentication_native_mobile_app_warning": "WAARSCHUWING: Zodra je dit hebt ingeschakeld, kun je niet inloggen op de native mobiele apps (Rocket.Chat+) met je wachtwoord totdat ze de 2FA implementeren.", "Type": "Type", - "typing": "typen", + "typing": "aan het typen", "Types": "Soorten", "Types_and_Distribution": "Types en distributie", "Type_your_email": "Typ uw e-mail", @@ -4315,6 +4351,7 @@ "Unit_removed": "Unit verwijderd", "Unknown_Import_State": "Onbekende importstatus", "Unlimited": "Onbeperkt", + "Unmute": "Dempen opheffen", "Unmute_someone_in_room": "Schakel het dempen van iemand in de kamer uit", "Unmute_user": "Dempen van gebruiker opheffen", "Unnamed": "Naamloos", @@ -4330,6 +4367,7 @@ "Unread_Rooms_Mode": "Ongelezen kamers-modus", "Unread_Tray_Icon_Alert": "Waarschuwing in systeemvak voor ongelezen berichten", "Unstar_Message": "Verwijder markering", + "Unmute_microphone": "Microfoon inschakelen", "Update": "Bijwerken", "Update_EnableChecker": "Update Checker inschakelen", "Update_EnableChecker_Description": "Controleert automatisch op nieuwe updates / belangrijke berichten van de Rocket.Chat-ontwikkelaars en ontvangt meldingen indien beschikbaar. De melding verschijnt één keer per nieuwe versie als een klikbare banner en als bericht van de Rocket.Cat-bot, beide alleen zichtbaar voor beheerders.", @@ -4490,6 +4528,7 @@ "UTF8_User_Names_Validation_Description": "RegExp dat zal worden gebruikt om gebruikersnamen te valideren", "UTF8_Channel_Names_Validation": "Validatie van UTF8-kanaalnamen", "UTF8_Channel_Names_Validation_Description": "Validatie van UTF8-kanaalnamen", + "Videocall_enabled": "Videogesprek ingeschakeld", "Validate_email_address": "E-mailadres valideren", "Validation": "Validatie", "Value_messages": "__value__ berichten", @@ -4511,10 +4550,12 @@ "Video_Conference": "Videoconferentie", "Video_message": "Videoboodschap", "Videocall_declined": "Videogesprek geweigerd.", - "Videocall_enabled": "Videogesprek ingeschakeld", + "Video_and_Audio_Call": "Video- en audiogesprek", "Videos": "Videos", "View_All": "Bekijk alle leden", "View_channels": "Bekijk kanalen", + "view-omnichannel-contact-center": "Omnichannel-contactcentrum bekijken", + "view-omnichannel-contact-center_description": "Toestemming om het Omnichannel-contactcentrum te bekijken en ermee te werken", "View_Logs": "Logboeken bekijken", "View_mode": "Weergavemodus", "View_original": "Bekijk origineel", @@ -4588,6 +4629,7 @@ "Visitor_message": "Bezoekersberichten", "Visitor_Name": "Bezoekersnaam", "Visitor_Name_Placeholder": "Voer een bezoekersnaam in...", + "Visitor_does_not_exist": "Bezoeker bestaat niet!", "Visitor_Navigation": "Bezoekersnavigatie", "Visitor_page_URL": "URL van bezoekerspagina", "Visitor_time_on_site": "Bezoeker tijd op de site", @@ -4615,6 +4657,7 @@ "Webhook_Details": "WebHook details", "Webhook_URL": "Webhook-URL", "Webhooks": "Webhooks", + "WebRTC_Call": "WebRTC-oproep", "WebRTC_direct_audio_call_from_%s": "Directe audiogesprek van %s", "WebRTC_direct_video_call_from_%s": "Direct videogesprek van %s", "WebRTC_Enable_Channel": "Inschakelen voor openbare kanalen", @@ -4625,6 +4668,8 @@ "WebRTC_monitor_call_from_%s": "Monitor oproep van %s", "WebRTC_Servers": "STUN / TURN Servers", "WebRTC_Servers_Description": "Een lijst met STUN- en TURN-servers gescheiden door komma's.
      Gebruikersnaam, wachtwoord en poort zijn toegestaan in de indeling `gebruikersnaam: wachtwoord@stun:host:poort` of `gebruikersnaam:wachtwoord@turn:host:poort`.", + "WebRTC_call_ended_message": " Gesprek beëindigd om __endTime__ - Duurde __callDuration__", + "WebRTC_call_declined_message": " Gesprek geweigerd door contact.", "Website": "Website", "Wednesday": "Woensdag", "Weekly_Active_Users": "Wekelijks actieve gebruikers", diff --git a/packages/rocketchat-i18n/i18n/no.i18n.json b/packages/rocketchat-i18n/i18n/no.i18n.json index 308f89c11c2d..9b48b55c553e 100644 --- a/packages/rocketchat-i18n/i18n/no.i18n.json +++ b/packages/rocketchat-i18n/i18n/no.i18n.json @@ -618,6 +618,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Kontinuerlige lydvarsler for nytt livechat-rom", "Conversation": "Samtale", "Conversation_closed": "Samtalen avsluttet: __comment__.", + "Conversation_finished": "Samtalen er avsluttet", "Conversation_finished_message": "Samtalen avsluttet melding", "conversation_with_s": "samtalen med %s", "Convert_Ascii_Emojis": "Konverter ASCII til Emoji", @@ -2784,6 +2785,7 @@ "Users_added": "Brukerne har blitt lagt til", "Users_in_role": "Brukere i rollen", "UTF8_Names_Slugify": "UTF8 Navn Slugify", + "Videocall_enabled": "Videoanrop aktivert", "Validate_email_address": "Bekreft e-postadresse", "Verification": "Bekreftelse", "Verification_Description": "Du kan bruke følgende plassholdere:
      • [Verification_Url] for verifikasjonsadressen.
      • [navn], [fname], [lname] for brukerens fulle navn, fornavn eller etternavn.
      • [email] for brukerens e-postadresse.
      • [Site_Name] og [Site_URL] for henholdsvis programnavnet og nettadressen.
      ", @@ -2798,7 +2800,6 @@ "Video_Conference": "Video konferanse", "Video_message": "Videomelding", "Videocall_declined": "Videoanrop avslått.", - "Videocall_enabled": "Videoanrop aktivert", "View_All": "Se alle medlemmer", "View_Logs": "Se logger", "View_mode": "Visningsmodus", diff --git a/packages/rocketchat-i18n/i18n/pl.i18n.json b/packages/rocketchat-i18n/i18n/pl.i18n.json index 9b6a1b8dbfba..8c8174f77739 100644 --- a/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -345,7 +345,7 @@ "API_Add_Personal_Access_Token": "Dodaj nowy osobisty token dostępowy", "API_Allow_Infinite_Count": "Pozwól uzyskać wszystko", "API_Allow_Infinite_Count_Description": "Czy połączenia z interfejsem REST API powinny zwracać wszystko w jednym wywołaniu?", - "API_Analytics": "Analityka", + "API_Analytics": "Analytics", "API_CORS_Origin": "CORS Origin", "API_Default_Count": "Domyślny licznik", "API_Default_Count_Description": "Domyślna liczba dla REST API wynika, jeśli konsument nie podał żadnych.", @@ -4392,6 +4392,7 @@ "UTF8_User_Names_Validation_Description": "RegExp, który będzie używany do sprawdzania poprawności nazw użytkowników", "UTF8_Channel_Names_Validation": "Walidacja nazw kanałów UTF8", "UTF8_Channel_Names_Validation_Description": "RegExp, który będzie używany do sprawdzania poprawności nazw kanałów", + "Videocall_enabled": "Rozmowa video uruchomiona", "Validate_email_address": "Sprawdź poprawność adresu e-mail", "Validation": "Walidacja", "Value_messages": "__value__ wiadomości", @@ -4413,7 +4414,6 @@ "Video_Conference": "Konferencja wideo", "Video_message": "Wiadomość wideo", "Videocall_declined": "Rozmowa video odrzucona.", - "Videocall_enabled": "Rozmowa video uruchomiona", "Videos": "Wideo", "View_All": "Pokaż wszystko", "View_channels": "Wyświetl kanały", diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 28b4089154bc..6f2e9780080e 100644 --- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -32,7 +32,7 @@ "access-setting-permissions": "Modifique as permissões baseadas em configuração", "access-setting-permissions_description": "Permissão para modificar permissões baseadas em configuração", "Accessing_permissions": "Acessando permissões", - "Account_SID": "Conta SID", + "Account_SID": "SID da Conta", "Accounts": "Contas", "Accounts_Admin_Email_Approval_Needed_Default": "

      O usuário [nome] ([email]) foi registrado.

      Verifique \"Administração ->Usuários\" para ativá-lo ou excluí-lo.

      ", "Accounts_Admin_Email_Approval_Needed_Subject_Default": "Um novo usuário se registrou e precisa de aprovação", @@ -43,14 +43,14 @@ "Accounts_AllowedDomainsList": "Lista de Domínios Permitidos", "Accounts_AllowedDomainsList_Description": "Lista de domínios permitidos, separados por vírgula", "Accounts_AllowInvisibleStatusOption": "Permitir opção de status Invisível", - "Accounts_AllowEmailChange": "Permitir alterar e-mail", + "Accounts_AllowEmailChange": "Permitir Alteração de E-mail", "Accounts_AllowEmailNotifications": "Permitir notificações por e-mail", "Accounts_AllowPasswordChange": "Permitir Alteração de Senha", "Accounts_AllowPasswordChangeForOAuthUsers": "Permitir alteração de senha para usuários OAuth", "Accounts_AllowRealNameChange": "Permitir Mudança de Nome", - "Accounts_AllowUserAvatarChange": "Permitir que O Usuário Troque O Avatar", + "Accounts_AllowUserAvatarChange": "Permitir que o Usuário Troque o Avatar", "Accounts_AllowUsernameChange": "Permitir Alterar Nome de Usuário", - "Accounts_AllowUserProfileChange": "Permitir Mudança no Perfil de Usuário", + "Accounts_AllowUserProfileChange": "Permitir Alteração no Perfil de Usuário", "Accounts_AllowUserStatusMessageChange": "Permitir Mensagem de Estado Personalizada", "Accounts_AvatarBlockUnauthenticatedAccess": "Bloquear acesso não autenticado aos avatares", "Accounts_AvatarCacheTime": "Tempo de cache do avatar", @@ -65,7 +65,7 @@ "Accounts_BlockedUsernameList_Description": "Lista de nomes de usuários bloqueados, separada por vírgulas (não diferencia maiúsculas)", "Accounts_CustomFields_Description": "Deve ser um JSON válido onde as chaves são os nomes de campos contendo um dicionário de configuração de campos. Exemplo:
      {\n \"role\": {\n \"type\": \"select\",\n \"defaultValue\": \"student\",\n \"options\": [\"teacher\", \"student\"],\n \"required\": true,\n \"modifyRecordField\": {\n \"array\": true,\n \"field\": \"roles\"\n }\n },\n \"twitter\": {\n \"type\": \"text\",\n \"required\": true,\n \"minLength\": 2,\n \"maxLength\": 10\n }\n}", "Accounts_CustomFieldsToShowInUserInfo": "Campos Personalizados para Exibir em Informação do Usuário", - "Accounts_Default_User_Preferences": "Preferências de Usuário Padrões", + "Accounts_Default_User_Preferences": "Preferências Padrão de Usuário", "Accounts_Default_User_Preferences_audioNotifications": "Áudio do Alerta de Notificação Padrão", "Accounts_Default_User_Preferences_desktopNotifications": "Alerta de Notificação para Desktop Padrão", "Accounts_Default_User_Preferences_pushNotifications": "Alerta de Notificação Push Padrão", @@ -79,7 +79,7 @@ "Accounts_Email_Approved_Subject": "Conta aprovada", "Accounts_Email_Deactivated": "[name]

      Sua conta foi desativada.

      ", "Accounts_Email_Deactivated_Subject": "Conta desativada", - "Accounts_EmailVerification": "Verificação de E-mail", + "Accounts_EmailVerification": "Permitir login apenas de usuários verificados", "Accounts_EmailVerification_Description": "Certifique-se de que possui definições SMTP corretas para usar este recurso", "Accounts_Enrollment_Email": "E-mail de Inscrição", "Accounts_Enrollment_Email_Default": "

      Bem-vindo ao [Site_Name]

      Vá para [Site_URL] e teste a melhor solução de bate-papo open source disponível!

      ", @@ -114,6 +114,8 @@ "Accounts_OAuth_Custom_Merge_Users": "Mesclar usuários", "Accounts_OAuth_Custom_Name_Field": "Campo de nome", "Accounts_OAuth_Custom_Roles_Claim": "Nome do campo de funções / grupos", + "Accounts_OAuth_Custom_Roles_To_Sync": "Papéis a Sincronizar", + "Accounts_OAuth_Custom_Roles_To_Sync_Description": "Papéis OAuth a sincronizar na autenticação e criação do usuário (separado por vírgulas)", "Accounts_OAuth_Custom_Scope": "Escopo", "Accounts_OAuth_Custom_Secret": "Secreto", "Accounts_OAuth_Custom_Show_Button_On_Login_Page": "Mostrar Botão na Página de Autenticação", @@ -370,7 +372,9 @@ "API_Enable_Rate_Limiter_Dev": "Ativar limitador de taxa em desenvolvimento", "API_Enable_Rate_Limiter_Dev_Description": "Deve limitar a quantidade de chamadas para os endpoints no ambiente de desenvolvimento?", "API_Enable_Rate_Limiter_Limit_Calls_Default": "Número padrão de chamadas para o limitador de taxa", - "API_Enable_Rate_Limiter_Limit_Calls_Default_Description": "Número de chamadas padrão para cada endpoint da API REST, permitido dentro do intervalo de tempo definido abaixo", + "Rate_Limiter_Limit_RegisterUser": "Número de chamadas para as endpoint de registro de usuário", + "Rate_Limiter_Limit_RegisterUser_Description": "Número de chamadas padrão para as endpoints de registro de usuário(API REST e real-time), permitido dentro do intervalo de tempo definido abaixo", + "API_Enable_Rate_Limiter_Limit_Calls_Default_Description": "Número de chamadas padrão para cada endpoint da API REST, permitido dentro do intervalo de tempo definido na seção de limitação de taxa.", "API_Enable_Rate_Limiter_Limit_Time_Default": "Limite de tempo padrão para o limitador de taxa (em ms)", "API_Enable_Rate_Limiter_Limit_Time_Default_Description": "Tempo limite padrão para limitar o número de chamadas em cada endpoint da API REST (em ms)", "API_Enable_Shields": "Ativar Protetores", @@ -385,6 +389,7 @@ "API_Personal_Access_Tokens_Regenerate_Modal": "Se perdeu ou esqueceu o seu código, pode recuperá-lo, mas lembre-se de que todos os aplicativos que usam esse código devem ser atualizados", "API_Personal_Access_Tokens_Remove_Modal": "Tem certeza de que deseja remover este código de acesso pessoal?", "API_Personal_Access_Tokens_To_REST_API": "Código de acesso pessoal para API REST", + "API_Rate_Limiter": "Limitação de Taxa de API", "API_Shield_Types": "Tipos de escudo", "API_Shield_Types_Description": "Tipos de escudos para habilitar como uma lista separada por vírgulas, escolha entre `online`, `channel` ou `*` para todos", "API_Shield_user_require_auth": "Exigir autenticaçāo para escudos de usuários", @@ -701,6 +706,9 @@ "By_author": "Por __author__", "cache_cleared": "Cache limpo", "Call": "Ligação", + "Call_declined": "Chamada Recusada!", + "Call_provider": "Provedor de Chamada", + "Call_Already_Ended": "Chamada já Encerrada", "call-management": "Gestão de Chamadas", "call-management_description": "Permissão para iniciar reunião", "Caller": "Caller", @@ -882,7 +890,7 @@ "close-others-livechat-room_description": "Permissão para fechar outras salas de Omnichannel", "Closed": "Fechado", "Closed_At": "Fechado em", - "Closed_automatically": "Fechado automaticamente pelo sistema", + "Closed_automatically": "Encerrado automaticamente pelo sistema", "Closed_automatically_chat_queued_too_long": "Encerrado automaticamente pelo sistema (tempo máximo de fila excedido)", "Closed_by_visitor": "Encerrado pelo visitante", "Closing_chat": "Encerrando chat", @@ -1286,7 +1294,7 @@ "Custom_Emoji_Added_Successfully": "Emoji customizado adicionado com sucesso", "Custom_Emoji_Delete_Warning": "A exclusão de um emoji não pode ser desfeita.", "Custom_Emoji_Error_Invalid_Emoji": "Emoji inválido", - "Custom_Emoji_Error_Name_Or_Alias_Already_In_Use": "O emoji customizado ou um dos seus apelidos já está em uso.", + "Custom_Emoji_Error_Name_Or_Alias_Already_In_Use": "O emoji personalizado ou um dos seus apelidos já está em uso.", "Custom_Emoji_Has_Been_Deleted": "O emoji personalizado foi excluído", "Custom_Emoji_Info": "Informações do emoji customizado", "Custom_Emoji_Updated_Successfully": "Emoji customizado atualizado com sucesso", @@ -1313,7 +1321,7 @@ "Custom_Sound_Saved_Successfully": "Som personalizado salvo com sucesso", "Custom_Sounds": "Sons personalizados", "Custom_Status": "Situação Personalizada", - "Custom_Translations": "Traduções personalizadas", + "Custom_Translations": "Traduções Personalizadas", "Custom_Translations_Description": "Deve ser um JSON válido onde as chaves são idiomas contendo um dicionário de chave e traduções. Exemplo:
      {\n \"en\": {\n \"Channels\": \"Rooms\"\n },\n\"pt\": {\n \"Channels\": \"Salas\"\n }\n}", "Custom_User_Status": "Situação Personalizada de Usuário", "Custom_User_Status_Add": "Adicionar Situação Personalizada de Usuário", @@ -1338,8 +1346,9 @@ "DAU_value": "DAU __value__", "days": "dias", "Days": "Dias", - "DB_Migration": "Migração de banco de dados", + "DB_Migration": "Migração de Banco de Dados", "DB_Migration_Date": "Data da migração do banco de dados", + "DDP_Rate_Limit": "Limitação de Taxa de DDP", "DDP_Rate_Limit_Connection_By_Method_Enabled": "Limite por Conexão por Método: Habilitado", "DDP_Rate_Limit_Connection_By_Method_Interval_Time": "Limite por Conexão por Método: Intervalo de tempo", "DDP_Rate_Limit_Connection_By_Method_Requests_Allowed": "Limite por Conexão por Método: Solicitações permitidas", @@ -1438,7 +1447,7 @@ "Disallow_reacting": "Não permitir reagir", "Disallow_reacting_Description": "Não permite reagir", "Discard": "Descartar", - "Disconnect": "Desconectado", + "Disconnect": "Desconectar", "Discussion": "Discussão", "Discussion_description": "Ajude a manter uma visão geral sobre o que está acontecendo! Ao criar uma discussão, um sub-canal do que selecionou é criado e ambos são ligados.", "Discussion_first_message_disabled_due_to_e2e": "Você pode começar a enviar mensagens criptografadas End-to-End nessa discussão após sua criação.", @@ -1609,6 +1618,8 @@ "Encryption_key_saved_successfully": "Sua chave de criptografia foi salva com sucesso.", "EncryptionKey_Change_Disabled": "Você não pode definir uma senha para sua chave de criptografia porque sua chave privada não está presente neste cliente. Para definir uma nova senha, você precisa inserir sua chave privada usando sua senha existente ou usar um cliente onde a chave já esteja em uso.", "End": "Fim", + "End_call": "Encerrar Chamada", + "Expand_view": "Expandir visão", "End_OTR": "Finalizar OTR", "Engagement_Dashboard": "Dashboard de Engajamento", "Enter": "Enter", @@ -1836,7 +1847,9 @@ "Favorite": "Adicionar aos Favoritos", "Favorite_Rooms": "Ativar Salas Favoritas", "Favorites": "Favoritos", + "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "Essa funcionalidade depende do provedor de chamada selecionado acima para ser habilitado nas configurações administrativas", "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "Esse recurso depende de \"Enviar histórico de navegação do visitante como uma mensagem\" para ser ativado.", + "Feature_Limiting": "Limitação de funcionalidades", "Features": "Funcionalidades", "Features_Enabled": "Funcionalidades habilitadas", "Feature_Disabled": "Funcionalidade desabilitada", @@ -2063,7 +2076,7 @@ "Group_discussions": "Discussões em grupo", "Group_favorites": "Grupos favoritos", "Group_mentions_disabled_x_members": "As menções de grupo `@all` e` @here` foram desativadas para salas com mais de __total__ membros.", - "Group_mentions_only": "Grupo menciona apenas", + "Group_mentions_only": "Apenas menções a Grupo", "Grouping": "Agrupamento", "Guest": "Convidado", "Hash": "Hash", @@ -2089,6 +2102,7 @@ "Hide_System_Messages": "Ocultar mensagens do sistema", "Hide_Unread_Room_Status": "Ocultar status da sala não lida", "Hide_usernames": "Ocultar nomes de usuário", + "Hide_video": "Ocultar Video", "Highlights": "Destaques", "Highlights_How_To": "Para ser notificado quando alguém menciona uma palavra ou frase, adicione-as aqui. Você pode separar palavras ou frases com vírgulas. Não há diferenciação de maiúsculas e minúsculas ao destacar palavras.", "Highlights_List": "Destacar palavras", @@ -2338,12 +2352,14 @@ "Jitsi_Limit_Token_To_Room": "Limitar token a Sala Jitsi", "Job_Title": "Cargo", "join": "Entrar", + "Join_call": "Participar da Chamada", "Join_audio_call": "Entrar na chamada de áudio", "Join_Chat": "Junte-se ao Chat", "Join_default_channels": "Entrar em canais predefinidos", "Join_the_Community": "Junte-se à Comunidade", "Join_the_given_channel": "Entrar no canal informado", "Join_video_call": "Entrar na chamada de vídeo", + "Join_my_room_to_start_the_video_call": "Participar da minha sala para iniciar chamada de vídeo", "join-without-join-code": "Cadastre-se sem se juntar ao código", "join-without-join-code_description": "Permissão para ignorar o código de associação em canais com o código de associação ativado", "Joined": "Entrou", @@ -2551,6 +2567,10 @@ "LDAP_Sync_User_Data_Roles_Filter_Description": "O filtro de busca LDAP usado para verificar se um usuário está em um grupo.", "LDAP_Sync_User_Data_RolesMap": "Mapeamento de Grupo de Dados de Usuário", "LDAP_Sync_User_Data_RolesMap_Description": "Mapeia grupos LDAP para papeis de usuário Rocket.Chat
      Por exemplo, `{\"rocket-admin\":\"admin\", \"suporte-tecnico\":\"suporte\"}` irá mapear o grupo LDAP rocket-admin para o papel \"admin\" do Rocket.Chat.", + "LDAP_Teams_BaseDN": "BaseDN LDAP de Equipes", + "LDAP_Teams_BaseDN_Description": "O BaseDN LDAP usado para procurar equipes do usuário", + "LDAP_Teams_Name_Field": "Atributo de Nome do Time LDAP", + "LDAP_Teams_Name_Field_Description": "O atributo LDAP que o Rocket.Chat deve usar para carregar o nome da equipe. Voce pode especificar mais de um nome de atributo possível se voce separá-los com uma vírgula.", "LDAP_Timeout": "Tempo limite (ms)", "LDAP_Timeout_Description": "Qual tempo limite esperar por um resultado de pesquisa antes de retornar um erro", "LDAP_Unique_Identifier_Field": "Campo Identificador Único", @@ -2628,6 +2648,7 @@ "Livechat_Managers": "Gerentes", "Livechat_max_queue_wait_time_action": "O que fazer com chats na fila quando tempo máximo de espera for atingido", "Livechat_maximum_queue_wait_time": "Tempo máximo de espera na fila", + "Livechat_maximum_queue_wait_time_description": "Tempo máximo (em minutos) para manter conversas na fila de espera. -1 para ilimitado", "Livechat_message_character_limit": "Limite de caracteres da mensagem no livechat", "Livechat_monitors": "Monitores de Livechat", "Livechat_Monitors": "Monitores", @@ -2663,6 +2684,7 @@ "Livechat_Triggers": "Gatilhos de Livechat", "Livechat_user_sent_chat_transcript_to_visitor": "__agent__ enviou a transcrição do bate-papo para __guest__", "Livechat_Users": "Usuários Omnichannel", + "Livechat_Calls": "Chamadas Livechat", "Livechat_visitor_email_and_transcript_email_do_not_match": "O email do visitante e o email da transcrição não correspondem", "Livechat_visitor_transcript_request": "__guest__ solicitou a transcrição do bate-papo", "LiveStream & Broadcasting": "LiveStream e Broadcasting", @@ -2799,6 +2821,7 @@ "Markdown_Parser": "Parser Markdown", "Markdown_SupportSchemesForLink": "Protocolos Suportados para Markdown de Links", "Markdown_SupportSchemesForLink_Description": "Lista de protocolos separados por vírgulas", + "Marketplace": "Marketplace", "Marketplace_view_marketplace": "Ver Marketplace", "MAU_value": "MAU __value__", "Max_length_is": "Tamanho máximo é %s", @@ -2882,7 +2905,7 @@ "Message_has_been_unpinned": "Mensagem foi desfixada", "Message_has_been_unstarred": "Mensagem foi desfavoritada", "Message_HideType_au": "Ocultar mensagens \"Usuário adicionado\"", - "Message_HideType_mute_unmute": "Ocultar mensagens \"Usuário silenciado / não modificado\"", + "Message_HideType_mute_unmute": "Ocultar mensagens \"Usuário silenciado / não silenciado\"", "Message_HideType_r": "Ocultar mensagens \"Nome da Sala alterado\"", "Message_HideType_rm": "Ocultar mensagens \"Mensagem Excluída\"", "Message_HideType_room_allowed_reacting": "Ocultar mensagens \"Permissão de reagir adicionada\"", @@ -2913,7 +2936,7 @@ "Message_MaxAllowedSize": "Tamanho máximo de mensagem permitido", "Message_pinning": "Fixação de mensagem", "message_pruned": "mensagem removida", - "Message_QuoteChainLimit": "Número máximo de citações acorrentadas", + "Message_QuoteChainLimit": "Número máximo de citações encadeadas", "Message_Read_Receipt_Enabled": "Mostrar Recibos de Leitura", "Message_Read_Receipt_Store_Users": "Recibos de leitura detalhados", "Message_Read_Receipt_Store_Users_Description": "Mostra os recibos de leitura de cada usuário", @@ -2972,6 +2995,8 @@ "Mobex_sms_gateway_restful_address_desc": "IP ou Host da API REST Mobex. Por exemplo, `http://192.168.1.1:8080` ou `https://www.example.com:8080`", "Mobex_sms_gateway_username": "Nome de Usuário", "Mobile": "Móvel", + "mobile-download-file": "Permitir download em dispositivos móveis", + "mobile-upload-file": "Permitir upload em dispositivos móveis", "Mobile_Push_Notifications_Default_Alert": "Alertas Padrão de Notificações Push", "Monday": "Segunda-feira", "Mongo_storageEngine": "Mongo Storage Engine", @@ -2996,11 +3021,13 @@ "Msgs": "Msgs", "multi": "multi", "multi_line": "linha múltipla", + "Mute": "Silenciar", "Mute_all_notifications": "Silenciar todas as notificações", "Mute_Focused_Conversations": "Conversas focalizadas em silêncio", "Mute_Group_Mentions": "Silenciar menções @all e @here", "Mute_someone_in_room": "Silenciar alguém na sala", "Mute_user": "Silenciar usuário", + "Mute_microphone": "Silenciar Microfone", "mute-user": "Silenciar usuário", "mute-user_description": "Permissão para silenciar outros usuários no mesmo canal", "Muted": "Silenciado", @@ -3180,6 +3207,7 @@ "Omnichannel_External_Frame_URL": "URL do Frame Externo", "On": "Em", "On_Hold_Chats": "Em Espera", + "On_Hold_conversations": "Conversas em espera", "online": "online", "Online": "Online", "Only_authorized_users_can_write_new_messages": "Somente usuários autorizados podem escrever novas mensagens", @@ -3768,6 +3796,7 @@ "Send_invitation_email_error": "Você não forneceu um e-mail válido.", "Send_invitation_email_info": "Você pode enviar vários convites por e-mail de uma vez.", "Send_invitation_email_success": "Você enviou com sucesso um convite por e-mail para os seguintes endereços:", + "Send_it_as_attachment_instead_question": "Enviar mensagem como anexo em vez disso?", "Send_me_the_code_again": "Envie-me o código novamente", "Send_request_on": "Enviar Requisição em", "Send_request_on_agent_message": "Enviar requisição para mensagens do Agente", @@ -3824,6 +3853,7 @@ "Setup_Wizard": "Assistente de configuração", "Setup_Wizard_Info": "Vamos apoiar na configuração do seu primeiro usuário administrador, na configuração da sua organização e no registro do servidor, para que possa receber notificações push gratuitas e muito mais.", "Share_Location_Title": "Compartilhar localização?", + "Share_screen": "Compartilhar tela", "New_CannedResponse": "Nova Resposta Modelo", "Edit_CannedResponse": "Editar Resposta Modelo", "Sharing": "Compartilhando", @@ -3851,6 +3881,7 @@ "Show_room_counter_on_sidebar": "Mostrar contador da sala na barra lateral", "Show_Setup_Wizard": "Mostrar assistente de configuração", "Show_the_keyboard_shortcut_list": "Mostrar a lista de atalhos de teclado", + "Show_video": "Exibir video", "Showing_archived_results": "

      Exibindo %s resultados arquivados

      ", "Showing_online_users": "Mostrando __total_showing__, Online: __online__, Total: __total__ usuários", "Showing_results": "

      Exibindo %s resultados

      ", @@ -3981,6 +4012,7 @@ "StatusMessage_Placeholder": "O que você está fazendo agora?", "StatusMessage_Too_Long": "A mensagem de status deve ter menos de 120 caracteres.", "Step": "Passo", + "Stop_call": "Interromper chamada", "Stop_Recording": "Parar Gravação", "Store_Last_Message": "Armazene a última mensagem", "Store_Last_Message_Sent_per_Room": "Armazene a última mensagem enviada em cada sala.", @@ -4272,6 +4304,8 @@ "Tuesday": "Terça-feira", "Turn_OFF": "Desligar", "Turn_ON": "Ligar", + "Turn_on_video": "Habilitar video", + "Turn_off_video": "Desabilitar video", "Two Factor Authentication": "Autenticação de dois fatores", "Two-factor_authentication": "Autenticação de dois fatores por TOTP", "Two-factor_authentication_disabled": "Autenticação de dois fatores desativada", @@ -4317,6 +4351,7 @@ "Unit_removed": "Unidade Removida", "Unknown_Import_State": "Estado de Importação Desconhecido", "Unlimited": "Ilimitado", + "Unmute": "Não silenciar", "Unmute_someone_in_room": "Permitir que alguém fale na sala", "Unmute_user": "Permitir que o usuário fale", "Unnamed": "Sem nome", @@ -4332,6 +4367,7 @@ "Unread_Rooms_Mode": "Agrupar Salas Não Lidas", "Unread_Tray_Icon_Alert": "Alerta do ícone da bandeja não lida", "Unstar_Message": "Remover Favorito", + "Unmute_microphone": "Tirar Microfone do Mudo", "Update": "Atualizar", "Update_EnableChecker": "Habilitar Verificador de Atualização", "Update_EnableChecker_Description": "Verifica automaticamente por novas atualizações / mensagens importantes do desenvolvedores do Rocket.Chat e receber notificações quando disponíveis. A notificação aparece uma vez por nova versão como um banner clicável e como uma mensagem do bot Rocket.Cat, ambos visíveis apenas para administradores.", @@ -4386,7 +4422,7 @@ "User_added": "Usuário adicionado", "User_added_by": "Usuário __user_added__ adicionado à conversa por __user_by__.", "User_added_successfully": "Usuário adicionado com sucesso", - "User_and_group_mentions_only": "O usuário e o grupo mencionam apenas", + "User_and_group_mentions_only": "Apenas menções a usuário e grupo", "User_cant_be_empty": "O usuário não pode estar vazio", "User_created_successfully!": "Usuário criado com sucesso!", "User_default": "Padrão do usuário", @@ -4421,7 +4457,7 @@ "User_left_team_male": "Saiu da equpe.", "User_logged_out": "Usuário não logado", "User_management": "Gerenciamento de usuários", - "User_mentions_only": "O usuário menciona apenas", + "User_mentions_only": "Apenas menções a usuário", "User_muted": "Usuário silenciado", "User_muted_by": "Usuário __user_muted__ silenciado por __user_by__.", "User_not_found": "Usuário não encontrado", @@ -4492,6 +4528,7 @@ "UTF8_User_Names_Validation_Description": "RegExp que será usado para validar nomes de usuários", "UTF8_Channel_Names_Validation": "Validação de Nomes de Canal UTF8", "UTF8_Channel_Names_Validation_Description": "RegExp que será usada para validar nomes de canais", + "Videocall_enabled": "Vídeoconferência habilitada", "Validate_email_address": "Validar endereço de e-mail", "Validation": "Validação", "Value_messages": "__value__ mensagens", @@ -4513,10 +4550,12 @@ "Video_Conference": "Vídeo Conferência", "Video_message": "Mensagem de vídeo", "Videocall_declined": "Chamada de vídeo negada.", - "Videocall_enabled": "Vídeoconferência habilitada", + "Video_and_Audio_Call": "Chamadas de Video e Áudio", "Videos": "Vídeos", "View_All": "Ver Todos Membros", "View_channels": "Ver Canais", + "view-omnichannel-contact-center": "Ver o Centro de Contatos do Omnichannel", + "view-omnichannel-contact-center_description": "Permissão para ver e interagir com o Centro de Contatos do Omnichannel", "View_Logs": "Ver Logs", "View_mode": "Modo de visualização", "View_original": "Visualizar original", @@ -4590,6 +4629,7 @@ "Visitor_message": "Mensagens de Visitantes", "Visitor_Name": "Nome do Visitante", "Visitor_Name_Placeholder": "Digite o nome do visitante ...", + "Visitor_does_not_exist": "Visitante não existe!", "Visitor_Navigation": "Navegação do Visitante", "Visitor_page_URL": "URL da página de visitante", "Visitor_time_on_site": "Tempo do visitante no site", @@ -4617,6 +4657,7 @@ "Webhook_Details": "Detalhes do WebHook", "Webhook_URL": "URL do webhook", "Webhooks": "Webhooks", + "WebRTC_Call": "Chamada WebRTC", "WebRTC_direct_audio_call_from_%s": "Chamada de áudio direta de %s", "WebRTC_direct_video_call_from_%s": "Videochamada direta de %s", "WebRTC_Enable_Channel": "Habilitar para Canais Públicos", @@ -4627,6 +4668,8 @@ "WebRTC_monitor_call_from_%s": "Monitore a chamada de %s", "WebRTC_Servers": "Servidores STUN/TURN", "WebRTC_Servers_Description": "Uma lista de servidores STUN e TURN separados por vírgula.
      Nome de usuário, senha e porta são permitidos no formato `username:password @stun:host:port` ou `username:password@turn:host:port`.", + "WebRTC_call_ended_message": " Chamada encerrada em __endTime__ - Duração __callDuration__", + "WebRTC_call_declined_message": " Chamada Recusada pelo Contato.", "Website": "Site", "Wednesday": "Quarta-feira", "Weekly_Active_Users": "Usuários Ativos Semanalmente", @@ -4717,4 +4760,4 @@ "Your_temporary_password_is_password": "Sua senha temporária é [password].", "Your_TOTP_has_been_reset": "Seu TOTP de dois fatores foi redefinido.", "Your_workspace_is_ready": "O seu espaço de trabalho está pronto para usar 🎉" -} +} \ No newline at end of file diff --git a/packages/rocketchat-i18n/i18n/pt.i18n.json b/packages/rocketchat-i18n/i18n/pt.i18n.json index f623c63a1b07..5961977f1d27 100644 --- a/packages/rocketchat-i18n/i18n/pt.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt.i18n.json @@ -692,6 +692,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Notificações sonoras contínuas, para nova sala de livechat", "Conversation": "Conversa", "Conversation_closed": "Chat encerrado: __comment__.", + "Conversation_finished": "Chat encerrado", "Conversation_finished_message": "Mensagem ao encerrar chat", "conversation_with_s": "a conversa com %s", "Conversations": "Conversas", @@ -1977,6 +1978,7 @@ "Max_length_is": "O comprimento máximo é %s", "Media": "Média", "Medium": "Médio", + "Members": "Membros", "Members_List": "Lista de Membros", "mention-all": "Mencionar todos", "mention-all_description": "Permissão para usar a menção @todos", @@ -2771,7 +2773,7 @@ "Sunday": "Domingo", "Support": "Apoio", "Survey": "Inquérito", - "Survey_instructions": "Classifique cada questão de acordo com a sua satisfação, 1 significa que você está completamente insatisfeito e 5 significa que você está completamente satisfeito.", + "Survey_instructions": "Classifique cada questão de acordo com a sua satisfação, 1 significa que está completamente insatisfeito e 5 significa que está completamente satisfeito.", "Symbols": "Símbolos", "Sync": "Sincronizar", "Sync / Import": "Sincronizar / Importar", @@ -3093,6 +3095,7 @@ "Users_added": "Os utilizadores foram adicionados", "Users_in_role": "Utilizadores na função", "UTF8_Names_Slugify": "Slugify Nomes UTF8 ", + "Videocall_enabled": "Vídeoconferência habilitada", "Validate_email_address": "Validar endereço de e-mail", "Verification": "Validação", "Verification_Description": "Pode usar os seguintes espaços reservados:
      • [Verification_Url] para o URL de verificação.
      • [name], [fname], [lname] para o nome completo, primeiro nome ou sobrenome do utilizador, respetivamente.
      • [email] para o email do usuário.
      • [Site_Name] e [Site_URL] para o Nome da aplicação e o URL, respetivamente.
      ", @@ -3109,7 +3112,6 @@ "Video_Conference": "Vídeo Conferência", "Video_message": "Mensagem de vídeo", "Videocall_declined": "Chamada de vídeo rejeitada.", - "Videocall_enabled": "Vídeoconferência habilitada", "View_All": "Ver todos os utilizadores", "View_Logs": "Ver Registo", "View_mode": "Modo de visualização", diff --git a/packages/rocketchat-i18n/i18n/ro.i18n.json b/packages/rocketchat-i18n/i18n/ro.i18n.json index 9c222c8979b8..b62744fc559f 100644 --- a/packages/rocketchat-i18n/i18n/ro.i18n.json +++ b/packages/rocketchat-i18n/i18n/ro.i18n.json @@ -555,6 +555,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Notificări de sunet continue pentru o cameră livechat nouă", "Conversation": "Conversaţie", "Conversation_closed": "Conversație închisă: __comment__.", + "Conversation_finished": "conversație terminat", "Conversation_finished_message": "Conversație Mesaj terminat", "conversation_with_s": "conversația cu %s", "Convert_Ascii_Emojis": "Conversie ASCII în Emoji", @@ -1685,6 +1686,7 @@ "Max_length_is": "Lungimea maximă este%s", "Media": "Mass-media", "Medium": "Mediu", + "Members": "Membri", "Members_List": "Lista de membri", "mention-all": "Menționați pe toate", "mention-all_description": "Permisiunea de a utiliza mențiunea @all", @@ -2300,6 +2302,7 @@ "Show_Setup_Wizard": "Afișați asistentul de configurare", "Show_the_keyboard_shortcut_list": "Afișați lista de comenzi rapide de la tastatură", "Showing_archived_results": "

      Se arată %s rezultate arhivate

      ", + "Showing_online_users": null, "Showing_results": "

      Se afișează %s rezultate

      ", "Sidebar": "Bara laterală", "Sidebar_list_mode": "Modul listei de canale din bara laterală", @@ -2678,6 +2681,7 @@ "Users_added": "Utilizatorii au fost adăugați", "Users_in_role": "Utilizatorii în rol", "UTF8_Names_Slugify": "UTF8 Names Slugify", + "Videocall_enabled": "Apel video este activat", "Validate_email_address": "Validați adresa de e-mail", "Verification": "Verificare", "Verification_Description": "Puteți utiliza următorii substituenți:
      • [Verification_Url] pentru adresa URL de verificare.
      • [name], [fname], [lname] pentru numele complet al utilizatorului, prenumele sau numele de familie.
      • [e-mail] pentru e-mailul utilizatorului.
      • [Site_Name] și [Site_URL] pentru numele aplicației și respectiv adresa URL.
      ", @@ -2692,7 +2696,6 @@ "Video_Conference": "Conferințe video", "Video_message": "Mesaj video", "Videocall_declined": "Videoclipul a fost refuzat.", - "Videocall_enabled": "Apel video este activat", "View_All": "Vezi toți", "View_Logs": "Vezi log-uri", "View_mode": "mod de vizualizare", @@ -2778,6 +2781,7 @@ "Yes_unarchive_it": "Da, dezarhivați-o!", "yesterday": "ieri", "You": "Tu", + "you_are_in_preview_mode_of": null, "You_are_logged_in_as": "Sunteți autentificat ca ", "You_are_not_authorized_to_view_this_page": "Nu sunteți autorizat pentru a vizualiza această pagină.", "You_can_change_a_different_avatar_too": "Puteți înlocui avatarul folosit pentru a posta din această integrare.", @@ -2813,4 +2817,4 @@ "Your_push_was_sent_to_s_devices": "Mesajul Push a fost trimis la %s dispozitive", "Your_server_link": "Linkul dvs. de server", "Your_workspace_is_ready": "Spațiul dvs. de lucru este gata de utilizare 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/ru.i18n.json b/packages/rocketchat-i18n/i18n/ru.i18n.json index dbe9514dcea6..e707a4230856 100644 --- a/packages/rocketchat-i18n/i18n/ru.i18n.json +++ b/packages/rocketchat-i18n/i18n/ru.i18n.json @@ -949,6 +949,7 @@ "Confirm_password": "Подтвердить пароль", "Confirmation": "Подтверждение", "Connect": "Подключение", + "Connected": "Подключено", "Connect_SSL_TLS": "Подключение с помощью SSL/TLS", "Connection_Closed": "Соединение закрыто", "Connection_Reset": "Сброс соединения", @@ -4385,6 +4386,7 @@ "UTF8_User_Names_Validation_Description": "Регулярное выражение для валидации имени пользователя", "UTF8_Channel_Names_Validation": "UTF8 валидация имени чата", "UTF8_Channel_Names_Validation_Description": "Регулярное выражение для валидации имени чата", + "Videocall_enabled": "Видеозвонок включен", "Validate_email_address": "Подтвердите адрес электронной почты", "Validation": "Валидация", "Value_messages": "__value__ сообщений", @@ -4406,7 +4408,6 @@ "Video_Conference": "Видеоконференция", "Video_message": "Видеосообщение", "Videocall_declined": "Видеозвонок отклонён.", - "Videocall_enabled": "Видеозвонок включен", "Videos": "Видеозаписи", "View_All": "Смотреть всех участников", "View_channels": "Просмотр каналов", @@ -4607,7 +4608,7 @@ "Your_push_was_sent_to_s_devices": "Оповещение было отправлено на %s устройств.", "Your_question": "Ваш вопрос", "Your_server_link": "Ссылка на ваш сервер", - "Your_temporary_password_is_password": "Ваш временный пароль [password].", + "Your_temporary_password_is_password": "Ваш временный пароль [password]", "Your_TOTP_has_been_reset": "Ваш двухфакторный TOTP был сброшен.", "Your_workspace_is_ready": "Ваше рабочее пространство готово к работе 🎉" } \ No newline at end of file diff --git a/packages/rocketchat-i18n/i18n/sk-SK.i18n.json b/packages/rocketchat-i18n/i18n/sk-SK.i18n.json index f44ce1eb3c01..4d8f4b11dbd6 100644 --- a/packages/rocketchat-i18n/i18n/sk-SK.i18n.json +++ b/packages/rocketchat-i18n/i18n/sk-SK.i18n.json @@ -561,6 +561,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Nepretržité zvukové upozornenia pre novú miestnosť livechat", "Conversation": "Konverzácia", "Conversation_closed": "Konverzácia bola uzavretá: __comment__.", + "Conversation_finished": "Konverzácia bola dokončená", "Conversation_finished_message": "Oznam o ukončení konverzácie", "conversation_with_s": "Konverzácia s %s", "Convert_Ascii_Emojis": "Previesť ASCII na Emoji", @@ -1284,7 +1285,7 @@ "hours": "hodiny", "Hours": "hodiny", "How_friendly_was_the_chat_agent": "Ako priateľský bol chatový agent?", - "How_knowledgeable_was_the_chat_agent": "Ako bol znalý chatový agent?", + "How_knowledgeable_was_the_chat_agent": "Ako informovaný bol diskusný agent?", "How_long_to_wait_after_agent_goes_offline": "Ako dlho čakať po tom, ako sa agent stane offline", "How_responsive_was_the_chat_agent": "Ako reagoval chatový agent?", "How_satisfied_were_you_with_this_chat": "Ako ste boli spokojní s týmto rozhovorom?", @@ -1975,7 +1976,7 @@ "Placeholder_for_password_login_field": "Zástupca pre pole Prihlasovacie heslo", "Please_add_a_comment": "Pridajte komentár", "Please_add_a_comment_to_close_the_room": "Pridajte komentár na zatvorenie miestnosti", - "Please_answer_survey": "Venujte chvíľu odpovede na rýchly prieskum o tomto rozhovore", + "Please_answer_survey": "Venujte prosím chvíľu odpovediam v rýchlom prieskume o tejto diskusii", "Please_enter_usernames": "Zadajte používateľské mená ...", "please_enter_valid_domain": "Zadajte platnú doménu", "Please_enter_value_for_url": "Zadajte hodnotu pre adresu URL vášho avatara.", @@ -2249,7 +2250,7 @@ "Select_user": "Vyberte používateľa", "Select_users": "Vyberte používateľov", "Selected_agents": "Vybraní zástupcovia", - "Send": "odoslať", + "Send": "Odoslať", "Send_a_message": "Poslať správu", "Send_a_test_mail_to_my_user": "Pošlite skúšobnú poštu môjmu používateľovi", "Send_a_test_push_to_my_user": "Pošlite skúšobný krok môjmu používateľovi", @@ -2322,7 +2323,7 @@ "Site_Url": "Adresa URL lokality", "Site_Url_Description": "Príklad: https://chat.domena.com/", "Size": "veľkosť", - "Skip": "preskočiť", + "Skip": "Preskočiť", "Slack_Users": "Slack používatelia CSV", "SlackBridge_error": "SlackBridge dostal chybu pri importovaní vašich správ na%s:%s", "SlackBridge_finish": "Služba SlackBridge dokončila importovanie správ na%s. Znova načítajte všetky správy.", @@ -2418,8 +2419,8 @@ "Success_message": "Správa o úspechu", "Sunday": "nedeľa", "Support": "podpora", - "Survey": "prehľad", - "Survey_instructions": "Hodnoť každú otázku podľa vašej spokojnosti, 1 znamená, že ste úplne nespokojní a 5 znamená, že ste úplne spokojní.", + "Survey": "Prieskum", + "Survey_instructions": "Ohodnoťte každú otázku na základe vašej spokojnosti, 1 znamená úplnú nespokojnosť, 5 znamená úplnú spokojnosť.", "Symbols": "symboly", "Sync_in_progress": "Prebieha synchronizácia", "Sync_success": "Synchronizácia úspechu", @@ -2440,7 +2441,7 @@ "Test_Connection": "Test pripojenia", "Test_Desktop_Notifications": "Testovanie upozornení na pracovnej ploche", "Thank_you_exclamation_mark": "Ďakujem!", - "Thank_you_for_your_feedback": "Ďakujeme vám za vašu reakciu", + "Thank_you_for_your_feedback": "Ďakujeme vám za vašu spätnú väzbu", "The_application_name_is_required": "Názov aplikácie je povinný", "The_channel_name_is_required": "Názov kanála je povinný", "The_emails_are_being_sent": "Posielajú sa e-maily.", @@ -2692,6 +2693,7 @@ "Users_added": "Používatelia boli pridaní", "Users_in_role": "Používatelia v úlohe", "UTF8_Names_Slugify": "Mená UTF8", + "Videocall_enabled": "Videohovor je zapnutý", "Validate_email_address": "Overiť e-mailovú adresu", "Verification": "overenie", "Verification_Description": "Môžete použiť nasledujúce zástupné symboly:
      • [Verification_Url] pre verifikačnú adresu URL.
      • [meno], [fname], [lname] pre celé meno používateľa, krstné meno alebo priezvisko.
      • [email] pre e-mail používateľa.
      • [Site_Name] a [Site_URL] pre názov aplikácie a adresu URL.
      ", @@ -2706,7 +2708,6 @@ "Video_Conference": "Video konferencia", "Video_message": "Video správy", "Videocall_declined": "Zamietnutý videohovor.", - "Videocall_enabled": "Videohovor je zapnutý", "View_All": "Zobraziť všetkých členov", "View_Logs": "Zobraziť denníky", "View_mode": "Režim zobrazenia", @@ -2780,7 +2781,7 @@ "will_be_able_to": "budú môcť", "Worldwide": "celosvetovo", "Would_you_like_to_return_the_inquiry": "Chcete vrátiť dotaz?", - "Yes": "Áno,", + "Yes": "Áno", "Yes_archive_it": "Áno, archivujte to!", "Yes_clear_all": "Áno, jasné všetko!", "Yes_delete_it": "Áno, odstráňte ju!", @@ -2791,7 +2792,7 @@ "Yes_remove_user": "Áno, odstráňte používateľa!", "Yes_unarchive_it": "Áno, dearchívujte to!", "yesterday": "včera", - "You": "vy", + "You": "Vy", "you_are_in_preview_mode_of": "Nachádzate sa v režime náhľadu kanálu # __room_name__", "You_are_logged_in_as": "Ste prihlásení ako", "You_are_not_authorized_to_view_this_page": "Nemáte oprávnenie na zobrazenie tejto stránky.", diff --git a/packages/rocketchat-i18n/i18n/sl-SI.i18n.json b/packages/rocketchat-i18n/i18n/sl-SI.i18n.json index 9732fa2b5591..f5a0f7306a32 100644 --- a/packages/rocketchat-i18n/i18n/sl-SI.i18n.json +++ b/packages/rocketchat-i18n/i18n/sl-SI.i18n.json @@ -552,6 +552,7 @@ "Continue": "Nadaljuj", "Continuous_sound_notifications_for_new_livechat_room": "Neprekinjena zvočna obvestila za novo sobo za življenje", "Conversation": "Pogovor", + "Conversation_finished": "Pogovor končan", "Conversation_finished_message": "Konverzirano sporočilo", "conversation_with_s": "pogovor z %s", "Convert_Ascii_Emojis": "Pretvori ASCII v čustveni simbol", @@ -2672,6 +2673,7 @@ "Users_added": "Uporabniki so bili dodani", "Users_in_role": "Uporabniki v vlogi", "UTF8_Names_Slugify": "Uporabi UTF8 Slugify za imena", + "Videocall_enabled": "Video klic je omogočen", "Validate_email_address": "Potrdite e-poštni naslov", "Verification": "Preverjanje", "Verification_Description": "Za URL za preverjanje lahko uporabite naslednje označbe:
      • [Verification_Url].
      • [name], [fname], [lname] za polno ime, ime ali priimek uporabnika.
      • [email] za uporabnikov e-poštni naslov.
      • [Site_Name] in [Site_URL] za ime aplikacije in URL.
      ", @@ -2686,7 +2688,6 @@ "Video_Conference": "Video konferenca", "Video_message": "Video sporočilo", "Videocall_declined": "Video klic zavrnjen.", - "Videocall_enabled": "Video klic je omogočen", "View_All": "Ogled vseh članov", "View_Logs": "Ogled dnevnikov", "View_mode": "Način pogleda", diff --git a/packages/rocketchat-i18n/i18n/sq.i18n.json b/packages/rocketchat-i18n/i18n/sq.i18n.json index edd5658a2ed3..1e5706a37ab5 100644 --- a/packages/rocketchat-i18n/i18n/sq.i18n.json +++ b/packages/rocketchat-i18n/i18n/sq.i18n.json @@ -555,6 +555,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Njoftime të vazhdueshme të zërit për dhomën e re të jetesës", "Conversation": "Biseda", "Conversation_closed": "Biseda mbyllur: __comment__.", + "Conversation_finished": "biseda përfunduar", "Conversation_finished_message": "Biseda Përfundoi Mesazhi", "conversation_with_s": "biseda me %s", "Convert_Ascii_Emojis": "Convert ASCII të Emoji", @@ -1684,6 +1685,7 @@ "Max_length_is": "Gjatësia maksimale është %s", "Media": "Media", "Medium": "medium", + "Members": "Anëtarët", "Members_List": "Lista e Anëtarëve", "mention-all": "Përmend të gjitha", "mention-all_description": "Leja për të përdorur të gjitha përmendur", @@ -2679,6 +2681,7 @@ "Users_added": "Përdoruesit janë shtuar", "Users_in_role": "Përdoruesit në rolin", "UTF8_Names_Slugify": "UTF8 Emrat Slugify", + "Videocall_enabled": "Thirrje video të aktivizuara", "Validate_email_address": "Validoni adresën e emailit", "Verification": "verifikim", "Verification_Description": "Mund të përdorësh vendin e mëposhtëm:
      • [Verification_Url] për URL-në e verifikimit.
      • [emër], [fname], [lname] për emrin e plotë, emrin ose mbiemrin e përdoruesit.
      • [email] për emailin e përdoruesit.
      • [Site_Name] dhe [Site_URL] për emrin e aplikacionit dhe URL përkatësisht.
      ", @@ -2693,7 +2696,6 @@ "Video_Conference": "Konferenca Video", "Video_message": "Mesazh video", "Videocall_declined": "Thirrje video nuk pranohet.", - "Videocall_enabled": "Thirrje video të aktivizuara", "View_All": "Shiko të gjitha", "View_Logs": "Shiko Shkrime", "View_mode": "mënyra e shfaqjes", diff --git a/packages/rocketchat-i18n/i18n/sr.i18n.json b/packages/rocketchat-i18n/i18n/sr.i18n.json index b384b8e64926..391f91bc4ffe 100644 --- a/packages/rocketchat-i18n/i18n/sr.i18n.json +++ b/packages/rocketchat-i18n/i18n/sr.i18n.json @@ -1,6 +1,7 @@ { "403": "Забрањен", "500": "Интерна грешка сервера", + "__username__was_set__role__by__user_by_": null, "@username": "@корисничко име", "@username_message": "@ корисничко име ", "#channel": "#канал", @@ -48,6 +49,7 @@ "Accounts_EmailVerification": "Дозволи пријаву само потврђеним корисницима", "Accounts_EmailVerification_Description": "Потврдите да имате исправне SMTP параметре да би сте користили ову могућност", "Accounts_Enrollment_Email_Default": "

      Добродошли на[Site_Name]

      Иди на [Site_URL] и испробај најбоље решење за ћаскање отвореног кода које је тренутно доступно!

      ", + "Accounts_Enrollment_Email_Description": "Можете користити [name], [fname], [lname] за пуно име корисника, имену или презимену, респективно.
      Можете користити [email] е-поште корисника.", "Accounts_Enrollment_Email_Subject_Default": "Добродошли на [Site_Name]", "Accounts_ForgetUserSessionOnWindowClose": "Заборави корисничку сесију по затварању прозора", "Accounts_Iframe_api_method": "Api метода", @@ -71,13 +73,25 @@ "Accounts_OAuth_Custom_Token_Path": "Путања токена", "Accounts_OAuth_Custom_Token_Sent_Via": "Токен послат путем", "Accounts_OAuth_Custom_Username_Field": "Поље корисничког имена", + "Accounts_OAuth_Facebook_callback_url": "Фацебоок УРЛ за повратни позив", + "Accounts_OAuth_Facebook_id": "ИД фацебоок апликација", + "Accounts_OAuth_Github_callback_url": "Гитхуб УРЛ за повратни позив", + "Accounts_OAuth_GitHub_Enterprise": "ОАутх Омогућено", "Accounts_OAuth_GitHub_Enterprise_id": "Ид клијента", "Accounts_OAuth_GitHub_Enterprise_secret": "Тајна клијента", "Accounts_OAuth_Github_id": "Ид клијента", "Accounts_OAuth_Github_secret": "Тајна клијента", + "Accounts_OAuth_Gitlab": "ОАутх Омогућено", + "Accounts_OAuth_Gitlab_callback_url": "ГитЛаб УРЛ за повратни позив", + "Accounts_OAuth_Gitlab_id": "ГитЛаб ИД", "Accounts_OAuth_Gitlab_identity_path": "Путања до идентитета", "Accounts_OAuth_Gitlab_secret": "Тајна клијента", + "Accounts_OAuth_Google": "гоогле Пријава", + "Accounts_OAuth_Google_id": "гоогле ИД", + "Accounts_OAuth_Meteor": "метеор Пријава", + "Accounts_OAuth_Meteor_callback_url": "Метеор УРЛ за повратни позив", "Accounts_OAuth_Wordpress_authorize_path": "Путања до ауторизације", + "Accounts_OAuth_Wordpress_id": "ВордПресс ИД", "Accounts_OAuth_Wordpress_identity_path": "Путања до идентитета", "Accounts_OAuth_Wordpress_scope": "Опсег", "Accounts_OAuth_Wordpress_server_type_custom": "Прилагођено", @@ -110,6 +124,7 @@ "Accounts_RegistrationForm_Public": "Јавни", "Accounts_RegistrationForm_Secret_URL": "Тајна УРЛ адреса", "Accounts_RegistrationForm_SecretURL": "Тајна УРЛ адреса обрасца за регистрацију", + "Accounts_RegistrationForm_SecretURL_Description": "Морате обезбедити случајни низ који ће бити додат на ваш регистрације УРЛ. Пример: хттпс://демо.роцкет.цхат/регистер/[secret_hash]", "Accounts_RequireNameForSignUp": "Захтева име за регистрацију", "Accounts_RequirePasswordConfirmation": "Захтевај потврду лозинке", "Accounts_SearchFields": "Поља за размишљање у потрази", @@ -128,6 +143,7 @@ "Activity": "Активност", "Add": "Додај", "Add_agent": "Додај агента", + "Add_custom_oauth": "Додај прилагођени ОАутх", "Add_Domain": "Додај домен", "Add_files_from": "Додајте датотеке из", "Add_manager": "Додај менаџера", @@ -148,6 +164,7 @@ "Additional_emails": "Додатне Е-поште", "Administration": "Администрација", "Adult_images_are_not_allowed": "Слике за одрасле нису дозвољене", + "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "Након ОАутх2 аутентификације, корисници ће бити преусмерени на овај УРЛ", "Agent": "Агент", "Agent_added": "Агент додат", "Agent_removed": "Агент уклоњен", @@ -165,6 +182,7 @@ "Always_open_in_new_window": "Увек отвори у новом прозору", "Analytics": "Аналитика", "Analytics_features_enabled": "Активиране могућности", + "Analytics_features_users_Description": "Треки прилагођене догађаје који се односе на акције које се односе на кориснике (пассворд ресет пута, профил Промена слике, итд).", "Analytics_Google": "Гугл аналитика", "Analytics_Google_id": "ИД праћења", "and": "и", @@ -173,10 +191,13 @@ "Announcement": "Најава", "API": "АПИ", "API_Analytics": "Аналитика", + "API_CORS_Origin": "ЦОРС Оригин", "API_Drupal_URL_Description": "Пример: https://domain.com (искључујући крајњу косу црту)", "API_Embed": "Убаци преглед линкова", "API_Embed_Description": "Да ли су омогућени прегледи уграђене везе када корисник поставља линк на веб локацију.", + "API_EmbedDisabledFor": "Онемогући Додајте за кориснике", "API_EmbedDisabledFor_Description": "Зарезом одвојена листа корисничких имена којима је онемогућен преглед убачених линкова.", + "API_EmbedIgnoredHosts_Description": "Зарезом одвојена листа хостова или цидр адресе, нпр. лоцалхост 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16", "API_EmbedSafePorts": "Безбедни портови", "API_EmbedSafePorts_Description": "Зарезом одвојена листа портова дозвољених за преглед.", "API_Enable_Direct_Message_History_EndPoint": "Омогући крајњу тачку историје директних порука", @@ -188,6 +209,9 @@ "API_Token": "АПИ токен", "API_Tokenpass_URL_Description": "Пример: https://domain.com (искључујући крајњу косу црту)", "API_Upper_Count_Limit": "Максимално записа", + "API_User_Limit": "Корисник лимит за додавање Сви корисници на канал", + "API_Wordpress_URL": "УордПресс УРЛ адреса", + "Apiai_Language": "Апи.аи Језик", "App_author_homepage": "веб страница аутора", "App_Information": "Информације о апликацији", "App_Installation": "Инсталација апликације", @@ -229,6 +253,7 @@ "Audio_Notification_Value_Description": "Може бити сваки прилагођени звук или подразумевани: звучни сигнал, звук, динг, капљица, високи звук, годишња доба", "Audio_Notifications_Default_Alert": "Подразумевана обавештења о аудио обавештењима", "Audio_Notifications_Value": "Подразумевано обавештење о поруци за звук", + "Auth_Token": "аутх токен", "Author": "Аутор", "Author_Information": "Информације о аутору", "Authorization_URL": "УРЛ адреса за ауторизацију", @@ -237,7 +262,11 @@ "Auto_Translate": "Аутоматски преведи", "auto-translate": "Аутоматско превођење", "auto-translate_description": "Дозвола за коришћење алата за аутоматско превођење", + "AutoLinker_Phone": "АутоЛинкер телефона", + "AutoLinker_StripPrefix": "АутоЛинкер Стрип Префикс", "AutoLinker_StripPrefix_Description": "Кратак приказ. нпр https://rocket.chat => rocket.chat", + "AutoLinker_Urls_Scheme": "АутоЛинкер Шема: // адресе", + "AutoLinker_UrlsRegExp": "АутоЛинкер УРЛ-Регулар Екпрессион", "Automatic_Translation": "Аутоматско превођење", "AutoTranslate": "Аутоматски преведи", "AutoTranslate_APIKey": "АПИ кључ", @@ -249,6 +278,7 @@ "Avatar": "Аватар", "Avatar_changed_successfully": "Аватар успешно промењен", "Avatar_URL": "УРЛ адреса аватара", + "Avatar_url_invalid_or_error": "УРЛ адреса ако је неважећи или није доступна. Покушајте поново, али са различитим УРЛ.", "away": "одсутан(на)", "Away": "Одсутан(на)", "away_female": "недоступан", @@ -322,6 +352,8 @@ "Channels_are_where_your_team_communicate": "Канали су тамо где ваш тим комуницира", "Channels_list": "Листа јавних канала", "Chat_Now": "Ћаскај сада", + "Chatops_Enabled": "Омогући Цхатопс", + "Chatops_Title": "Цхатопс панел", "Chatpal_All_Results": "Све", "Chatpal_API_Key": "АПИ кључ", "Chatpal_Base_URL": "Основни УРЛ", @@ -402,6 +434,7 @@ "Continue": "наставити", "Continuous_sound_notifications_for_new_livechat_room": "Непрекидна звучна обавештења за нову собу за живот", "Conversation": "Разговор", + "Conversation_finished": "разговор завршио", "Conversation_finished_message": "Завршена порука конверзације", "conversation_with_s": "разговор са %s", "Convert_Ascii_Emojis": "Претворити ASCII у емотиконе", @@ -685,6 +718,7 @@ "Custom_Emoji_Info": "Информације о прилагођеном емотикону", "Custom_Emoji_Updated_Successfully": "Прилагођени емотикон је успешно ажуриран", "Custom_Fields": "Произвољна поља", + "Custom_oauth_helper": "Када подешавате ОАутх провајдера, мораћете да обавести УРЛ за повратни позив. употреба
       % с 
      .", "Custom_Script_Logged_In": "Произвољне скрипте за пријављене кориснике", "Custom_Script_Logged_Out": "Произвољне скрипте за одјављене кориснике", "Custom_Scripts": "Прилагођене скрипте", @@ -738,6 +772,7 @@ "Desktop_Notifications_Default_Alert": "Подразумевана обавештења на радној површини", "Desktop_Notifications_Disabled": "Обавештења на радној површини су искључена. Промените подешавања вашег прегледача ако желите да укључите обавештења.", "Desktop_Notifications_Duration": "Трајање обавештења на радној површини", + "Desktop_Notifications_Duration_Description": "Секунде да бисте приказали обавештење десктоп. Ово може утицати на ОС Кс Нотифицатион Центер. Унесите 0 за коришћење подразумевана подешавања претраживача и не утиче на ОС Кс Нотифицатион Центер.", "Desktop_Notifications_Enabled": "Обавештења на радној површини су омогућена", "Different_Style_For_User_Mentions": "Различит стил за помињање корисника", "Direct_message_someone": "Пошаљи директну поруку некоме", @@ -836,6 +871,7 @@ "Enable_two-factor_authentication": "Омогућите двоструку аутентификацију", "Enabled": "Оmogućeno", "Encrypted_message": "Шифрована порука", + "End_OTR": "Крај ОТР", "Enter_a_room_name": "Унесите име собе", "Enter_a_username": "Унесите корисничко име", "Enter_Alternative": "Алтернативни режим (послати с Ентер + Цтрл / Алт / Схифт / ЦМД)", @@ -854,6 +890,8 @@ "Error_RocketChat_requires_oplog_tailing_when_running_in_multiple_instances": "Грешка: Роцкет.Цхат захтева оплог таилинг када ради у више инстанци", "Error_RocketChat_requires_oplog_tailing_when_running_in_multiple_instances_details": "Проверите да ли је ваш МонгоДБ на режиму РеплицаСет, а варијабла окружења МОНГО_ОПЛОГ_УРЛ је исправно дефинирана на серверу апликација", "error-application-not-found": "Апликација није пронађена", + "error-avatar-invalid-url": "Инвалид Аватар УРЛ: __url__", + "error-avatar-url-handling": "Грешка при руковању подешавање аватар из УРЛ (__url__) за __username__", "error-cant-invite-for-direct-room": "Није могуће позивати кориснике у директне собе", "error-channels-setdefault-is-same": "Подразумевана поставка канала је иста као и оно на шта ће се променити.", "error-channels-setdefault-missing-default-param": "Захтева се бодиПарам 'дефаулт'", @@ -869,6 +907,8 @@ "error-field-unavailable": "__field__ је већ у употреби :(", "error-file-too-large": "Датотека је превелика", "error-importer-not-defined": "Увозник није правилно дефинисан, недостаје му класа за увоз.", + "error-input-is-not-a-valid-field": "__input__ није валидан __field__", + "error-invalid-actionlink": "Неважећи радња Линк", "error-invalid-arguments": "Неисправни аргументи", "error-invalid-channel": "Неисправан канал.", "error-invalid-channel-start-with-chars": "Неисправан канал. Почните са @ или #", @@ -888,6 +928,7 @@ "error-invalid-name": "Неисправан назив", "error-invalid-password": "Неисправна лозинка", "error-invalid-permission": "Неважећа дозвола", + "error-invalid-redirectUri": "неважећи редирецтУри", "error-invalid-role": "Неисправна улога", "error-invalid-room": "Неисправна соба", "error-invalid-room-name": "__room_name__ није исправан назив соба", @@ -895,12 +936,14 @@ "error-invalid-settings": "Унешена су неисправна подешавања", "error-invalid-subscription": "Неисправна претплата", "error-invalid-token": "Неисправан токен", + "error-invalid-triggerWords": "Неважећи триггерВордс", "error-invalid-urls": "Погрешна УРЛ адреса", "error-invalid-user": "Погрешан корисник", "error-invalid-username": "Погрешно корисничко име", "error-logged-user-not-in-room": "Нисте у соби `%s`", "error-message-deleting-blocked": "Брисање порука је блокирано", "error-message-editing-blocked": "Уређивање порука је блокирано", + "error-message-size-exceeded": "величина поруке већа од Мессаге_МакАлловедСизе", "error-missing-unsubscribe-link": "Морате навести [unsubscribe] линк.", "error-no-tokens-for-this-user": "Нема токена за овог корисника", "error-not-allowed": "Није дозвољено", @@ -913,10 +956,13 @@ "error-password-policy-not-met-oneSpecial": "Лозинка не испуњава политику сервера од најмање једног специјалног карактера", "error-password-policy-not-met-oneUppercase": "Лозинка не испуњава политику сервера од најмање једног великог слова", "error-password-policy-not-met-repeatingCharacters": "Лозинка не испуњава политику сервера забрањених понављања знакова (имате превише истих знакова поред једне друге)", + "error-push-disabled": "Пусх је онемогућен", "error-remove-last-owner": "Ово је последњи власник. Подесите новог власника пре него што овог уклоните.", "error-role-in-use": "Не можете обрисати улогу јер је у употреби", "error-role-name-required": "Име улоге је неопходно", "error-room-is-not-closed": "Соба није затворена", + "error-the-field-is-required": "је обавезно поље __field__.", + "error-too-many-requests": "Грешка, превише захтева. Молим те успори. Морате да сачекате __seconds__ секунди пре него што поново покушава.", "error-user-has-no-roles": "Корисник нема улоге", "error-user-is-not-activated": "Корисник није активиран", "error-user-limit-exceeded": "Број корисника које покушавате позвати у #цханнел_наме превазилази лимит који је поставио администратор", @@ -947,6 +993,7 @@ "Favorite": "Омиљено", "Favorite_Rooms": "Омогући омиљене собе", "Favorites": "Омиљене", + "Features_Enabled": "karakteristike Омогућено", "Federation_Domain": "Домен", "Federation_Discovery_method": "Начин откривања", "FEDERATION_Discovery_Method": "Начин откривања", @@ -972,6 +1019,7 @@ "FileUpload_Error": "Грешка при постављању датотеке", "FileUpload_File_Empty": "Датотека је празна", "FileUpload_FileSystemPath": "Системска путања", + "FileUpload_GoogleStorage_Bucket": "Гоогле Стораге Буцкет Наме", "FileUpload_GoogleStorage_Secret_Description": "Пратите ова упутства и залепите резултат овде.", "FileUpload_MaxFileSize": "Максимална величина уплоад сизе (ин битес)", "FileUpload_MaxFileSizeDescription": "Подесите на -1 да уклоните ограничење величине датотеке.", @@ -1010,6 +1058,7 @@ "Financial_Services": "Финансијске услуге", "First_Channel_After_Login": "Први канал након пријаве", "Flags": "Заставе", + "Follow_social_profiles": "Пратите наше друштвене профиле, форкујте нас на Гитхабу и поделите ваша мишљења о нашој rocket.chat апликацији на нашој Трело табли.", "Fonts": "Фонтови", "Food_and_Drink": "Храна и пиће", "Footer": "Подножје", @@ -1038,6 +1087,8 @@ "From_email_warning": "Упозорење: Поље Од подлеже поставкама твог севера е-поште.", "Gaming": "Гаминг", "General": "Општи", + "Give_a_unique_name_for_the_custom_oauth": "Дати јединствено име за прилагођени ОАутх", + "Give_the_application_a_name_This_will_be_seen_by_your_users": "Дајте Апликација име. Ово ће бити виђена од својих корисника.", "Global": "Глобално", "Global_purge_override_warning": "Успостављена је глобална политика задржавања. Ако оставите \"Оверриде глобал полици ретентион\" искључену, можете примијенити само политику која је строжија од глобалне политике.", "Global_Search": "Глобално претраживање", @@ -1069,6 +1120,7 @@ "Host": "Домаћин", "hours": "сати", "Hours": "Сати", + "How_knowledgeable_was_the_chat_agent": "Како знања је агент ћаскање?", "How_long_to_wait_after_agent_goes_offline": "Како дуго чекати након што агент престане да ради", "How_to_handle_open_sessions_when_agent_goes_offline": "Како поступати са отвореним сесијама када агент отпутује ван мреже", "Idle_Time_Limit": "Лимит Идле Тиме", @@ -1099,6 +1151,7 @@ "Importer_CSV_Information": "ЦСВ увознику је потребан одређени формат, молимо прочитајте документацију како структурирати своју зип датотеку:", "Importer_done": "Увоз завршен!", "Importer_finishing": "Завршавање увоза.", + "Importer_From_Description": "Увоз __from__ 'с подацима у Роцкет.Цхат.", "Importer_HipChatEnterprise_BetaWarning": "Имајте на уму да је овај увоз и даље посао у току, пријавите грешке које се јављају у ГитХуб-у:", "Importer_HipChatEnterprise_Information": "Датотека која је отпремљена мора бити дешифрована тар.гз, прочитајте документацију за додатне информације:", "Importer_import_cancelled": "Увоз отказан.", @@ -1111,6 +1164,8 @@ "Importer_not_setup": "Увозник није подешен правилно, пошто није вратио никакве податке.", "Importer_Prepare_Restart_Import": "Почни увоз поново", "Importer_Prepare_Start_Import": "Почни увожење", + "Importer_Prepare_Uncheck_Archived_Channels": "Поништите потврду Архивирани Канали", + "Importer_progress_error": "Није добио напредак за увоз.", "Importer_setup_error": "Дошло је до грешке приликом подешавања увозника.", "Importer_Slack_Users_CSV_Information": "Датотека која је отпремљена мора да буде датотека извоза корисника Слацк-а, која је ЦСВ датотека. Погледајте овде за више информација:", "Importer_Source_File": "Избор извора датотека", @@ -1120,6 +1175,7 @@ "Incoming_WebHook": "Долазни ВебХоок", "Industry": "Индустрија", "initials_avatar": "Инитиалс Аватар", + "Install_Extension": "Инсталл Ектенсион", "Install_package": "Инсталирајте пакет", "Installation": "Инсталација", "Installed_at": "инсталиран у", @@ -1169,6 +1225,7 @@ "InternalHubot_Username_Description": "Ово мора бити валидно корисничко име бота регистрованог на твом серверу.", "Invalid_confirm_pass": "Потврдна лозинка се не поклапа са лозинком", "Invalid_email": "Унета је неисправна адреса е-поште", + "Invalid_Import_File_Type": "Погрешна врста Увоз датотеке.", "Invalid_name": "Име не сме бити празно", "Invalid_notification_setting_s": "Неисправна поставка обавештења: %s", "Invalid_pass": "Лозинка не сме бити празна", @@ -1244,6 +1301,7 @@ "Last_seen": "Последњи пут виђен", "Launched_successfully": "Покренут успешно", "Layout": "Распоред", + "Layout_Home_Title": "хоме Наслов", "Layout_Login_Terms": "Услови пријаве", "Layout_Privacy_Policy": "Правила о приватности", "Layout_Sidenav_Footer_description": "Фоотер је величине 260 х 70 пиксела", @@ -1291,6 +1349,8 @@ "LDAP_Login_Fallback_Description": "Ако пријава на ЛДАП-у није успешна, покушајте да се пријавите у систему подразумеваног / локалног налога. Помаже када је ЛДАП из неког разлога пао.", "LDAP_Merge_Existing_Users": "Споји постојеће кориснике", "LDAP_Merge_Existing_Users_Description": "* Опрез! * Када увозите корисника из ЛДАП-а и корисник са истим корисничким именом већ постоји, ЛДАП инфо и лозинка ће бити постављени у постојећи корисник.", + "LDAP_Port": "лука", + "LDAP_Port_Description": "Порт за приступ ЛДАП. нпр: `389` или` 636` за ЛДАПС", "LDAP_Reconnect": "Поново повежите", "LDAP_Reconnect_Description": "Покушајте да се аутоматски повежете када је веза прекинута из неког разлога током извршавања операција", "LDAP_Reject_Unauthorized": "Одбиј неовлашћено", @@ -1339,11 +1399,16 @@ "Livechat_guest_count": "Гост Цоунтер-", "Livechat_Inquiry_Already_Taken": "Ливецхат упит већ преузет", "Livechat_managers": "ЛивеЦхат менаџери", + "Livechat_offline": "ЛивеЦхат онлине", "Livechat_online": "ЛивеЦхат на мрежи", "Livechat_Queue": "Ливецхат Куеуе", "Livechat_registration_form": "Образац за регистрацију", + "Livechat_room_count": "ЛивеЦхат соба датотека", "Livechat_Routing_Method": "Ливецхат метод рутирања", "Livechat_Take_Confirm": "Хоћеш ли узети овог клијента?", + "Livechat_title": "ЛивеЦхат Наслов", + "Livechat_title_color": "ЛивеЦхат Наслов Боја позадине", + "Livechat_Users": "ЛивеЦхат корисника", "Livestream_close": "Затвори Ливестреам", "Livestream_enable_audio_only": "Омогућите само аудио режим", "Livestream_not_found": "Ливестреам није доступан", @@ -1374,11 +1439,15 @@ "Logistics": "Логистика", "Logout": "Одјави се", "Logout_Others": "Одјави ме из других пријављених локација", + "Mail_Message_Invalid_emails": "Које сте дали један или више неважећих е-поште:% с", "Mail_Message_No_messages_selected_select_all": "Нисте изабрали ниједну поруку.", "Mail_Messages": "Поруке е-поште", "Mail_Messages_Instructions": "Изабрати које поруке желите да пошаљете путем е-маила тако што ћете кликнути поруке", + "Mail_Messages_Subject": "Овде је изабран део% с порука", "mail-messages": "Поруке на е-пошту", "mail-messages_description": "Дозвола за кориштење опције поштанских порука", + "Mailer_body_tags": "Морате користити [unsubscribe] за Унсубсцриптион линк.
      Можете користити [name], [fname], [lname] за пуно име корисника, имену или презимену, респективно.
      Можете користити [email] е-поште корисника.", + "Mailing": "маилинг", "Make_sure_you_have_a_copy_of_your_codes_1": "Проверите да ли имате копију својих кодова:", "Make_sure_you_have_a_copy_of_your_codes_2": "Ако изгубите приступ вашој апликацији за аутентификацију, можете да користите један од ових шифара за пријављивање.", "manage-apps": "Управљај апликацијама", @@ -1419,6 +1488,7 @@ "Max_length_is": "Максимална дужина је %s", "Media": "Медији", "Medium": "Средње", + "Members": "Чланови", "Members_List": "Списак чланова", "mention-all": "Ментион Алл", "mention-all_description": "Дозволите да користите @алл напомену", @@ -1437,7 +1507,9 @@ "Message_AllowEditing": "Дозволи уређивање порука", "Message_AllowEditing_BlockEditInMinutes": "Блокирај уређивање порука након (х) минута", "Message_AllowEditing_BlockEditInMinutesDescription": "Унесите 0 да искључите блокирање.", + "Message_AllowPinning": "Дозволи порука пиннинг", "Message_AllowSnippeting": "Дозволи Сниппинг порука", + "Message_AllowStarring": "Дозволи Порука Улоге", "Message_AllowUnrecognizedSlashCommand": "Дозволи непознате команде за сласх", "Message_Attachments": "Додаци за поруке", "Message_Attachments_GroupAttach": "Дугмад за додавање групе", @@ -1564,6 +1636,7 @@ "No_group_with_name_%s_was_found": "Приватна група са именом \"%s\" није пронађена!", "No_groups_yet": "Још увек немате приватне групе.", "No_integration_found": "Интеграција није пронађена од стране обезбеђеног ИД-а.", + "No_livechats": "Немате ливецхатс.", "No_mentions_found": "Помињања нису пронађена", "No_messages_yet": "Још нема порука", "No_pages_yet_Try_hitting_Reload_Pages_button": "Још нема страница. Покушајте да притиснете дугме \"Релоад Пагес\".", @@ -1571,6 +1644,7 @@ "No_results_found": "Нема резултата", "No_results_found_for": "Није пронађен ниједан резултат за:", "No_snippet_messages": "Нема сниппет", + "No_starred_messages": "Но звездицом порука", "No_such_command": "Нема такве команде: `/__command__`", "No_user_with_username_%s_was_found": "Корисник са корисничким именом \"%s\" није пронађен!", "Nobody_available": "Нико није доступан", @@ -1643,8 +1717,14 @@ "Organization_Name": "Назив организације", "Organization_Type": "Тип организације", "Original": "Оригинал", + "OS_Cpus": "ОС Процесор Точка", + "OS_Freemem": "ОС Слободна меморија", + "OS_Release": "ОС Издање", + "OS_Totalmem": "ОС Укупно меморије", + "OS_Type": "тури", "Other": "Друго", "others": "други", + "OTR_is_only_available_when_both_users_are_online": "ОТР је доступна само ако оба корисника су онлине", "Outgoing_WebHook": "Одлазни ВебХоок", "Outgoing_WebHook_Description": "Добијте податке из Роцкет.Цхат у реалном времену.", "Page_title": "Наслов странице", @@ -2007,6 +2087,7 @@ "Show_Setup_Wizard": "Прикажи чаробњак за подешавање", "Show_the_keyboard_shortcut_list": "Покажите листу пречица на тастатури", "Showing_archived_results": "

      Показујући %s архивиране резултате

      ", + "Showing_online_users": null, "Showing_results": "

      Приказујем %s резултата

      ", "Sidebar": "Сидебар", "Sidebar_list_mode": "Режим листе канала бочне траке", @@ -2195,6 +2276,8 @@ "This_email_has_already_been_used_and_has_not_been_verified__Please_change_your_password": "Ова адреса је већ коришћен и није потврђена. Молимо Вас да промените лозинку.", "This_is_a_desktop_notification": "Ово је обавештење десктоп", "This_is_a_push_test_messsage": "Ово је гурање теста Месссаге", + "This_room_has_been_archived_by__username_": "Ова соба је архивиран од __username__", + "This_room_has_been_unarchived_by__username_": "Ова соба је неархивирано од __username__", "Thursday": "Четвртак", "Time_in_seconds": "Време у секундама", "Title": "наслов", @@ -2298,6 +2381,9 @@ "Use_url_for_avatar": "Користи УРЛ за аватар", "Use_User_Preferences_or_Global_Settings": "Користите Усер Преференцес или Глобал Сеттингс", "User": "Корисник", + "User__username__is_now_a_moderator_of__room_name_": "Корисник __username__ је сада модератор за __room_name__", + "User__username__is_now_a_owner_of__room_name_": "Корисник __username__ је сада власник __room_name__", + "User__username__removed_from__room_name__moderators": "Корисник __username__ уклоњен из модератори __room_name__", "User_added": "Корисник/ца додат(а)", "User_added_by": "Корисник/ца __user_added__ је додат(а) од стране __user_by__.", "User_added_successfully": "Корисник је успешно додат", @@ -2356,6 +2442,7 @@ "Username_Change_Disabled": "Ваш Роцкет.Цхат Администратор је онемогућио промену корисничких имена", "Username_description": "Корисничко име се користи да би други могли да вас спомињу у порукама.", "Username_doesnt_exist": "Корисничко име `%s` не постоји.", + "Username_ended_the_OTR_session": "__username__ завршио ОТР сесију", "Username_invalid": "%s није исправно корисничко име,
      користите само слова, бројеве, тачке и повлаке", "Username_is_already_in_here": "`@%s` је већ овде.", "Username_is_not_in_this_room": "Корисник `#%s` није у овој соби.", @@ -2365,6 +2452,7 @@ "Users_added": "Корисници су додати", "Users_in_role": "Корисници у улози", "UTF8_Names_Slugify": "УТФ8 имена Слугифи", + "Videocall_enabled": "Видео позив је омогућен", "Validate_email_address": "Потврдите е-адресу", "Verification": "Верификација", "Verification_Description": "Можете користити следеће држаче:
      • [Верифицатион_Урл] за УРЛ за верификацију.
      • [име], [фнаме], [лнаме] за пуно име, презиме или презиме корисника.
      • [емаил] за е-пошту корисника.
      • [Сите_Наме] и [Сите_УРЛ] за име апликације и УРЛ адресу респективно.
      ", @@ -2379,7 +2467,6 @@ "Video_Conference": "Видео конференција", "Video_message": "Видео порука", "Videocall_declined": "Видео позив је одбачен.", - "Videocall_enabled": "Видео позив је омогућен", "View_All": "Погледај све", "View_Logs": "Погледај протоколе", "View_mode": "Опције приказа", @@ -2465,6 +2552,7 @@ "Yes_unarchive_it": "Да, одвојите га!", "yesterday": "јуче", "You": "ти", + "you_are_in_preview_mode_of": "Ви сте у режиму прегледа од канала # __room_name__", "You_are_logged_in_as": "Пријављени сте као", "You_are_not_authorized_to_view_this_page": "Нисте ауторизовани да видите ову страницу.", "You_can_change_a_different_avatar_too": "Можете премостити аватар користити за постављање из ове интеграције.", @@ -2498,4 +2586,4 @@ "Your_push_was_sent_to_s_devices": "Ваш притиском је послат на %s уређајима", "Your_server_link": "Веза са сервером", "Your_workspace_is_ready": "Ваш радни простор је спреман за кориштење 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/sv.i18n.json b/packages/rocketchat-i18n/i18n/sv.i18n.json index 85668f1fde4c..aaa696bfc544 100644 --- a/packages/rocketchat-i18n/i18n/sv.i18n.json +++ b/packages/rocketchat-i18n/i18n/sv.i18n.json @@ -604,8 +604,9 @@ "Content": "Innehåll", "Continue": "Fortsätt", "Continuous_sound_notifications_for_new_livechat_room": "Kontinuerliga ljudmeddelanden för nytt livechat-rum", - "Conversation": "Konversation", + "Conversation": "Meddelande", "Conversation_closed": "Konversation stängd: __comment__.", + "Conversation_finished": "Konversation avslutad", "Conversation_finished_message": "Konversation Slutfört meddelande", "conversation_with_s": "konversationen med %s", "Conversations": "Konversationer", @@ -1391,6 +1392,7 @@ "Importer_not_setup": "Importören är inte korrekt inställd, eftersom det inte returnerade några data.", "Importer_Prepare_Restart_Import": "Starta om import", "Importer_Prepare_Start_Import": "Börja importera", + "Importer_Prepare_Uncheck_Archived_Channels": "Avmarkera Arkiverade Kanaler", "Importer_Prepare_Uncheck_Deleted_Users": "Avmarkera borttagna användare", "Importer_progress_error": "Det gick inte att få information importstatus.", "Importer_setup_error": "Ett fel uppstod när importeraren skulle skapas.", @@ -1758,6 +1760,7 @@ "Max_length_is": "Max längd är%s", "Media": "Media", "Medium": "Medium", + "Members": "Medlemmar", "Members_List": "Medlemslista", "mention-all": "Nämna alla", "mention-all_description": "Tillstånd att använda @all mention", @@ -2386,6 +2389,7 @@ "Show_Setup_Wizard": "Visa installationsguiden", "Show_the_keyboard_shortcut_list": "Visa genvägslistan för tangentbordet", "Showing_archived_results": "

      Visar %s arkiverade resultat

      ", + "Showing_online_users": null, "Showing_results": "

      Visar %s resultat

      ", "Sidebar": "Sidebar", "Sidebar_list_mode": "Sidpanel Kanallista läge", @@ -2777,6 +2781,7 @@ "Users_added": "Användarna har blivit tillagda", "Users_in_role": "Användare i rollen", "UTF8_Names_Slugify": "UTF8 Names Slugify", + "Videocall_enabled": "Videosamtal aktiverat", "Validate_email_address": "Validera e-postadress", "Verification": "Verifikation", "Verification_Description": "Du kan använda följande platsinnehavare:
      • [Verification_Url] för verifieringsadressen.
      • [namn], [fname], [lname] för användarens fullständiga namn, förnamn eller efternamn.
      • [email] för användarens email.
      • [Site_Name] och [Site_URL] för respektive programnamn och URL.
      ", @@ -2792,7 +2797,6 @@ "Video_Conference": "Videokonferens", "Video_message": "Videomeddelande", "Videocall_declined": "Videokall avvisad.", - "Videocall_enabled": "Videosamtal aktiverat", "View_All": "Visa Alla", "View_Logs": "Visa loggar", "View_mode": "Visningsläge", @@ -2916,4 +2920,4 @@ "Your_push_was_sent_to_s_devices": "Din push skickades till %s enheter", "Your_server_link": "Din serverlänk", "Your_workspace_is_ready": "Din arbetsyta är redo att använda 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/ta-IN.i18n.json b/packages/rocketchat-i18n/i18n/ta-IN.i18n.json index 92007b250281..293eb0975800 100644 --- a/packages/rocketchat-i18n/i18n/ta-IN.i18n.json +++ b/packages/rocketchat-i18n/i18n/ta-IN.i18n.json @@ -555,6 +555,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "புதிய livechat அறைக்கு தொடர்ச்சியான ஒலி அறிவிப்புகள்", "Conversation": "உரையாடல்", "Conversation_closed": "உரையாடல் மூடப்பட்டது: __comment__.", + "Conversation_finished": "உரையாடலை முடித்தேன்", "Conversation_finished_message": "உரையாடல் முடிந்தது செய்தி", "conversation_with_s": "%s உடன் உரையாடல்", "Convert_Ascii_Emojis": "ஈமோஜியில் ஆஸ்கி மாற்ற", @@ -1684,6 +1685,7 @@ "Max_length_is": "அதிகபட்சம்%s", "Media": "ஊடகம்", "Medium": "நடுத்தர", + "Members": "உறுப்பினர்கள்", "Members_List": "உறுப்பினர்கள் பட்டியல்", "mention-all": "எல்லாவற்றையும் குறிப்பிடுங்கள்", "mention-all_description": "@ எல்லாவற்றையும் பயன்படுத்த அனுமதி", @@ -2299,6 +2301,7 @@ "Show_Setup_Wizard": "அமைவு வழிகாட்டி காட்டு", "Show_the_keyboard_shortcut_list": "விசைப்பலகை குறுக்குவழி பட்டியலைக் காட்டு", "Showing_archived_results": "

      %s ஐக் காட்டுகிறது காப்பகப் முடிவுகள்

      ", + "Showing_online_users": null, "Showing_results": "

      %s ஐக் காட்டுகிறது முடிவுகள்

      ", "Sidebar": "பக்கப்பட்டி", "Sidebar_list_mode": "பக்கப்பட்டி சேனல் பட்டியல் முறை", @@ -2596,6 +2599,7 @@ "User": "பயனர்", "User__username__is_now_a_moderator_of__room_name_": "பயனர் __username__ இப்போது __room_name__ ஒரு மதிப்பீட்டாளர்", "User__username__is_now_a_owner_of__room_name_": "பயனர் __username__ இப்போது __room_name__ ஒரு உரிமையாளர் ஆவார்", + "User__username__removed_from__room_name__leaders": "__room_name__ தலைவர்களிடமிருந்து பயனர் __username__ நீக்கப்பட்டது", "User__username__removed_from__room_name__moderators": "பயனர் __username__ __room_name__ நடுவர்களின் நீக்கப்பட்டது", "User__username__removed_from__room_name__owners": "பயனர் __username__ __room_name__ உரிமையாளர்கள் இருந்து நீக்கப்பட்டது", "User_added": "பயனர் __user_added__சேர்க்கப்பட்டார்.", @@ -2676,6 +2680,7 @@ "Users_added": "பயனர்கள் சேர்க்கப்பட்டுள்ளனர்", "Users_in_role": "பாத்திரத்தில் பயனர்கள்", "UTF8_Names_Slugify": "UTF8 பெயர்கள் Slugify", + "Videocall_enabled": "வீடியோ அழைப்பு இயக்கப்பட்டது", "Validate_email_address": "மின்னஞ்சல் முகவரி சரிபார்க்கவும்", "Verification": "சரிபார்ப்பு", "Verification_Description": "நீங்கள் பின்வரும் பெட்டிகளைப் பயன்படுத்தலாம்: சரிபார்ப்பு URL க்கான
      • [சரிபார்ப்பு_உருல்]. முறையே பயனரின் முழுப்பெயர், முதல் பெயர் அல்லது கடைசி பெயர்
      • [name], [fname], [lname]. பயனர் மின்னஞ்சலுக்கான
      • [மின்னஞ்சல்]. விண்ணப்பம் பெயர் மற்றும் URL ஆகியவற்றை முறையே
      • [Site_Name] மற்றும் [Site_URL].
      ", @@ -2690,7 +2695,6 @@ "Video_Conference": "வீடியோ மாநாடு", "Video_message": "வீடியோ செய்தி", "Videocall_declined": "வீடியோ அழைப்பு மறுக்கப்பட்டது.", - "Videocall_enabled": "வீடியோ அழைப்பு இயக்கப்பட்டது", "View_All": "அனைத்தையும் பார்க்க", "View_Logs": "காண்க பதிவுகள்", "View_mode": "காண்க முறையில்", @@ -2785,6 +2789,7 @@ "You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM": "நீங்கள் எளிதாக உங்கள் CRM உடன் livechat ஒருங்கிணைக்க webhooks பயன்படுத்த முடியும்.", "You_cant_leave_a_livechat_room_Please_use_the_close_button": "நீங்கள் ஒரு livechat அறையில் விட்டு போக முடியாது. தயவு செய்து, நெருங்கிய பொத்தானை பயன்படுத்த.", "You_have_been_muted": "நீங்கள் ஒலியடக்கப்பட்டுள்ளன இந்த அறையில் பேச முடியாது வேண்டும்", + "You_have_n_codes_remaining": null, "You_have_not_verified_your_email": "நீங்கள் உங்கள் மின்னஞ்சல் சரிபார்க்கப்படவில்லை.", "You_have_successfully_unsubscribed": "நீங்கள் வெற்றிகரமாக எங்கள் Mailling பட்டியல் விலகியுள்ளீர்கள்.", "You_have_to_set_an_API_token_first_in_order_to_use_the_integration": "ஒருங்கிணைப்புகளைப் பயன்படுத்த நீங்கள் முதலில் ஒரு API டோக்கனை அமைக்க வேண்டும்.", @@ -2811,4 +2816,4 @@ "Your_push_was_sent_to_s_devices": "உங்கள் மிகுதி% கள் சாதனங்கள் அனுப்பப்பட்டது", "Your_server_link": "உங்கள் சர்வர் இணைப்பு", "Your_workspace_is_ready": "உங்கள் பணியிடம் use பயன்படுத்த தயாராக உள்ளது" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/th-TH.i18n.json b/packages/rocketchat-i18n/i18n/th-TH.i18n.json index 5fa0c9f8999b..bda06fa64cd8 100644 --- a/packages/rocketchat-i18n/i18n/th-TH.i18n.json +++ b/packages/rocketchat-i18n/i18n/th-TH.i18n.json @@ -554,6 +554,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "การแจ้งเตือนเสียงอย่างต่อเนื่องสำหรับห้องสดใหม่", "Conversation": "การสนทนา", "Conversation_closed": "ปิดการสนทนา: __comment__", + "Conversation_finished": "การสนทนาเสร็จสิ้นแล้ว", "Conversation_finished_message": "การสนทนาข้อความเสร็จสิ้น", "conversation_with_s": "การสนทนากับ %s", "Convert_Ascii_Emojis": "แปลง ASCII เป็น Emoji", @@ -2670,6 +2671,7 @@ "Users_added": "มีการเพิ่มผู้ใช้แล้ว", "Users_in_role": "ผู้ใช้ที่มีบทบาท", "UTF8_Names_Slugify": "UTF8 ชื่อ Slugify", + "Videocall_enabled": "ใช้งานแฮงเอาท์วิดีโอแล้ว", "Validate_email_address": "ยืนยันที่อยู่อีเมล", "Verification": "การตรวจสอบ", "Verification_Description": "คุณสามารถใช้ตัวยึดตำแหน่งต่อไปนี้:
      • [Verification_Url] สำหรับ URL การยืนยัน
      • [name], [fname], [lname] สำหรับชื่อเต็มของผู้ใช้ชื่อหรือนามสกุลตามลำดับ
      • [email] สำหรับอีเมลของผู้ใช้
      • [Site_Name] และ [Site_URL] สำหรับชื่อแอ็พพลิเคชันและ URL ตามลำดับ
      ", @@ -2684,7 +2686,6 @@ "Video_Conference": "การประชุมทางไกลผ่านระบบวิดีโอ", "Video_message": "ข้อความวิดีโอ", "Videocall_declined": "ปฏิเสธการโทรทางวิดีโอแล้ว", - "Videocall_enabled": "ใช้งานแฮงเอาท์วิดีโอแล้ว", "View_All": "ดูสมาชิกทั้งหมด", "View_Logs": "ดูบันทึก", "View_mode": "โหมดดู", diff --git a/packages/rocketchat-i18n/i18n/tr.i18n.json b/packages/rocketchat-i18n/i18n/tr.i18n.json index 23e9e4dd517c..8a1bad1788bc 100644 --- a/packages/rocketchat-i18n/i18n/tr.i18n.json +++ b/packages/rocketchat-i18n/i18n/tr.i18n.json @@ -48,7 +48,7 @@ "Accounts_AvatarExternalProviderUrl": "Avatar için Dış Sağlayıcı URL'si", "Accounts_AvatarExternalProviderUrl_Description": "Örnek: `https://acme.com/api/v1/{username}`", "Accounts_AvatarResize": "Avatarlar Yeniden Boyutlandırılsın", - "Accounts_AvatarSize": "Avatar Boyutu", + "Accounts_AvatarSize": "Profil Resmi Boyutu", "Accounts_BlockedDomainsList": "Engellenen Alanlar Listesi", "Accounts_BlockedDomainsList_Description": "Engellenen alanların virgülle ayrılmış listesi", "Accounts_BlockedUsernameList": "Engellenen Kullanıcı Adı Listesi", @@ -261,7 +261,7 @@ "All_added_tokens_will_be_required_by_the_user": "Eklenen tüm işaretçiler kullanıcı tarafından gerekli olacaktır", "All_channels": "Tüm kanallar", "All_closed_chats_have_been_removed": "Tüm kapalı sohbetler kaldırıldı", - "All_logs": "Tüm kayıtlar", + "All_logs": "Tüm Kayıtlar", "All_messages": "Tüm iletiler", "All_users": "Tüm kullanıcılar", "All_users_in_the_channel_can_write_new_messages": "Kanaldaki tüm kullanıcılar yeni ileti yazabilir", @@ -1107,6 +1107,7 @@ "Discussion_target_channel_description": "Sormak istediğinizle ilgili bir kanal seçin", "Discussion_target_channel_prefix": "Burada bir tartışma oluşturuyorsunuz", "Discussion_title": "Yeni tartışma oluştur", + "discussion-created": "__message__", "Discussions": "Tartışmalar", "Display_chat_permissions": "Mesajlaşma yetkilerini göster", "Display_offline_form": "Çevrimdışı formu görüntüle", @@ -1539,6 +1540,7 @@ "Highlights_List": "Vurgulanacak sözcükler", "History": "Geçmiş", "Home": "Ev", + "Host": "evsahibi", "hours": "saatler", "Hours": "Saatler", "How_friendly_was_the_chat_agent": "Görüşme temsilcisi ne kadar dost canlısıydı?", @@ -1868,6 +1870,7 @@ "LDAP_User_Search_Filter_Description": "Bu filtreyle eşleşen belirtilen, sadece kullanıcıların oturum izin verilecek olursa filtre belirtilirse., belirtilen etki alanı tabanının kapsamındaki tüm kullanıcılar oturum mümkün olacak.
      Active Directory örneğin `memberOf = cn = ROCKET_CHAT, ou = Genel gruplarının iş.
      OpenLDAP için (örn genişletilebilir maç arama) `ou: dn: = ROCKET_CHAT`.", "LDAP_User_Search_Scope": "Kapsam", "LDAP_Username_Field": "Kullanıcı adı alanı", + "LDAP_Username_Field_Description": "Hangi alan yeni kullanıcılar için * kullanıcı adı * olarak kullanılacaktır. giriş sayfasında haberdar adını kullanmak için boş bırakın.
      Sen # {givenName} `gibi, çok şablon etiketlerini kullanabilirsiniz. # {Sn}`.
      Varsayılan değer `sAMAccountName` olduğunu.", "Lead_capture_email_regex": "Kurşun yakalama e-posta regex'i", "Lead_capture_phone_regex": "Telefon yakalama regex'ini yönet", "Leave": "Ayrıl", @@ -2725,6 +2728,7 @@ "Show_Setup_Wizard": "Kurulum Sihirbazını Göster", "Show_the_keyboard_shortcut_list": "Klavye kısayol listesini göster", "Showing_archived_results": "

      %s arşivlenmiş sonuçlar gösteriliyor

      ", + "Showing_online_users": null, "Showing_results": "

      %s kayıt bulundu

      ", "Sidebar": "Kenar çubuğu", "Sidebar_list_mode": "Kenar Çubuğu Kanal Listesi Modu", @@ -3172,6 +3176,7 @@ "Users_added": "Kullanıcılar eklendi", "Users_in_role": "Rol içerisindeki kullanıcılar", "UTF8_Names_Slugify": "UTF8 İsimler Slugify", + "Videocall_enabled": "Video Görüşmesi Etkin", "Validate_email_address": "E-Posta Adresini Doğrula", "Verification": "Doğrulama", "Verification_Description": "Şu yer tutucularını kullanabilirsiniz: Doğrulama URL'si için
      • [Doğrulama_Url]. Sırasıyla kullanıcının tam adı, adı veya soyadı için
      • [ad], [fname], [lname].
      • [e-posta] kullanıcının e-postası için. Sırasıyla Uygulama Adı ve URL için
      • [Site_Name] ve [Site_URL].
      ", @@ -3189,7 +3194,6 @@ "Video_Conference": "Görüntülü Görüşme", "Video_message": "Görüntülü ileti", "Videocall_declined": "Video Görüşmesi Reddedildi.", - "Videocall_enabled": "Video Görüşmesi Etkin", "Videos": "Videolar", "View_All": "Tüm Üyeleri Görüntüle", "View_Logs": "Günlükleri Görüntüle", @@ -3329,4 +3333,4 @@ "Your_question": "Sorunuz", "Your_server_link": "Sunucu bağlantınız", "Your_workspace_is_ready": "Çalışma alanınız kullanılmaya hazır 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/ug.i18n.json b/packages/rocketchat-i18n/i18n/ug.i18n.json index 3bdfe9bf36f3..dd77c6c4ac3c 100644 --- a/packages/rocketchat-i18n/i18n/ug.i18n.json +++ b/packages/rocketchat-i18n/i18n/ug.i18n.json @@ -263,6 +263,7 @@ "Confirm_password": "مەخپىي نومۇرنى جەزملەشتۈرۈش", "Conversation": "پاراڭلىشىش", "Conversation_closed": "__comment__ پاراڭلىشىش ئاخىرلاشتى", + "Conversation_finished": "سۆھبەتلىشىش ئاخىرلاشتى", "Convert_Ascii_Emojis": "خەتلەردىكى چىراي ئىپادىسىنى ئۆزلۈكىدىن تونۇش", "Copied": "كۆپەيتىلدى", "Copy": "كۆپەيتىش", @@ -311,6 +312,7 @@ "Desktop_Notifications_Enabled": "ئۈستەل يۈزى ئۇقتۇرۇشىنى ئىشلىتىش باشلاندى", "Direct_message_someone": "بىۋاسىتە مەلۇم ئادەمگە ئۇچۇر يوللاش", "Direct_Messages": "بىۋاسىتە ئۇچۇر يوللاش", + "Direct_Reply_Password": "پارول", "Display_offline_form": "تورسىز ھالەتتە جەدۋەلنى كۆرسىتىش", "Displays_action_text": "ھەرىكەتتىكى خەتلەرنى كۆرسىتىش", "Do_you_want_to_change_to_s_question": "بۇنداق قىلىپ ئۆزگەرتكۈڭىز بارمۇ ؟%s سىزنىڭ", @@ -589,6 +591,7 @@ "Layout_Sidenav_Footer_description": "260 x 70pxبەت ئاستىنىڭ چوڭ-كىچىكلىكى بولسا", "Layout_Terms_of_Service": "مۇلازىمەت تارمىقى", "LDAP": "LDAP", + "LDAP_Authentication_Password": "پارول", "LDAP_Authentication_UserDN_Description": "`cn=Administrator,cn=Users,dc=Example,dc=com`دە ئۈچىنچى تەرەپنىڭ توپلىشىشى ئۈچۈن قۇرۇلغان ھېسابات نومۇرى. ئىناۋەتلىك بولغان پۈتۈن ئىسىم ئىشلىتىڭ مەسىلەن: . LDAP كۆپ ئەھۋالدا ، ئۇ ئەزا
      نىڭ ئىچىدە ئىزدەش ۋە دەلىللەش رولىنى ئۆتەيدۇ.LDAP ئەزا باشقا ئەزالار كىرگەن چاغدا LDAP بۇ", "LDAP_BaseDN_Description": ".تىكى يۇقىرى دەرىجىلىك مۇندەرىجىنى ئاساسىي تورنامى قىلىپ ئىشلىتىشنى تەكلىپ قىلىمىز .تۆۋەنكى 'بەلگىلەنگەن تورنامى ئىزدەش'تاللاش شەرتىگە ئاساسەن ئەزا چەكلەش ئېلىپ بېرىڭ. .LDAP .ئاستىدىكى ئەزانىلا ئىزدىيەلەيدۇ.DNنىڭ ئاساسىي تور نامى بولىشىنى ئەگەر سىز چەكلىگەن بولسىڭىز بۇ ۋاقىتتا پەقەت بۇ DN.نى سىز كۆپرەپ قاتسىڭىز بولىدۇ لېكىن گۇرۇپپا ۋە گۇرۇپپا ئىچىدە ئەزا نامى چوقۇم بىر تورنامى ئاستىدا بولىشى كېرەك DN .نىڭ تارماق دەرىخى ئىچىدىن ئەزا ۋە گۇرۇپ ئىزدىشىگە قولايلىق بولىدۇ .LDAP نىڭ DNبۇنداق بولغاندا (Domain Base)نى تولدۇرۇپ ئۇنى ئاساسى توربەت نامىد قىددلىڭ ،DNئىناۋەتلىك", "LDAP_CA_Cert": "CA گۇۋاھنامىسى", @@ -663,6 +666,7 @@ "Markdown_Headers": "تېمىسىMarkdown", "Markdown_SupportSchemesForLink": "قوللايدىغان ئۇلانما كېلىشمىىMarkdown", "Markdown_SupportSchemesForLink_Description": "ئىنگلىزچە پەش ئارقىلىق ئايرىپ تۇرىدىغان كېلىشمە تىزىملىكى", + "Members": "قوللانچىلار", "Members_List": "ئەزالار تىزىملىكى", "Mentions": "تىلغا ئېلىش", "Mentions_default": "تىلغا ئېلىش (بەلگىلەنگەن)", @@ -975,6 +979,7 @@ "Show_only_online": "توردا بار ئەزانى كۆرسىتىش", "Show_preregistration_form": "كىرىشتىن بۇرۇنقى ھۆججەتنى كۆرسىتىش", "Showing_archived_results": "

      ئاللىبۇرۇن تۈرگە ئايرىپ ساقلاندى %s كۆرسىتىش

      ", + "Showing_online_users": null, "Showing_results": "تال نەتىجە %sكۆرسىتىش

      ", "since_creation": "دىن باشلانغان %s", "Site_Name": "تور نامى", @@ -1076,6 +1081,7 @@ "This_is_a_desktop_notification": "بۇ بىر ئۈستەل ئۇقتۇرۇىشى", "This_is_a_push_test_messsage": "بۇ بىر سىناق ئۇچۇر", "This_room_has_been_archived_by__username_": "بۇ ياتاق پېچەتلەندى ، __username__تەرىپىدىن", + "This_room_has_been_unarchived_by__username_": "تەرىپىدىن بۇ ياتاق پېچەتلەشتىن بىكار قىلىندى __username__", "Time_in_seconds": "ۋاقىت ()سېكۇنت", "Title": "ماۋزۇ", "Title_bar_color": "ماۋزۇ بۆلىكى رەڭگى", @@ -1174,6 +1180,7 @@ "View_All": "ھەممىنى كۆرۈش", "View_Logs": "زىيارەت قىلغاندىكى كۈندىلىك خاتىرە", "View_mode": "ۋېدىئو ئەندىزىسى", + "view-logs": "زىيارەت قىلغاندىكى كۈندىلىك خاتىرە", "Viewing_room_administration": "مەنزىرە قىلىدىغان ئۆينى باشقۇرۇش", "Visibility": "كۆرۈنۈش دەرىجىسى", "Visible": "كۆرۈنىدىغان", @@ -1234,4 +1241,4 @@ "Your_mail_was_sent_to_s": "يوللاندى %s سىزنىڭ ئىلخىتىڭىز ئاللىبۇرۇن", "Your_password_is_wrong": "پارول خاتا !", "Your_push_was_sent_to_s_devices": "ئۈسكىنىگە يوللاندى %s سىزنىڭ ئىتتىرگىنىڭىز" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/uk.i18n.json b/packages/rocketchat-i18n/i18n/uk.i18n.json index b8dc3c7b161b..57b9b4837766 100644 --- a/packages/rocketchat-i18n/i18n/uk.i18n.json +++ b/packages/rocketchat-i18n/i18n/uk.i18n.json @@ -1196,6 +1196,7 @@ "Discussion_target_channel_description": "Виберіть канал, пов’язаний із тим, що ви хочете запитати", "Discussion_target_channel_prefix": "Ви створили обговорення в", "Discussion_title": "Створити нове обговорення", + "discussion-created": "__message__", "Discussions": "Обговорення", "Display_chat_permissions": "Відображення дозволів чату", "Display_offline_form": "Відобразити офлайн-форму", @@ -2191,6 +2192,7 @@ "Me": "Я", "Media": "Медіа", "Medium": "Середній", + "Members": "Учасники", "Members_List": "Список учасників", "mention-all": "Згадати все", "mention-all_description": "Дозвіл на використання @all згадки", @@ -2849,6 +2851,7 @@ "Show_Setup_Wizard": "Показати майстер налаштування", "Show_the_keyboard_shortcut_list": "Показати список комбінацій клавіш", "Showing_archived_results": "

      Відображення %s заархівовані результати

      ", + "Showing_online_users": null, "Showing_results": "

      Показано результатів %s

      ", "Sidebar": "Бічна панель", "Sidebar_list_mode": "Режим списку каналів бічної панелі", @@ -3262,6 +3265,7 @@ "Users_in_role": "Користувачі з роллю", "Uses": "Використовує", "UTF8_Names_Slugify": "UTF8 Імена Slugify", + "Videocall_enabled": "Відеодзвінок увімкнено", "Validate_email_address": "Підтвердити адресу електронної пошти", "Verification": "Верифікація", "Verification_Description": "Ви можете використовувати наступні заповнювачі:
      • [Verification_Url] для URL-адреси для підтвердження.
      • [ім'я], [ім'я-псевдоніма], [Lname] для повного імені користувача, прізвища або прізвища, відповідно.
      • [email] для електронної пошти користувача.
      • [Назва сайту] і [Site_URL] для імені додатка та URL-адреси, відповідно.
      ", @@ -3277,7 +3281,6 @@ "Video_Conference": "Відеоконференція", "Video_message": "Відео повідомлення", "Videocall_declined": "Відеодзвінок відхилено.", - "Videocall_enabled": "Відеодзвінок увімкнено", "Videos": "Відео", "View_All": "Показати все", "View_Logs": "Перегляд журналів", @@ -3416,4 +3419,4 @@ "Your_server_link": "Посилання на Ваш сервер", "Your_temporary_password_is_password": "Ваш тимчасовий пароль [password].", "Your_workspace_is_ready": "Ваш робочий простір готовий до використання 🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/vi-VN.i18n.json b/packages/rocketchat-i18n/i18n/vi-VN.i18n.json index ab283e8a0a5e..6ed3e62af3b6 100644 --- a/packages/rocketchat-i18n/i18n/vi-VN.i18n.json +++ b/packages/rocketchat-i18n/i18n/vi-VN.i18n.json @@ -546,6 +546,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "Thông báo âm thanh liên tục cho phòng livechat mới", "Conversation": "Cuộc hội thoại", "Conversation_closed": "Trò chuyện đóng lại: __comment__.", + "Conversation_finished": "Cuộc hội thoại đã kết thúc", "Conversation_finished_message": "Tin nhắn hoàn thành cuộc trò chuyện", "conversation_with_s": "cuộc trò chuyện với %s", "Convert_Ascii_Emojis": "Chuyển đổi ASCII sang Emoji", @@ -2675,6 +2676,7 @@ "Users_added": "Người dùng đã được thêm vào", "Users_in_role": "Người dùng có vai trò", "UTF8_Names_Slugify": "UTF8 Tên Slugify", + "Videocall_enabled": "Bật cuộc gọi điện video", "Validate_email_address": "Xác nhận Địa chỉ Email", "Verification": "Xác minh", "Verification_Description": "Bạn có thể sử dụng các placeholder sau:
      • [Verification_Url] cho URL xác minh.
      • [name], [fname], [lname] tương ứng cho tên, họ hoặc họ của người dùng, tương ứng.
      • [email] cho email của người dùng.
      • [Site_Name] và [Site_URL] cho Tên Ứng dụng và URL tương ứng.
      ", @@ -2689,7 +2691,6 @@ "Video_Conference": "Hội nghị Video", "Video_message": "Tin nhắn video", "Videocall_declined": "Cuộc gọi video bị Từ chối.", - "Videocall_enabled": "Bật cuộc gọi điện video", "View_All": "Xem tất cả thành viên", "View_Logs": "Xem các bản ghi", "View_mode": "Chế độ xem", diff --git a/packages/rocketchat-i18n/i18n/zh-HK.i18n.json b/packages/rocketchat-i18n/i18n/zh-HK.i18n.json index d42251b9dd47..5e1669a48b8b 100644 --- a/packages/rocketchat-i18n/i18n/zh-HK.i18n.json +++ b/packages/rocketchat-i18n/i18n/zh-HK.i18n.json @@ -576,6 +576,7 @@ "Continuous_sound_notifications_for_new_livechat_room": "新的即时聊天室的连续声音通知", "Conversation": "会话", "Conversation_closed": "对话已关闭:__comment__。", + "Conversation_finished": "對話已結束", "Conversation_finished_message": "对话完成的消息", "conversation_with_s": "与%s的对话", "Convert_Ascii_Emojis": "自动识别文字中的表情", @@ -1705,6 +1706,7 @@ "Max_length_is": "最大长度是%s", "Media": "媒体", "Medium": "中", + "Members": "成员", "Members_List": "成员列表", "mention-all": "提及所有", "mention-all_description": "允许使用@all提到", @@ -2322,6 +2324,7 @@ "Show_Setup_Wizard": "显示安装向导", "Show_the_keyboard_shortcut_list": "显示键盘快捷键列表", "Showing_archived_results": "

      显示%s已存档结果

      ", + "Showing_online_users": null, "Showing_results": "

      显示%s条结果

      ", "Sidebar": "侧边栏", "Sidebar_list_mode": "边栏频道列表模式", @@ -2698,6 +2701,7 @@ "Users_added": "用户已被添加", "Users_in_role": "角色中的用户", "UTF8_Names_Slugify": "UTF8命名Slugify", + "Videocall_enabled": "视频通话已启用", "Validate_email_address": "验证电子邮件地址", "Verification": "验证", "Verification_Description": "您可以使用以下占位符:
      • [Verification_Url]获取验证网址。
      • [姓名],[fname],[lname]分别代表用户的全名,名字或姓氏。用户的电子邮件为
      • [email]。分别为应用程序名称和URL分别为
      • [Site_Name]和[Site_URL]。
      ", @@ -2712,7 +2716,6 @@ "Video_Conference": "视频会议", "Video_message": "视频消息", "Videocall_declined": "视频通话被拒绝。", - "Videocall_enabled": "视频通话已启用", "View_All": "查看全部", "View_Logs": "查看日志", "View_mode": "视图", @@ -2835,4 +2838,4 @@ "Your_push_was_sent_to_s_devices": "您的推送已发送到%s设备", "Your_server_link": "您的服务器链接", "Your_workspace_is_ready": "您的工作区已准备好使用🎉" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/zh-TW.i18n.json b/packages/rocketchat-i18n/i18n/zh-TW.i18n.json index c5fef69215b2..f443c8059331 100644 --- a/packages/rocketchat-i18n/i18n/zh-TW.i18n.json +++ b/packages/rocketchat-i18n/i18n/zh-TW.i18n.json @@ -3028,7 +3028,7 @@ "Organization_Type": "組織類型", "Original": "原始的", "OS_Arch": "作業系統架構", - "OS_Cpus": "作業系統處理器數量", + "OS_Cpus": "處理器數量", "OS_Freemem": "作業系統可用記憶體", "OS_Loadavg": "作業系統平均負載", "OS_Platform": "作業系統平台", @@ -4172,6 +4172,7 @@ "Uses": "使用次數", "Uses_left": "剩使用次數", "UTF8_Names_Slugify": "UTF8 名稱 Slugify", + "Videocall_enabled": "視訊通話已啟用", "Validate_email_address": "驗證電子郵件地址", "Validation": "驗證", "Value_messages": "__value__條訊息", @@ -4193,7 +4194,6 @@ "Video_Conference": "多人視訊", "Video_message": "影音訊息", "Videocall_declined": "視訊通話被拒絕。", - "Videocall_enabled": "視訊通話已啟用", "Videos": "影片", "View_All": "檢視全部成員", "View_Logs": "查看日誌", diff --git a/packages/rocketchat-i18n/i18n/zh.i18n.json b/packages/rocketchat-i18n/i18n/zh.i18n.json index 8ae0df53fe9e..68c6bf878fb1 100644 --- a/packages/rocketchat-i18n/i18n/zh.i18n.json +++ b/packages/rocketchat-i18n/i18n/zh.i18n.json @@ -4026,6 +4026,7 @@ "Uses": "使用次数", "Uses_left": "剩余使用次数", "UTF8_Names_Slugify": "着重显示 UTF-8 名字", + "Videocall_enabled": "视频通话已启用", "Validate_email_address": "验证邮箱", "Validation": "验证", "Value_messages": "__value__ 消息", @@ -4047,7 +4048,6 @@ "Video_Conference": "视频会议", "Video_message": "视频消息", "Videocall_declined": "视频通话被拒绝。", - "Videocall_enabled": "视频通话已启用", "Videos": "视频", "View_All": "查看全部", "View_Logs": "查看日志", diff --git a/server/configuration/ldap.ts b/server/configuration/ldap.ts index ea5f3547c95f..5368d5298349 100644 --- a/server/configuration/ldap.ts +++ b/server/configuration/ldap.ts @@ -1,5 +1,4 @@ import { Accounts } from 'meteor/accounts-base'; -import { Promise } from 'meteor/promise'; import { callbacks } from '../../app/callbacks/server'; import { LDAP } from '../sdk'; diff --git a/server/cron/statistics.js b/server/cron/statistics.js index 7c51e51963cf..8f268bf0c1a5 100644 --- a/server/cron/statistics.js +++ b/server/cron/statistics.js @@ -5,8 +5,8 @@ import { getWorkspaceAccessToken } from '../../app/cloud/server'; import { statistics } from '../../app/statistics'; import { settings } from '../../app/settings/server'; -function generateStatistics(logger) { - const cronStatistics = statistics.save(); +async function generateStatistics(logger) { + const cronStatistics = await statistics.save(); cronStatistics.host = Meteor.absoluteUrl(); diff --git a/server/database/readSecondaryPreferred.ts b/server/database/readSecondaryPreferred.ts index b23e538a319c..e29e90b4dc92 100644 --- a/server/database/readSecondaryPreferred.ts +++ b/server/database/readSecondaryPreferred.ts @@ -1,6 +1,6 @@ -import { ReadPreference, Db } from 'mongodb'; +import { ReadPreference, Db, ReadPreferenceOrMode } from 'mongodb'; -export function readSecondaryPreferred(db: Db, tags: any[] = []): ReadPreference | string { +export function readSecondaryPreferred(db: Db, tags: any[] = []): ReadPreferenceOrMode { const { readPreferenceTags, readPreference } = db.options; if (tags.length) { diff --git a/server/features/EmailInbox/EmailInbox.ts b/server/features/EmailInbox/EmailInbox.ts index f483e546a4ab..4e8f4aac8607 100644 --- a/server/features/EmailInbox/EmailInbox.ts +++ b/server/features/EmailInbox/EmailInbox.ts @@ -55,7 +55,7 @@ export async function configureEmailInboxes(): Promise { } try { - await EmailMessageHistory.insertOne({ _id: email.messageId, email: emailInboxRecord.email }); + await EmailMessageHistory.create({ _id: email.messageId, email: emailInboxRecord.email }); onEmailReceived(email, emailInboxRecord.email, emailInboxRecord.department); } catch (e: any) { // In case the email message history has been received by other instance.. diff --git a/server/features/EmailInbox/EmailInbox_Incoming.ts b/server/features/EmailInbox/EmailInbox_Incoming.ts index 570a52998cba..c4edb686077f 100644 --- a/server/features/EmailInbox/EmailInbox_Incoming.ts +++ b/server/features/EmailInbox/EmailInbox_Incoming.ts @@ -27,7 +27,7 @@ type FileAttachment = { video_size?: string; } -const language = settings.get('Language') || 'en'; +const language = settings.get('Language') || 'en'; const t = (s: string): string => TAPi18n.__(s, { lng: language }); function getGuestByEmail(email: string, name: string, department?: string): any { diff --git a/server/features/EmailInbox/EmailInbox_Outgoing.ts b/server/features/EmailInbox/EmailInbox_Outgoing.ts index a9f913a2f3f9..271729d20a62 100644 --- a/server/features/EmailInbox/EmailInbox_Outgoing.ts +++ b/server/features/EmailInbox/EmailInbox_Outgoing.ts @@ -8,7 +8,8 @@ import { IEmailInbox } from '../../../definition/IEmailInbox'; import { IUser } from '../../../definition/IUser'; import { FileUpload } from '../../../app/file-upload/server'; import { slashCommands } from '../../../app/utils/server'; -import { Messages, Rooms, Uploads, Users } from '../../../app/models/server'; +import { Messages, Rooms, Users } from '../../../app/models/server'; +import { Uploads } from '../../../app/models/server/raw'; import { Inbox, inboxes } from './EmailInbox'; import { sendMessage } from '../../../app/lib/server/functions/sendMessage'; import { settings } from '../../../app/settings/server'; @@ -19,7 +20,7 @@ const livechatQuoteRegExp = /^\[\s\]\(https?:\/\/.+\/live\/.+\?msg=(?.+?)\)\ const user: IUser = Users.findOneById('rocket.cat'); -const language = settings.get('Language') || 'en'; +const language = settings.get('Language') || 'en'; const t = (s: string): string => TAPi18n.__(s, { lng: language }); const sendErrorReplyMessage = (error: string, options: any): void => { @@ -81,7 +82,11 @@ slashCommands.add('sendEmailAttachment', (command: any, params: string) => { }); } - const file = Uploads.findOneById(message.file._id); + const file = Promise.await(Uploads.findOneById(message.file._id)); + + if (!file) { + return; + } FileUpload.getBuffer(file, (_err?: Error, buffer?: Buffer) => { !_err && buffer && sendEmail(inbox, { diff --git a/server/lib/channelExport.ts b/server/lib/channelExport.ts index 720304cc9247..d8a046522482 100644 --- a/server/lib/channelExport.ts +++ b/server/lib/channelExport.ts @@ -156,9 +156,9 @@ export const sendFile = async (data: ExportFile, user: IUser): Promise => await exportMessages(); - fullFileList.forEach((attachmentData: any) => { - copyFile(attachmentData, assetsPath); - }); + for await (const attachmentData of fullFileList) { + await copyFile(attachmentData, assetsPath); + } const exportFile = `${ baseDir }-export.zip`; await makeZipFile(exportPath, exportFile); diff --git a/server/lib/fileUtils.tests.ts b/server/lib/fileUtils.tests.ts index 6c5b5853acbb..5e82ad360864 100644 --- a/server/lib/fileUtils.tests.ts +++ b/server/lib/fileUtils.tests.ts @@ -1,4 +1,3 @@ -/* eslint-env mocha */ import { expect } from 'chai'; import { fileName, joinPath } from './fileUtils'; diff --git a/server/lib/migrations.ts b/server/lib/migrations.ts index f83db8d418dd..a7be121af9d8 100644 --- a/server/lib/migrations.ts +++ b/server/lib/migrations.ts @@ -135,7 +135,7 @@ function migrate(direction: 'up' | 'down', migration: IMigration): void { log.startup(`Running ${ direction }() on version ${ migration.version }${ migration.name ? `(${ migration.name })` : '' }`); - migration[direction]?.(migration); + Promise.await(migration[direction]?.(migration)); } diff --git a/server/lib/pushConfig.js b/server/lib/pushConfig.ts similarity index 83% rename from server/lib/pushConfig.js rename to server/lib/pushConfig.ts index bb70e738c492..150617458b5f 100644 --- a/server/lib/pushConfig.js +++ b/server/lib/pushConfig.ts @@ -2,13 +2,13 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { getWorkspaceAccessToken } from '../../app/cloud/server'; -import { hasRole } from '../../app/authorization'; -import { settings } from '../../app/settings'; +import { hasRole } from '../../app/authorization/server'; +import { settings } from '../../app/settings/server'; import { appTokensCollection, Push } from '../../app/push/server'; Meteor.methods({ - push_test() { + 'push_test'() { const user = Meteor.user(); if (!user) { @@ -71,17 +71,25 @@ Meteor.methods({ }, }); -function configurePush() { - if (!settings.get('Push_enable')) { +settings.watch('Push_enable', async function(enabled) { + if (!enabled) { return; } - const gateways = settings.get('Push_enable_gateway') && settings.get('Register_Server') && settings.get('Cloud_Service_Agree_PrivacyTerms') - ? settings.get('Push_gateway').split('\n') + ? settings.get('Push_gateway').split('\n') : undefined; - let apn; - let gcm; + let apn: { + apiKey?: string; + passphrase: string; + key: string; + cert: string; + gateway?: string; + } | undefined; + let gcm: { + apiKey: string; + projectNumber: string; + } | undefined; if (!gateways) { gcm = { @@ -120,9 +128,7 @@ function configurePush() { gateways, uniqueId: settings.get('uniqueID'), getAuthorization() { - return `Bearer ${ getWorkspaceAccessToken() }`; + return `Bearer ${ Promise.await(getWorkspaceAccessToken()) }`; }, }); -} - -Meteor.startup(configurePush); +}); diff --git a/server/lib/sendMessagesToAdmins.d.ts b/server/lib/sendMessagesToAdmins.d.ts deleted file mode 100644 index 6c418291f9c4..000000000000 --- a/server/lib/sendMessagesToAdmins.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -type MessageToAdmin = { - fromId?: string; - checkFrom?: boolean; - msgs?: any[] | Function; - banners?: any[] | Function; -}; - -export declare function sendMessagesToAdmins(config: MessageToAdmin): void; diff --git a/server/lib/sendMessagesToAdmins.js b/server/lib/sendMessagesToAdmins.js deleted file mode 100644 index 5eedbf14aeae..000000000000 --- a/server/lib/sendMessagesToAdmins.js +++ /dev/null @@ -1,47 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { SystemLogger } from './logger/system'; -import { Roles, Users } from '../../app/models/server'; - -export function sendMessagesToAdmins({ - fromId = 'rocket.cat', - checkFrom = true, - msgs = [], - banners = [], -}) { - const fromUser = checkFrom ? Users.findOneById(fromId, { fields: { _id: 1 } }) : true; - - Roles.findUsersInRole('admin').forEach((adminUser) => { - if (fromUser) { - try { - Meteor.runAsUser(fromId, () => Meteor.call('createDirectMessage', adminUser.username)); - - const rid = [adminUser._id, fromId].sort().join(''); - - if (typeof msgs === 'function') { - msgs = msgs({ adminUser }); - } - - if (!Array.isArray(msgs)) { - msgs = [msgs]; - } - - if (typeof banners === 'function') { - banners = banners({ adminUser }); - } - - if (!Array.isArray(banners)) { - banners = Array.from(banners); - } - - Meteor.runAsUser(fromId, () => { - msgs.forEach((msg) => Meteor.call('sendMessage', Object.assign({ rid }, msg))); - }); - } catch (e) { - SystemLogger.error(e); - } - } - - banners.forEach((banner) => Users.addBannerById(adminUser._id, banner)); - }); -} diff --git a/server/lib/sendMessagesToAdmins.ts b/server/lib/sendMessagesToAdmins.ts new file mode 100644 index 000000000000..eed6ad798a65 --- /dev/null +++ b/server/lib/sendMessagesToAdmins.ts @@ -0,0 +1,57 @@ +import { SystemLogger } from './logger/system'; +import { Roles, Users } from '../../app/models/server/raw'; +import { executeSendMessage } from '../../app/lib/server/methods/sendMessage'; +import { createDirectMessage } from '../methods/createDirectMessage'; +import { IUser } from '../../definition/IUser'; +import { IMessage } from '../../definition/IMessage'; + +type Banner = { + id: string; + priority: number; + title: string; + text: string; + textArguments?: string[]; + modifiers: string[]; + link: string; +}; + +const getData = (param: T[] | Function, adminUser: IUser): T[] => { + const result = typeof param === 'function' ? param({ adminUser }) : param; + + if (!Array.isArray(result)) { + return [result]; + } + + return result; +}; + +export async function sendMessagesToAdmins({ + fromId = 'rocket.cat', + checkFrom = true, + msgs = [], + banners = [], +}: { + fromId?: string; + checkFrom?: boolean; + msgs?: Partial[] | Function; + banners?: Banner[] | Function; +}): Promise { + const fromUser = checkFrom ? await Users.findOneById(fromId, { projection: { _id: 1 } }) : true; + + const users = await (await Roles.findUsersInRole('admin')).toArray(); + + for await (const adminUser of users) { + if (fromUser) { + try { + const { rid } = createDirectMessage([adminUser.username], fromId); + + getData>(msgs, adminUser) + .forEach((msg) => executeSendMessage(fromId, Object.assign({ rid }, msg))); + } catch (error) { + SystemLogger.error(error); + } + } + + await Promise.all(getData(banners, adminUser).map((banner) => Users.addBannerById(adminUser._id, banner))); + } +} diff --git a/server/main.d.ts b/server/main.d.ts deleted file mode 100644 index 7bb7db2a2641..000000000000 --- a/server/main.d.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { EJSON } from 'meteor/ejson'; -import { Db, Collection } from 'mongodb'; -import * as mongodb from 'mongodb'; - -import { IStreamer, IStreamerConstructor } from './modules/streamer/streamer.module'; - -/* eslint-disable @typescript-eslint/interface-name-prefix */ -declare module 'meteor/random' { - namespace Random { - function _randomString(numberOfChars: number, map: string): string; - } -} - -declare module 'meteor/mongo' { - namespace MongoInternals { - function defaultRemoteCollectionDriver(): any; - } -} - -declare module 'meteor/accounts-base' { - namespace Accounts { - function _bcryptRounds(): number; - - function _getLoginToken(connectionId: string): string | undefined; - - function insertUserDoc(options: Record, user: Record): string; - - function _generateStampedLoginToken(): {token: string; when: Date}; - - function _runLoginHandlers(methodInvocation: Function, loginRequest: Record): Record | undefined; - - export class ConfigError extends Error {} - - export class LoginCancelledError extends Error { - public static readonly numericError: number; - } - } -} - -declare module 'meteor/meteor' { - type globalError = Error; - namespace Meteor { - interface ErrorStatic { - new (error: string | number, reason?: string, details?: any): Error; - } - interface Error extends globalError { - error: string | number; - reason?: string; - details?: string | undefined | Record; - } - - const Streamer: IStreamerConstructor & IStreamer; - - const server: any; - - const runAsUser: (userId: string, scope: Function) => any; - - interface MethodThisType { - twoFactorChecked: boolean | undefined; - } - } -} - -declare module 'meteor/ddp-common' { - namespace DDPCommon { - function stringifyDDP(msg: EJSON): string; - function parseDDP(msg: string): EJSON; - } -} - -declare module 'meteor/routepolicy' { - export class RoutePolicy { - static declare(urlPrefix: string, type: string): void; - } -} - -declare module 'meteor/rocketchat:tap-i18n' { - namespace TAPi18n { - function __(s: string, options: { lng: string }): string; - } -} - -declare module 'meteor/promise' { - namespace Promise { - function await(): any; - } -} - -declare module 'meteor/littledata:synced-cron' { - interface ICronAddParameters { - name: string; - schedule: Function; - job: Function; - } - namespace SyncedCron { - function add(params: ICronAddParameters): string; - function remove(name: string): string; - } -} - -declare module 'meteor/mongo' { - interface RemoteCollectionDriver { - mongo: MongoConnection; - } - interface OplogHandle { - stop(): void; - onOplogEntry(trigger: Record, callback: Function): void; - onSkippedEntries(callback: Function): void; - waitUntilCaughtUp(): void; - _defineTooFarBehind(value: number): void; - } - interface MongoConnection { - db: Db; - _oplogHandle: OplogHandle; - rawCollection(name: string): Collection; - } - - namespace MongoInternals { - export const NpmModules: { - mongodb: { - version: string; - module: typeof mongodb; - }; - }; - - function defaultRemoteCollectionDriver(): RemoteCollectionDriver; - - class ConnectionClass {} - - function Connection(): ConnectionClass; - } -} - -declare module 'async_hooks' { - export class AsyncLocalStorage { - disable(): void; - - getStore(): T | undefined; - - run(store: T, callback: (...args: any[]) => void, ...args: any[]): void; - - exit(callback: (...args: any[]) => void, ...args: any[]): void; - - runSyncAndReturn(store: T, callback: (...args: any[]) => R, ...args: any[]): R; - - exitSyncAndReturn(callback: (...args: any[]) => R, ...args: any[]): R; - - enterWith(store: T): void; - } -} diff --git a/server/main.js b/server/main.ts similarity index 100% rename from server/main.js rename to server/main.ts diff --git a/server/methods/OEmbedCacheCleanup.js b/server/methods/OEmbedCacheCleanup.js index 168cc4aba10b..d2a8d651467b 100644 --- a/server/methods/OEmbedCacheCleanup.js +++ b/server/methods/OEmbedCacheCleanup.js @@ -1,11 +1,11 @@ import { Meteor } from 'meteor/meteor'; -import { OEmbedCache } from '../../app/models'; +import { OEmbedCache } from '../../app/models/server/raw'; import { settings } from '../../app/settings'; import { hasRole } from '../../app/authorization'; Meteor.methods({ - OEmbedCacheCleanup() { + async OEmbedCacheCleanup() { if (Meteor.userId() && !hasRole(Meteor.userId(), 'admin')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'OEmbedCacheCleanup', @@ -15,7 +15,7 @@ Meteor.methods({ const date = new Date(); const expirationDays = settings.get('API_EmbedCacheExpirationDays'); date.setDate(date.getDate() - expirationDays); - OEmbedCache.removeAfterDate(date); + await OEmbedCache.removeAfterDate(date); return { message: 'cache_cleared', }; diff --git a/server/methods/afterVerifyEmail.js b/server/methods/afterVerifyEmail.js deleted file mode 100644 index 5e1d65004d5b..000000000000 --- a/server/methods/afterVerifyEmail.js +++ /dev/null @@ -1,29 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Users, Roles } from '../../app/models'; - -Meteor.methods({ - afterVerifyEmail() { - const userId = Meteor.userId(); - - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'afterVerifyEmail', - }); - } - - const user = Users.findOneById(userId); - if (user && user.emails && Array.isArray(user.emails)) { - const verifiedEmail = user.emails.find((email) => email.verified); - const rolesToChangeTo = { anonymous: ['user'] }; - const rolesThatNeedChanges = user.roles.filter((role) => rolesToChangeTo[role]); - - if (rolesThatNeedChanges.length && verifiedEmail) { - rolesThatNeedChanges.forEach((role) => { - Roles.addUserRoles(user._id, rolesToChangeTo[role]); - Roles.removeUserRoles(user._id, role); - }); - } - } - }, -}); diff --git a/server/methods/afterVerifyEmail.ts b/server/methods/afterVerifyEmail.ts new file mode 100644 index 000000000000..6f6a621fc315 --- /dev/null +++ b/server/methods/afterVerifyEmail.ts @@ -0,0 +1,39 @@ +import { Meteor } from 'meteor/meteor'; + +import { Users } from '../../app/models/server'; +import { Roles } from '../../app/models/server/raw'; +import { IUser } from '../../definition/IUser'; + +const rolesToChangeTo: Map = new Map([ + ['anonymous', ['user']], +]); + +Meteor.methods({ + async afterVerifyEmail() { + const userId = Meteor.userId(); + + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'afterVerifyEmail', + }); + } + + const user = Users.findOneById(userId) as IUser; + if (user && user.emails && Array.isArray(user.emails)) { + const verifiedEmail = user.emails.find((email) => email.verified); + + const rolesThatNeedChanges = user.roles.filter((role) => rolesToChangeTo.has(role)); + + + if (verifiedEmail) { + await Promise.all(rolesThatNeedChanges.map(async (role) => { + const rolesToAdd = rolesToChangeTo.get(role); + if (rolesToAdd) { + await Roles.addUserRoles(userId, rolesToAdd); + } + await Roles.removeUserRoles(user._id, [role]); + })); + } + } + }, +}); diff --git a/server/methods/browseChannels.js b/server/methods/browseChannels.js index 03bb78922c7c..9fc942a1657b 100644 --- a/server/methods/browseChannels.js +++ b/server/methods/browseChannels.js @@ -145,7 +145,7 @@ const getTeams = (user, searchTerm, sort, pagination) => { }; }; -const getUsers = (user, text, workspace, sort, pagination) => { +const getUsers = async (user, text, workspace, sort, pagination) => { if (!user || !hasPermission(user._id, 'view-outside-room') || !hasPermission(user._id, 'view-d-room')) { return; } @@ -183,7 +183,7 @@ const getUsers = (user, text, workspace, sort, pagination) => { // Try to find federated users, when applicable if (isFederationEnabled() && workspace === 'external' && text.indexOf('@') !== -1) { - const users = federationSearchUsers(text); + const users = await federationSearchUsers(text); for (const user of users) { if (results.find((e) => e._id === user._id)) { continue; } @@ -208,7 +208,7 @@ const getUsers = (user, text, workspace, sort, pagination) => { }; Meteor.methods({ - browseChannels({ text = '', workspace = '', type = 'channels', sortBy = 'name', sortDirection = 'asc', page, offset, limit = 10 }) { + async browseChannels({ text = '', workspace = '', type = 'channels', sortBy = 'name', sortDirection = 'asc', page, offset, limit = 10 }) { const searchTerm = s.trim(escapeRegExp(text)); if (!['channels', 'users', 'teams'].includes(type) || !['asc', 'desc'].includes(sortDirection) || ((!page && page !== 0) && (!offset && offset !== 0))) { diff --git a/server/methods/createDirectMessage.js b/server/methods/createDirectMessage.js index 151298a89c72..0a729ce72380 100644 --- a/server/methods/createDirectMessage.js +++ b/server/methods/createDirectMessage.js @@ -36,7 +36,7 @@ export function createDirectMessage(usernames, userId, excludeSelf = false) { // If the username does have an `@`, but does not exist locally, we create it first if (!to && username.indexOf('@') !== -1) { - to = addUser(username); + to = Promise.await(addUser(username)); } if (!to) { diff --git a/server/methods/deleteFileMessage.js b/server/methods/deleteFileMessage.js index 18353d1a45c8..a35a407f9211 100644 --- a/server/methods/deleteFileMessage.js +++ b/server/methods/deleteFileMessage.js @@ -5,7 +5,7 @@ import { FileUpload } from '../../app/file-upload'; import { Messages } from '../../app/models'; Meteor.methods({ - deleteFileMessage(fileID) { + async deleteFileMessage(fileID) { check(fileID, String); const msg = Messages.getMessageByFileId(fileID); diff --git a/server/methods/deleteUser.js b/server/methods/deleteUser.js index f47da58045e4..20ea77dd30b1 100644 --- a/server/methods/deleteUser.js +++ b/server/methods/deleteUser.js @@ -7,7 +7,7 @@ import { callbacks } from '../../app/callbacks/server'; import { deleteUser } from '../../app/lib/server'; Meteor.methods({ - deleteUser(userId, confirmRelinquish = false) { + async deleteUser(userId, confirmRelinquish = false) { check(userId, String); if (!Meteor.userId()) { @@ -46,7 +46,7 @@ Meteor.methods({ }); } - deleteUser(userId, confirmRelinquish); + await deleteUser(userId, confirmRelinquish); callbacks.run('afterDeleteUser', user); diff --git a/server/methods/registerUser.js b/server/methods/registerUser.js index cef1d1b8398b..5dd2675bc384 100644 --- a/server/methods/registerUser.js +++ b/server/methods/registerUser.js @@ -2,14 +2,15 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { Accounts } from 'meteor/accounts-base'; import s from 'underscore.string'; +import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import { Users } from '../../app/models'; import { settings } from '../../app/settings'; -import { validateEmailDomain, passwordPolicy } from '../../app/lib'; +import { validateEmailDomain, passwordPolicy, RateLimiter } from '../../app/lib'; import { validateInviteToken } from '../../app/invites/server/functions/validateInviteToken'; Meteor.methods({ - registerUser(formData) { + async registerUser(formData) { const AllowAnonymousRead = settings.get('Accounts_AllowAnonymousRead'); const AllowAnonymousWrite = settings.get('Accounts_AllowAnonymousWrite'); const manuallyApproveNewUsers = settings.get('Accounts_ManuallyApproveNewUsers'); @@ -45,7 +46,7 @@ Meteor.methods({ } try { - validateInviteToken(formData.secretURL); + await validateInviteToken(formData.secretURL); } catch (e) { throw new Meteor.Error('error-user-registration-secret', 'User registration is only allowed via Secret URL', { method: 'registerUser' }); } @@ -97,3 +98,18 @@ Meteor.methods({ return userId; }, }); + +let registerUserRuleId = RateLimiter.limitMethod('registerUser', + settings.get('Rate_Limiter_Limit_RegisterUser'), + settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'), { + userId() { return true; }, + }); + + +settings.watch('Rate_Limiter_Limit_RegisterUser', (value) => { + // remove old DDP rate limiter rule and create a new one with the updated setting value + DDPRateLimiter.removeRule(registerUserRuleId); + registerUserRuleId = RateLimiter.limitMethod('registerUser', value, settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'), { + userId() { return true; }, + }); +}); diff --git a/server/methods/removeRoomOwner.js b/server/methods/removeRoomOwner.ts similarity index 86% rename from server/methods/removeRoomOwner.js rename to server/methods/removeRoomOwner.ts index 9ae598883c6b..adb33a7b29b5 100644 --- a/server/methods/removeRoomOwner.js +++ b/server/methods/removeRoomOwner.ts @@ -1,14 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { hasPermission, getUsersInRole } from '../../app/authorization'; -import { Users, Subscriptions, Messages } from '../../app/models'; -import { settings } from '../../app/settings'; +import { hasPermission, getUsersInRole } from '../../app/authorization/server'; +import { Users, Subscriptions, Messages } from '../../app/models/server'; +import { settings } from '../../app/settings/server'; import { api } from '../sdk/api'; import { Team } from '../sdk'; Meteor.methods({ - removeRoomOwner(rid, userId) { + async removeRoomOwner(rid, userId) { check(rid, String); check(userId, String); @@ -45,7 +45,7 @@ Meteor.methods({ }); } - const numOwners = getUsersInRole('owner', rid).count(); + const numOwners = await (await getUsersInRole('owner', rid)).count(); if (numOwners === 1) { throw new Meteor.Error('error-remove-last-owner', 'This is the last owner. Please set a new owner before removing this one.', { @@ -65,9 +65,9 @@ Meteor.methods({ role: 'owner', }); - const team = Promise.await(Team.getOneByMainRoomId(rid)); + const team = await Team.getOneByMainRoomId(rid); if (team) { - Promise.await(Team.removeRolesFromMember(team._id, userId, ['owner'])); + await Team.removeRolesFromMember(team._id, userId, ['owner']); } if (settings.get('UI_DisplayRoles')) { diff --git a/server/methods/removeUserFromRoom.js b/server/methods/removeUserFromRoom.ts similarity index 89% rename from server/methods/removeUserFromRoom.js rename to server/methods/removeUserFromRoom.ts index 70576c73ced4..d4b8ac0f9a01 100644 --- a/server/methods/removeUserFromRoom.js +++ b/server/methods/removeUserFromRoom.ts @@ -1,14 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { hasPermission, hasRole, getUsersInRole, removeUserFromRoles } from '../../app/authorization'; -import { Users, Subscriptions, Rooms, Messages } from '../../app/models'; -import { callbacks } from '../../app/callbacks'; +import { hasPermission, hasRole, getUsersInRole, removeUserFromRoles } from '../../app/authorization/server'; +import { Users, Subscriptions, Rooms, Messages } from '../../app/models/server'; +import { callbacks } from '../../app/callbacks/server'; import { roomTypes, RoomMemberActions } from '../../app/utils/server'; import { Team } from '../sdk'; Meteor.methods({ - removeUserFromRoom(data) { + async removeUserFromRoom(data) { check(data, Match.ObjectIncluding({ rid: String, username: String, @@ -48,7 +48,7 @@ Meteor.methods({ } if (hasRole(removedUser._id, 'owner', room._id)) { - const numOwners = getUsersInRole('owner', room._id).fetch().length; + const numOwners = await (await getUsersInRole('owner', room._id)).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.', { @@ -74,7 +74,7 @@ Meteor.methods({ if (room.teamId && room.teamMain) { // if a user is kicked from the main team room, delete the team membership - Promise.await(Team.removeMember(room.teamId, removedUser._id)); + await Team.removeMember(room.teamId, removedUser._id); } Meteor.defer(function() { diff --git a/server/methods/reportMessage.js b/server/methods/reportMessage.js index 9ff33ef8426d..ea2f42daa1e5 100644 --- a/server/methods/reportMessage.js +++ b/server/methods/reportMessage.js @@ -1,10 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { Messages, Reports } from '../../app/models'; +import { Messages } from '../../app/models/server'; +import { Reports } from '../../app/models/server/raw'; Meteor.methods({ - reportMessage(messageId, description) { + async reportMessage(messageId, description) { check(messageId, String); check(description, String); @@ -27,6 +28,8 @@ Meteor.methods({ }); } - return Reports.createWithMessageDescriptionAndUserId(message, description, Meteor.userId()); + await Reports.createWithMessageDescriptionAndUserId(message, description, Meteor.userId()); + + return true; }, }); diff --git a/server/methods/requestDataDownload.js b/server/methods/requestDataDownload.js index f388b4831bd0..a49654f4807a 100644 --- a/server/methods/requestDataDownload.js +++ b/server/methods/requestDataDownload.js @@ -4,8 +4,8 @@ import path from 'path'; import mkdirp from 'mkdirp'; import { Meteor } from 'meteor/meteor'; -import { ExportOperations, UserDataFiles } from '../../app/models'; -import { settings } from '../../app/settings'; +import { ExportOperations, UserDataFiles } from '../../app/models/server/raw'; +import { settings } from '../../app/settings/server'; import { DataExport } from '../../app/user-data-download/server/DataExport'; let tempFolder = '/tmp/userData'; @@ -16,13 +16,13 @@ if (settings.get('UserData_FileSystemPath') != null) { } Meteor.methods({ - requestDataDownload({ fullExport = false }) { + async requestDataDownload({ fullExport = false }) { const currentUserData = Meteor.user(); const userId = currentUserData._id; - const lastOperation = ExportOperations.findLastOperationByUser(userId, fullExport); + const lastOperation = await ExportOperations.findLastOperationByUser(userId, fullExport); const requestDay = lastOperation ? lastOperation.createdAt : new Date(); - const pendingOperationsBeforeMyRequestCount = ExportOperations.findAllPendingBeforeMyRequest(requestDay).count(); + const pendingOperationsBeforeMyRequestCount = await ExportOperations.findAllPendingBeforeMyRequest(requestDay).count(); if (lastOperation) { const yesterday = new Date(); @@ -30,7 +30,7 @@ Meteor.methods({ if (lastOperation.createdAt > yesterday) { if (lastOperation.status === 'completed') { - const file = lastOperation.fileId ? UserDataFiles.findOneById(lastOperation.fileId) : UserDataFiles.findLastFileByUser(userId); + const file = lastOperation.fileId ? await UserDataFiles.findOneById(lastOperation.fileId) : await UserDataFiles.findLastFileByUser(userId); if (file) { return { requested: false, @@ -64,7 +64,7 @@ Meteor.methods({ userData: currentUserData, }; - const id = ExportOperations.create(exportOperation); + const id = await ExportOperations.create(exportOperation); exportOperation._id = id; const folderName = path.join(tempFolder, id); @@ -81,7 +81,7 @@ Meteor.methods({ exportOperation.assetsPath = assetsFolder; exportOperation.status = 'pending'; - ExportOperations.updateOperation(exportOperation); + await ExportOperations.updateOperation(exportOperation); return { requested: true, diff --git a/server/modules/notifications/notifications.module.ts b/server/modules/notifications/notifications.module.ts index da674daf8ca5..ba11ab8db5a0 100644 --- a/server/modules/notifications/notifications.module.ts +++ b/server/modules/notifications/notifications.module.ts @@ -1,4 +1,5 @@ -import { IStreamer, IStreamerConstructor, IPublication } from '../streamer/streamer.module'; +import type { IStreamer, IStreamerConstructor, IPublication } from 'meteor/rocketchat:streamer'; + import { Authorization } from '../../sdk'; import { RoomsRaw } from '../../../app/models/server/raw/Rooms'; import { SubscriptionsRaw } from '../../../app/models/server/raw/Subscriptions'; @@ -168,7 +169,11 @@ export class NotificationsModule { this.streamRoom.allowRead(async function(eventName, extraData): Promise { - const [rid] = eventName.split('/'); + const [rid, e] = eventName.split('/'); + + if (e === 'webrtc') { + return true; + } // typing from livechat widget if (extraData?.token) { @@ -213,7 +218,7 @@ export class NotificationsModule { return user[key] === username; } catch (e) { - SystemLogger.error(e); + SystemLogger.error('Error: ', e); return false; } } @@ -250,21 +255,41 @@ export class NotificationsModule { this.streamRoomUsers.allowRead('none'); this.streamRoomUsers.allowWrite(async function(eventName, ...args) { - if (!this.userId) { - return false; - } - const [roomId, e] = eventName.split('/'); - if (await Subscriptions.countByRoomIdAndUserId(roomId, this.userId) > 0) { + if (!this.userId) { + const room = await Rooms.findOneById(roomId, { projection: { t: 1, 'servedBy._id': 1 } }); + if (room && room.t === 'l' && e === 'webrtc' && room.servedBy) { + notifyUser(room.servedBy._id, e, ...args); + return false; + } + } else if (await Subscriptions.countByRoomIdAndUserId(roomId, this.userId) > 0) { + const livechatSubscriptions: ISubscription[] = await Subscriptions.findByLivechatRoomIdAndNotUserId(roomId, this.userId, { projection: { 'v._id': 1, _id: 0 } }).toArray(); + if (livechatSubscriptions && e === 'webrtc') { + livechatSubscriptions.forEach((subscription) => subscription.v && notifyUser(subscription.v._id, e, ...args)); + return false; + } const subscriptions: ISubscription[] = await Subscriptions.findByRoomIdAndNotUserId(roomId, this.userId, { projection: { 'u._id': 1, _id: 0 } }).toArray(); subscriptions.forEach((subscription) => notifyUser(subscription.u._id, e, ...args)); } return false; }); - this.streamUser.allowWrite('logged'); + this.streamUser.allowWrite(async function(eventName) { + const [userId, e] = eventName.split('/'); + + if (e === 'webrtc') { + return true; + } + + return (this.userId != null) && (this.userId === userId); + }); this.streamUser.allowRead(async function(eventName) { - const [userId] = eventName.split('/'); + const [userId, e] = eventName.split('/'); + + if (e === 'webrtc') { + return true; + } + return (this.userId != null) && this.userId === userId; }); diff --git a/server/modules/streamer/streamer.module.ts b/server/modules/streamer/streamer.module.ts index 4c8ae7f7d812..19745ebaff3d 100644 --- a/server/modules/streamer/streamer.module.ts +++ b/server/modules/streamer/streamer.module.ts @@ -1,4 +1,13 @@ import { EventEmitter } from 'eventemitter3'; +import type { + IPublication, + Rule, + Connection, + DDPSubscription, + IStreamer, + IRules, + TransformMessage, +} from 'meteor/rocketchat:streamer'; import { SystemLogger } from '../../lib/logger/system'; @@ -12,81 +21,6 @@ class StreamerCentralClass extends EventEmitter { export const StreamerCentral = new StreamerCentralClass(); -export type Client = { - meteorClient: boolean; - ws: any; - userId?: string; - send: Function; -} - -export interface IPublication { - onStop: Function; - stop: Function; - connection: Connection; - _session: { - sendAdded(publicationName: string, id: string, fields: Record): void; - userId?: string; - socket?: { - send: Function; - }; - }; - ready: Function; - userId: string | undefined; - client: Client; -} - -type Rule = (this: IPublication, eventName: string, ...args: any) => Promise; - -interface IRules { - [k: string]: Rule; -} - -export type Connection = any; - -export type DDPSubscription = { - eventName: string; - subscription: IPublication; -} - -export interface IStreamer { - serverOnly: boolean; - - subscriptions: Set; - - subscriptionName: string; - - allowEmit(eventName: string | boolean | Rule, fn?: Rule | 'all' | 'none' | 'logged'): void; - - allowWrite(eventName: string | boolean | Rule, fn?: Rule | 'all' | 'none' | 'logged'): void; - - allowRead(eventName: string | boolean | Rule, fn?: Rule | 'all' | 'none' | 'logged'): void; - - emit(event: string, ...data: any[]): void; - - on(event: string, fn: (...data: any[]) => void): void; - - removeSubscription(subscription: DDPSubscription, eventName: string): void; - - removeListener(event: string, fn: (...data: any[]) => void): void; - - __emit(...data: any[]): void; - - _emit(eventName: string, args: any[], origin: Connection | undefined, broadcast: boolean, transform?: TransformMessage): boolean; - - emitWithoutBroadcast(event: string, ...data: any[]): void; - - changedPayload(collection: string, id: string, fields: Record): string | false; - - _publish(publication: IPublication, eventName: string, options: boolean | {useCollection?: boolean; args?: any}): Promise; -} - -export interface IStreamerConstructor { - // eslint-disable-next-line @typescript-eslint/no-misused-new - new(name: string, options?: {retransmit?: boolean; retransmitToSelf?: boolean}): IStreamer; -} - -export type TransformMessage = (streamer: Streamer, subscription: DDPSubscription, eventName: string, args: any[], allowed: boolean | object) => string | false; - export abstract class Streamer extends EventEmitter implements IStreamer { public subscriptions = new Set(); diff --git a/server/modules/watchers/watchers.module.ts b/server/modules/watchers/watchers.module.ts index b95a6b535bad..3c9012bb9233 100644 --- a/server/modules/watchers/watchers.module.ts +++ b/server/modules/watchers/watchers.module.ts @@ -16,7 +16,7 @@ import { LivechatInquiryRaw } from '../../../app/models/server/raw/LivechatInqui import { IBaseData } from '../../../definition/IBaseData'; import { IPermission } from '../../../definition/IPermission'; import { ISetting, SettingValue } from '../../../definition/ISetting'; -import { IInquiry } from '../../../definition/IInquiry'; +import { ILivechatInquiryRecord } from '../../../definition/IInquiry'; import { UsersSessionsRaw } from '../../../app/models/server/raw/UsersSessions'; import { IUserSession } from '../../../definition/IUserSession'; import { subscriptionFields, roomFields } from './publishFields'; @@ -209,15 +209,15 @@ export function initWatchers(models: IModelsParam, broadcast: BroadcastCallback, }); } - watch(LivechatInquiry, async ({ clientAction, id, data, diff }) => { + watch(LivechatInquiry, async ({ clientAction, id, data, diff }) => { switch (clientAction) { case 'inserted': case 'updated': - data = data ?? await LivechatInquiry.findOneById(id); + data = data ?? await LivechatInquiry.findOneById(id) ?? undefined; break; case 'removed': - data = await LivechatInquiry.trashFindOneById(id); + data = await LivechatInquiry.trashFindOneById(id) ?? undefined; break; } @@ -364,13 +364,13 @@ export function initWatchers(models: IModelsParam, broadcast: BroadcastCallback, } }); - watch(Integrations, async ({ clientAction, id, data }) => { + watch(Integrations, async ({ clientAction, id, data: eventData }) => { if (clientAction === 'removed') { broadcast('watch.integrations', { clientAction, id, data: { _id: id } }); return; } - data = data ?? await Integrations.findOneById(id); + const data = eventData ?? await Integrations.findOneById(id); if (!data) { return; } diff --git a/server/publications/settings/index.js b/server/publications/settings/index.ts similarity index 54% rename from server/publications/settings/index.js rename to server/publications/settings/index.ts index 3e9db1beab85..bd2af883b14c 100644 --- a/server/publications/settings/index.js +++ b/server/publications/settings/index.ts @@ -1,38 +1,39 @@ import { Meteor } from 'meteor/meteor'; -import { Settings } from '../../../app/models/server'; import { hasPermission, hasAtLeastOnePermission } from '../../../app/authorization/server'; import { getSettingPermissionId } from '../../../app/authorization/lib'; import { SettingsEvents } from '../../../app/settings/server'; +import { Settings } from '../../../app/models/server/raw'; +import { ISetting } from '../../../definition/ISetting'; Meteor.methods({ - 'public-settings/get'(updatedAt) { + async 'public-settings/get'(updatedAt) { if (updatedAt instanceof Date) { - const records = Settings.findNotHiddenPublicUpdatedAfter(updatedAt).fetch(); + const records = await Settings.findNotHiddenPublicUpdatedAfter(updatedAt).toArray(); SettingsEvents.emit('fetch-settings', records); return { update: records, - remove: Settings.trashFindDeletedAfter(updatedAt, { + remove: await Settings.trashFindDeletedAfter(updatedAt, { hidden: { $ne: true, }, public: true, }, { - fields: { + projection: { _id: 1, _deletedAt: 1, }, - }).fetch(), + }).toArray(), }; } - const publicSettings = Settings.findNotHiddenPublic().fetch(); + const publicSettings = await Settings.findNotHiddenPublic().toArray() as ISetting[]; SettingsEvents.emit('fetch-settings', publicSettings); return publicSettings; }, - 'private-settings/get'(updatedAfter) { + async 'private-settings/get'(updatedAfter) { const uid = Meteor.userId(); if (!uid) { @@ -46,13 +47,13 @@ Meteor.methods({ return []; } - const bypass = (settings) => settings; + const bypass = (settings: T): T => settings; - const applyFilter = (fn, args) => fn(args); + const applyFilter = (fn: Function, args: any[]): any => fn(args); - const getAuthorizedSettingsFiltered = (settings) => settings.filter((record) => hasPermission(uid, getSettingPermissionId(record._id))); + const getAuthorizedSettingsFiltered = (settings: ISetting[]): ISetting[] => settings.filter((record) => hasPermission(uid, getSettingPermissionId(record._id))); - const getAuthorizedSettings = (updatedAfter, privilegedSetting) => applyFilter(privilegedSetting ? bypass : getAuthorizedSettingsFiltered, Settings.findNotHidden(updatedAfter && { updatedAfter }).fetch()); + const getAuthorizedSettings = async (updatedAfter: Date, privilegedSetting: boolean): Promise => applyFilter(privilegedSetting ? bypass : getAuthorizedSettingsFiltered, await Settings.findNotHidden(updatedAfter && { updatedAfter }).toArray()); if (!(updatedAfter instanceof Date)) { // this does not only imply an unfiltered setting range, it also identifies the caller's context: @@ -62,17 +63,17 @@ Meteor.methods({ } return { - update: getAuthorizedSettings(updatedAfter, privilegedSetting), - remove: Settings.trashFindDeletedAfter(updatedAfter, { + update: await getAuthorizedSettings(updatedAfter, privilegedSetting), + remove: await Settings.trashFindDeletedAfter(updatedAfter, { hidden: { $ne: true, }, }, { - fields: { + projection: { _id: 1, _deletedAt: 1, }, - }).fetch(), + }).toArray(), }; }, }); diff --git a/server/routes/avatar/room.js b/server/routes/avatar/room.js index 7f359f31f79f..000395d74aa9 100644 --- a/server/routes/avatar/room.js +++ b/server/routes/avatar/room.js @@ -7,17 +7,18 @@ import { setCacheAndDispositionHeaders, } from './utils'; import { FileUpload } from '../../../app/file-upload'; -import { Rooms, Avatars } from '../../../app/models/server'; +import { Rooms } from '../../../app/models/server'; +import { Avatars } from '../../../app/models/server/raw'; import { roomTypes } from '../../../app/utils'; -const getRoomAvatar = (roomId) => { +const getRoomAvatar = async (roomId) => { const room = Rooms.findOneById(roomId, { fields: { t: 1, prid: 1, name: 1, fname: 1 } }); if (!room) { return {}; } - const file = Avatars.findOneByRoomId(room._id); + const file = await Avatars.findOneByRoomId(room._id); // if it is a discussion that doesn't have it's own avatar, returns the parent's room avatar if (room.prid && !file) { @@ -27,10 +28,10 @@ const getRoomAvatar = (roomId) => { return { room, file }; }; -export const roomAvatar = Meteor.bindEnvironment(function(req, res/* , next*/) { +export const roomAvatar = Meteor.bindEnvironment(async function(req, res/* , next*/) { const roomId = decodeURIComponent(req.url.substr(1).replace(/\?.*$/, '')); - const { room, file } = getRoomAvatar(roomId); + const { room, file } = await getRoomAvatar(roomId); if (!room) { res.writeHead(404); res.end(); diff --git a/server/routes/avatar/user.js b/server/routes/avatar/user.js index ac93faca2563..2b93d20b4e7f 100644 --- a/server/routes/avatar/user.js +++ b/server/routes/avatar/user.js @@ -8,11 +8,12 @@ import { } from './utils'; import { FileUpload } from '../../../app/file-upload'; import { settings } from '../../../app/settings/server'; -import { Users, Avatars } from '../../../app/models/server'; +import { Users } from '../../../app/models/server'; +import { Avatars } from '../../../app/models/server/raw'; // request /avatar/@name forces returning the svg -export const userAvatar = Meteor.bindEnvironment(function(req, res) { +export const userAvatar = Meteor.bindEnvironment(async function(req, res) { const requestUsername = decodeURIComponent(req.url.substr(1).replace(/\?.*$/, '')); if (!requestUsername) { @@ -34,7 +35,7 @@ export const userAvatar = Meteor.bindEnvironment(function(req, res) { const reqModifiedHeader = req.headers['if-modified-since']; - const file = Avatars.findOneByName(requestUsername); + const file = await Avatars.findOneByName(requestUsername); if (file) { res.setHeader('Content-Security-Policy', 'default-src \'none\''); diff --git a/server/sdk/types/ITeamService.ts b/server/sdk/types/ITeamService.ts index a5ec4fa22ec2..511c42afc21f 100644 --- a/server/sdk/types/ITeamService.ts +++ b/server/sdk/types/ITeamService.ts @@ -12,25 +12,25 @@ export interface ITeamCreateRoom extends Omit { export interface ITeamCreateParams { team: Pick; room: ITeamCreateRoom; - members?: Array; // list of user _ids - owner?: string; // the team owner. If not present, owner = requester + members?: Array | null; // list of user _ids + owner?: string | null; // the team owner. If not present, owner = requester } export interface ITeamMemberParams { userId: string; - roles?: Array; + roles?: Array | null; } export interface IUserInfo { _id: string; - username?: string; + username?: string | null; name: string; status: string; } export interface ITeamMemberInfo { user: IUserInfo; - roles?: string[]; + roles?: string[] | null; createdBy: Omit; createdAt: Date; } @@ -41,17 +41,20 @@ export interface ITeamInfo extends ITeam { } export interface IListRoomsFilter { - name: string; + name?: string; isDefault: boolean; getAllRooms: boolean; allowPrivateTeam: boolean; } -export interface ITeamUpdateData { - name: string; - type: TEAM_TYPE; - updateRoom?: boolean; // default is true -} +export type ITeamUpdateData = + { updateRoom?: boolean } & ({ + name: string; + type?: TEAM_TYPE; + } | { + name?: string; + type: TEAM_TYPE; + }) export type ITeamAutocompleteResult = Pick; @@ -70,12 +73,14 @@ export interface ITeamService { members(uid: string, teamId: string, canSeeAll: boolean, options?: IPaginationOptions, queryOptions?: FilterQuery): Promise>; addMembers(uid: string, teamId: string, members: Array): Promise; updateMember(teamId: string, members: ITeamMemberParams): Promise; + removeMember(teamId: string, userId: string): Promise; removeMembers(uid: string, teamId: string, members: Array): Promise; getInfoByName(teamName: string): Promise | null>; getInfoById(teamId: string): Promise | null>; deleteById(teamId: string): Promise; deleteByName(teamName: string): Promise; unsetTeamIdOfRooms(teamId: string): void; + getOneById(teamId: string, options?: FindOneOptions): Promise; getOneById

      (teamId: string, options?: FindOneOptions

      ): Promise; getOneByName(teamName: string | RegExp, options?: FindOneOptions): Promise; getOneByMainRoomId(teamId: string): Promise | null>; @@ -89,4 +94,5 @@ export interface ITeamService { insertMemberOnTeams(userId: string, teamIds: Array): Promise; removeMemberFromTeams(userId: string, teamIds: Array): Promise; removeAllMembersFromTeam(teamId: string): Promise; + removeRolesFromMember(teamId: string, userId: string, roles: Array): Promise; } diff --git a/server/services/analytics/service.ts b/server/services/analytics/service.ts index b2d4b8dee9e0..ab69003a733e 100644 --- a/server/services/analytics/service.ts +++ b/server/services/analytics/service.ts @@ -1,8 +1,9 @@ -import { Db } from 'mongodb'; +import type { Db } from 'mongodb'; import { ServiceClass } from '../../sdk/types/ServiceClass'; import { IAnalyticsService } from '../../sdk/types/IAnalyticsService'; import { AnalyticsRaw } from '../../../app/models/server/raw/Analytics'; +import { IAnalyticsSeatRequest } from '../../../definition/IAnalytic'; export class AnalyticsService extends ServiceClass implements IAnalyticsService { protected name = 'analytics'; @@ -19,8 +20,8 @@ export class AnalyticsService extends ServiceClass implements IAnalyticsService } async getSeatRequestCount(): Promise { - const result = await this.Analytics.findOne({ type: 'seat-request' }); - return result ? result.count : 0; + const result = await this.Analytics.findOne({ type: 'seat-request' }, {}); + return result?.count ? result.count : 0; } async resetSeatRequestCount(): Promise { diff --git a/server/services/authorization/canAccessRoomLivechat.ts b/server/services/authorization/canAccessRoomLivechat.ts index a0dd92acbb17..0418d1056241 100644 --- a/server/services/authorization/canAccessRoomLivechat.ts +++ b/server/services/authorization/canAccessRoomLivechat.ts @@ -9,6 +9,7 @@ export const canAccessRoomLivechat: RoomAccessValidator = async (room, user, ext // room can be sent as `null` but in that case a `rid` is also sent on extraData // this is the case for file uploads const livechatRoom = room || (extraData?.rid && await Rooms.findOneById(extraData?.rid)); + if (livechatRoom?.t !== 'l') { return false; } diff --git a/server/services/authorization/service.ts b/server/services/authorization/service.ts index f5c6b0a1fa43..728d5a49c591 100644 --- a/server/services/authorization/service.ts +++ b/server/services/authorization/service.ts @@ -45,9 +45,16 @@ export class Authorization extends ServiceClass implements IAuthorization { this.Permissions = db.collection('rocketchat_permissions'); this.Users = new UsersRaw(db.collection('users')); - this.Roles = new RolesRaw(db.collection('rocketchat_roles')); - Subscriptions = new SubscriptionsRaw(db.collection('rocketchat_subscription')); + Subscriptions = new SubscriptionsRaw(db.collection('rocketchat_subscription'), { + Users: this.Users, + }); + + this.Roles = new RolesRaw(db.collection('rocketchat_roles'), { + Users: this.Users, + Subscriptions, + }); + Settings = new SettingsRaw(db.collection('rocketchat_settings')); Rooms = new RoomsRaw(db.collection('rocketchat_room')); TeamMembers = new TeamMemberRaw(db.collection('rocketchat_team_member')); @@ -98,7 +105,7 @@ export class Authorization extends ServiceClass implements IAuthorization { } private getPublicRoles = mem(async (): Promise => { - const roles = await this.Roles.find>({ scope: 'Users', description: { $exists: 1, $ne: '' } }, { projection: { _id: 1 } }).toArray(); + const roles = await this.Roles.find>({ scope: 'Users', description: { $exists: true, $ne: '' } }, { projection: { _id: 1 } }).toArray(); return roles.map(({ _id }) => _id); }, { maxAge: 10000 }); diff --git a/server/services/meteor/service.ts b/server/services/meteor/service.ts index 6d5290673af3..823795c31bdf 100644 --- a/server/services/meteor/service.ts +++ b/server/services/meteor/service.ts @@ -1,5 +1,4 @@ import { Meteor } from 'meteor/meteor'; -import { Promise } from 'meteor/promise'; import { ServiceConfiguration } from 'meteor/service-configuration'; import { UserPresenceMonitor, UserPresence } from 'meteor/konecty:user-presence'; import { MongoInternals } from 'meteor/mongo'; @@ -50,7 +49,7 @@ type Callbacks = { let processOnChange: (diff: Record, id: string) => void; // eslint-disable-next-line no-undef -const disableOplog = Package['disable-oplog']; +const disableOplog = !!(Package as any)['disable-oplog']; const serviceConfigCallbacks = new Set(); if (disableOplog) { diff --git a/server/services/nps/notification.ts b/server/services/nps/notification.ts index 303470dadfa2..0c65301812a5 100644 --- a/server/services/nps/notification.ts +++ b/server/services/nps/notification.ts @@ -6,10 +6,10 @@ import moment from 'moment'; import { settings } from '../../../app/settings/server'; import { IBanner, BannerPlatform } from '../../../definition/IBanner'; -import { sendMessagesToAdmins } from '../../lib/sendMessagesToAdmins.js'; +import { sendMessagesToAdmins } from '../../lib/sendMessagesToAdmins'; export const getBannerForAdmins = Meteor.bindEnvironment((expireAt: Date): Omit => { - const lng = settings.get('Language') || 'en'; + const lng = settings.get('Language') || 'en'; return { platform: [BannerPlatform.Web], @@ -39,9 +39,9 @@ export const getBannerForAdmins = Meteor.bindEnvironment((expireAt: Date): Omit< }); export const notifyAdmins = Meteor.bindEnvironment((expireAt: Date) => { - sendMessagesToAdmins({ + Promise.await(sendMessagesToAdmins({ msgs: ({ adminUser }: { adminUser: any }): any => ({ msg: TAPi18n.__('NPS_survey_is_scheduled_to-run-at__date__for_all_users', { date: moment(expireAt).format('YYYY-MM-DD'), lng: adminUser.language }), }), - }); + })); }); diff --git a/server/services/team/service.ts b/server/services/team/service.ts index 0bc5106439a7..d76e8bab32a9 100644 --- a/server/services/team/service.ts +++ b/server/services/team/service.ts @@ -59,10 +59,12 @@ export class TeamService extends ServiceClass implements ITeamService { super(); this.RoomsModel = new RoomsRaw(db.collection('rocketchat_room')); - this.SubscriptionsModel = new SubscriptionsRaw(db.collection('rocketchat_subscription')); + this.Users = new UsersRaw(db.collection('users')); + this.SubscriptionsModel = new SubscriptionsRaw(db.collection('rocketchat_subscription'), { + Users: this.Users, + }); this.TeamModel = new TeamRaw(db.collection('rocketchat_team')); this.TeamMembersModel = new TeamMemberRaw(db.collection('rocketchat_team_member')); - this.Users = new UsersRaw(db.collection('users')); this.MessagesModel = new MessagesRaw(db.collection('rocketchat_message')); } diff --git a/server/startup/initialData.js b/server/startup/initialData.js index 7db252c92b03..668426a7d3ca 100644 --- a/server/startup/initialData.js +++ b/server/startup/initialData.js @@ -1,15 +1,15 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; -import _ from 'underscore'; import { RocketChatFile } from '../../app/file'; -import { FileUpload } from '../../app/file-upload'; -import { addUserRoles, getUsersInRole } from '../../app/authorization'; -import { Users, Settings, Rooms } from '../../app/models'; -import { settings } from '../../app/settings'; -import { checkUsernameAvailability, addUserToDefaultChannels } from '../../app/lib'; - -Meteor.startup(function() { +import { FileUpload } from '../../app/file-upload/server'; +import { addUserRoles, getUsersInRole } from '../../app/authorization/server'; +import { Users, Rooms } from '../../app/models/server'; +import { settings } from '../../app/settings/server'; +import { checkUsernameAvailability, addUserToDefaultChannels } from '../../app/lib/server'; +import { Settings } from '../../app/models/server/raw'; + +Meteor.startup(async function() { if (settings.get('Show_Setup_Wizard') === 'pending' && !Rooms.findOneById('GENERAL')) { Rooms.createWithIdTypeAndName('GENERAL', 'c', 'general', { default: true, @@ -48,7 +48,7 @@ Meteor.startup(function() { } if (process.env.ADMIN_PASS) { - if (_.isEmpty(getUsersInRole('admin').fetch())) { + if (await (await getUsersInRole('admin')).count() === 0) { console.log('Inserting admin user:'.green); const adminUser = { name: 'Administrator', @@ -134,16 +134,16 @@ Meteor.startup(function() { } } - if (_.isEmpty(getUsersInRole('admin').fetch())) { + if (await (await getUsersInRole('admin')).count() === 0) { const oldestUser = Users.getOldest({ _id: 1, username: 1, name: 1 }); if (oldestUser) { - addUserRoles(oldestUser._id, 'admin'); + addUserRoles(oldestUser._id, ['admin']); console.log(`No admins are found. Set ${ oldestUser.username || oldestUser.name } as admin for being the oldest user`); } } - if (!_.isEmpty(getUsersInRole('admin').fetch())) { + if (await (await getUsersInRole('admin')).count() !== 0) { if (settings.get('Show_Setup_Wizard') === 'pending') { console.log('Setting Setup Wizard to "in_progress" because, at least, one admin was found'); Settings.updateValueById('Show_Setup_Wizard', 'in_progress'); @@ -189,7 +189,7 @@ Meteor.startup(function() { Accounts.setPassword(adminUser._id, adminUser._id); - addUserRoles(adminUser._id, 'admin'); + addUserRoles(adminUser._id, ['admin']); if (settings.get('Show_Setup_Wizard') === 'pending') { Settings.updateValueById('Show_Setup_Wizard', 'in_progress'); diff --git a/server/startup/instance.js b/server/startup/instance.js index 5dcebcb2be5b..c4aa1ebada0b 100644 --- a/server/startup/instance.js +++ b/server/startup/instance.js @@ -5,10 +5,6 @@ import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; import { startStreamBroadcast } from '../stream/streamBroadcast'; -export function getInstances() { - InstanceStatus.find().fetch(); -} - Meteor.startup(function() { const instance = { host: process.env.INSTANCE_IP ? String(process.env.INSTANCE_IP).trim() : 'localhost', diff --git a/server/startup/migrations/index.ts b/server/startup/migrations/index.ts index 37f25773c4e2..c3b3ff9fca6c 100644 --- a/server/startup/migrations/index.ts +++ b/server/startup/migrations/index.ts @@ -67,4 +67,10 @@ import './v240'; import './v241'; import './v242'; import './v243'; +import './v244'; +import './v245'; +import './v246'; +import './v247'; +import './v248'; +import './v249'; import './xrun'; diff --git a/server/startup/migrations/v174.js b/server/startup/migrations/v174.ts similarity index 53% rename from server/startup/migrations/v174.js rename to server/startup/migrations/v174.ts index a03aeb430a15..f9b063903599 100644 --- a/server/startup/migrations/v174.js +++ b/server/startup/migrations/v174.ts @@ -1,5 +1,5 @@ +import { Permissions } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Permissions } from '../../../app/models/server'; const appRolePermissions = [ 'api-bypass-rate-limit', @@ -18,9 +18,9 @@ const appRolePermissions = [ addMigration({ version: 174, up() { - Permissions.update({ _id: { $in: appRolePermissions } }, { $addToSet: { roles: 'app' } }, { multi: true }); + return Permissions.update({ _id: { $in: appRolePermissions } }, { $addToSet: { roles: 'app' } }, { multi: true }); }, down() { - Permissions.update({ _id: { $in: appRolePermissions } }, { $pull: { roles: 'app' } }, { multi: true }); + return Permissions.update({ _id: { $in: appRolePermissions } }, { $pull: { roles: 'app' } }, { multi: true }); }, }); diff --git a/server/startup/migrations/v175.js b/server/startup/migrations/v175.js index 0b12049c66d2..0d931a02e4c1 100644 --- a/server/startup/migrations/v175.js +++ b/server/startup/migrations/v175.js @@ -1,18 +1,17 @@ import { addMigration } from '../../lib/migrations'; import { theme } from '../../../app/theme/server/server'; -import { Settings } from '../../../app/models'; +import { Settings } from '../../../app/models/server/raw'; addMigration({ version: 175, up() { - Object.entries(theme.variables) + Promise.await(Promise.all(Object.entries(theme.variables) .filter(([, value]) => value.type === 'color') - .forEach(([key, { editor }]) => { - Settings.update({ _id: `theme-color-${ key }` }, { - $set: { - packageEditor: editor, - }, - }); - }); + .map(([key, { editor }]) => Settings.update({ _id: `theme-color-${ key }` }, { + $set: { + packageEditor: editor, + }, + })), + )); }, }); diff --git a/server/startup/migrations/v176.js b/server/startup/migrations/v176.js deleted file mode 100644 index 4484febcb5a6..000000000000 --- a/server/startup/migrations/v176.js +++ /dev/null @@ -1,12 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { - Settings, -} from '../../../app/models'; - - -addMigration({ - version: 176, - up() { - Settings.remove({ _id: 'Livechat', type: 'group' }); - }, -}); diff --git a/server/startup/migrations/v176.ts b/server/startup/migrations/v176.ts new file mode 100644 index 000000000000..5dcbf7bc8980 --- /dev/null +++ b/server/startup/migrations/v176.ts @@ -0,0 +1,9 @@ +import { Settings } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 176, + up() { + return Settings.deleteOne({ _id: 'Livechat', type: 'group' }); + }, +}); diff --git a/server/startup/migrations/v177.js b/server/startup/migrations/v177.ts similarity index 72% rename from server/startup/migrations/v177.js rename to server/startup/migrations/v177.ts index 3acca9065d7b..51b62d3c6bf2 100644 --- a/server/startup/migrations/v177.js +++ b/server/startup/migrations/v177.ts @@ -1,16 +1,18 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 177, up() { // Disable auto opt in for existent installations - Settings.upsert({ + Settings.update({ _id: 'Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In', }, { $set: { value: false, }, + }, { + upsert: true, }); }, }); diff --git a/server/startup/migrations/v178.js b/server/startup/migrations/v178.js deleted file mode 100644 index 2ee62cfd8166..000000000000 --- a/server/startup/migrations/v178.js +++ /dev/null @@ -1,12 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { - Settings, -} from '../../../app/models'; - - -addMigration({ - version: 178, - up() { - Settings.remove({ _id: 'Livechat_enable_inquiry_fetch_by_stream' }); - }, -}); diff --git a/server/startup/migrations/v178.ts b/server/startup/migrations/v178.ts new file mode 100644 index 000000000000..503c3d66d1cd --- /dev/null +++ b/server/startup/migrations/v178.ts @@ -0,0 +1,9 @@ +import { Settings } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 178, + up() { + return Settings.removeById('Livechat_enable_inquiry_fetch_by_stream'); + }, +}); diff --git a/server/startup/migrations/v182.js b/server/startup/migrations/v182.js index fde3cc07062c..74a840c88a9a 100644 --- a/server/startup/migrations/v182.js +++ b/server/startup/migrations/v182.js @@ -1,9 +1,9 @@ import { addMigration } from '../../lib/migrations'; -import { Analytics } from '../../../app/models/server'; +import { Analytics } from '../../../app/models/server/raw'; addMigration({ version: 182, up() { - Analytics.remove({}); + Analytics.deleteMany({}); }, }); diff --git a/server/startup/migrations/v183.js b/server/startup/migrations/v183.js index e031f63f028c..525148b0f8a8 100644 --- a/server/startup/migrations/v183.js +++ b/server/startup/migrations/v183.js @@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { addMigration } from '../../lib/migrations'; -import { Rooms, Messages, Subscriptions, Uploads, Settings, Users } from '../../../app/models/server'; +import { Rooms, Messages, Subscriptions, Users } from '../../../app/models/server'; +import { Settings, Uploads } from '../../../app/models/server/raw'; const unifyRooms = (room) => { // verify if other DM already exists @@ -59,8 +60,8 @@ const fixSelfDMs = () => { }, { multi: true }); // Fix error of upload permission check using Meteor.userId() - Meteor.runAsUser(room.uids[0], () => { - Uploads.update({ rid: room._id }, { + Meteor.runAsUser(room.uids[0], async () => { + await Uploads.update({ rid: room._id }, { $set: { rid: correctId, }, @@ -90,11 +91,11 @@ const fixDiscussions = () => { }); }; -const fixUserSearch = () => { - const setting = Settings.findOneById('Accounts_SearchFields', { fields: { value: 1 } }); +const fixUserSearch = async () => { + const setting = await Settings.findOneById('Accounts_SearchFields', { projection: { value: 1 } }); const value = setting?.value?.trim(); if (value === '' || value === 'username, name') { - Settings.updateValueById('Accounts_SearchFields', 'username, name, bio'); + await Settings.updateValueById('Accounts_SearchFields', 'username, name, bio'); } Users.tryDropIndex('name_text_username_text_bio_text'); @@ -105,6 +106,6 @@ addMigration({ up() { fixDiscussions(); fixSelfDMs(); - fixUserSearch(); + return fixUserSearch(); }, }); diff --git a/server/startup/migrations/v184.js b/server/startup/migrations/v184.ts similarity index 71% rename from server/startup/migrations/v184.js rename to server/startup/migrations/v184.ts index 84112ae971a9..4542313f029f 100644 --- a/server/startup/migrations/v184.js +++ b/server/startup/migrations/v184.ts @@ -1,16 +1,18 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 184, up() { // Set SAML signature validation type to 'Either' - Settings.upsert({ + Settings.update({ _id: 'SAML_Custom_Default_signature_validation_type', }, { $set: { value: 'Either', }, + }, { + upsert: true, }); }, }); diff --git a/server/startup/migrations/v185.js b/server/startup/migrations/v185.js deleted file mode 100644 index de080b1ebab4..000000000000 --- a/server/startup/migrations/v185.js +++ /dev/null @@ -1,19 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { - Settings, -} from '../../../app/models/server'; - -addMigration({ - version: 185, - up() { - const setting = Settings.findOne({ _id: 'Message_SetNameToAliasEnabled' }); - if (setting && setting.value) { - Settings.update({ _id: 'UI_Use_Real_Name' }, { - $set: { - value: true, - }, - }); - } - Settings.remove({ _id: 'Message_SetNameToAliasEnabled' }); - }, -}); diff --git a/server/startup/migrations/v185.ts b/server/startup/migrations/v185.ts new file mode 100644 index 000000000000..c9d79130aea8 --- /dev/null +++ b/server/startup/migrations/v185.ts @@ -0,0 +1,18 @@ +import { Settings } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + + +addMigration({ + version: 185, + async up() { + const setting = await Settings.findOne({ _id: 'Message_SetNameToAliasEnabled' }); + if (setting && setting.value) { + await Settings.update({ _id: 'UI_Use_Real_Name' }, { + $set: { + value: true, + }, + }); + } + return Settings.removeById('Message_SetNameToAliasEnabled'); + }, +}); diff --git a/server/startup/migrations/v187.js b/server/startup/migrations/v187.js index 349e4142dd54..f3f958a248dc 100644 --- a/server/startup/migrations/v187.js +++ b/server/startup/migrations/v187.js @@ -1,8 +1,7 @@ import { Mongo } from 'meteor/mongo'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; -import { NotificationQueue } from '../../../app/models/server/raw'; +import { NotificationQueue, Settings } from '../../../app/models/server/raw'; function convertNotification(notification) { try { @@ -55,14 +54,14 @@ async function migrateNotifications() { addMigration({ version: 187, - up() { - Settings.remove({ _id: 'Push_send_interval' }); - Settings.remove({ _id: 'Push_send_batch_size' }); - Settings.remove({ _id: 'Push_debug' }); - Settings.remove({ _id: 'Notifications_Always_Notify_Mobile' }); + async up() { + await Settings.removeById('Push_send_interval'); + await Settings.removeById('Push_send_batch_size'); + await Settings.removeById('Push_debug'); + await Settings.removeById('Notifications_Always_Notify_Mobile'); try { - Promise.await(migrateNotifications()); + await migrateNotifications(); } catch (err) { // Ignore if the collection does not exist if (!err.code || err.code !== 26) { diff --git a/server/startup/migrations/v188.js b/server/startup/migrations/v188.ts similarity index 51% rename from server/startup/migrations/v188.js rename to server/startup/migrations/v188.ts index b63ca803a96e..50bc75990d03 100644 --- a/server/startup/migrations/v188.js +++ b/server/startup/migrations/v188.ts @@ -1,5 +1,6 @@ +import { Permissions } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Permissions } from '../../../app/models/server'; + const newRolePermissions = [ 'view-d-room', @@ -11,6 +12,6 @@ const roleName = 'guest'; addMigration({ version: 188, up() { - Permissions.update({ _id: { $in: newRolePermissions } }, { $addToSet: { roles: roleName } }, { multi: true }); + return Permissions.update({ _id: { $in: newRolePermissions } }, { $addToSet: { roles: roleName } }, { multi: true }); }, }); diff --git a/server/startup/migrations/v189.js b/server/startup/migrations/v189.js deleted file mode 100644 index 9e90f77531d2..000000000000 --- a/server/startup/migrations/v189.js +++ /dev/null @@ -1,11 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; - -addMigration({ - version: 189, - up() { - Settings.remove({ _id: 'Livechat_Knowledge_Enabled' }); - Settings.remove({ _id: 'Livechat_Knowledge_Apiai_Key' }); - Settings.remove({ _id: 'Livechat_Knowledge_Apiai_Language' }); - }, -}); diff --git a/server/startup/migrations/v189.ts b/server/startup/migrations/v189.ts new file mode 100644 index 000000000000..310bad4bc224 --- /dev/null +++ b/server/startup/migrations/v189.ts @@ -0,0 +1,11 @@ +import { Settings } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 189, + async up() { + await Settings.removeById('Livechat_Knowledge_Enabled'); + await Settings.removeById('Livechat_Knowledge_Apiai_Key'); + await Settings.removeById('Livechat_Knowledge_Apiai_Language'); + }, +}); diff --git a/server/startup/migrations/v190.js b/server/startup/migrations/v190.js index a49a8e348283..52e07bc9ab62 100644 --- a/server/startup/migrations/v190.js +++ b/server/startup/migrations/v190.js @@ -5,7 +5,7 @@ addMigration({ version: 190, up() { // Remove unused settings - Promise.await(Settings.col.deleteOne({ _id: 'Accounts_Default_User_Preferences_desktopNotificationDuration' })); + Promise.await(Settings.removeById('Accounts_Default_User_Preferences_desktopNotificationDuration')); Promise.await(Subscriptions.col.updateMany({ desktopNotificationDuration: { $exists: true, diff --git a/server/startup/migrations/v191.js b/server/startup/migrations/v191.js deleted file mode 100644 index b2d7ed2b81df..000000000000 --- a/server/startup/migrations/v191.js +++ /dev/null @@ -1,9 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; - -addMigration({ - version: 191, - up() { - Settings.remove({ _id: /theme-color-status/ }, { multi: true }); - }, -}); diff --git a/server/startup/migrations/v191.ts b/server/startup/migrations/v191.ts new file mode 100644 index 000000000000..f30c1699899f --- /dev/null +++ b/server/startup/migrations/v191.ts @@ -0,0 +1,9 @@ +import { Settings } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 191, + async up() { + return Settings.deleteMany({ _id: /theme-color-status/ }); + }, +}); diff --git a/server/startup/migrations/v194.js b/server/startup/migrations/v194.js index 3129b37e5068..d7e20745beb8 100644 --- a/server/startup/migrations/v194.js +++ b/server/startup/migrations/v194.js @@ -1,12 +1,12 @@ import { - Settings, Users, } from '../../../app/models/server'; +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -function updateFieldMap() { +async function updateFieldMap() { const _id = 'SAML_Custom_Default_user_data_fieldmap'; - const setting = Settings.findOne({ _id }); + const setting = await Settings.findOne({ _id }); if (!setting || !setting.value) { return; } @@ -19,7 +19,7 @@ function updateFieldMap() { if (setting.value === '{"username":"username", "email":"email", "cn": "name"}') { // include de eppn identifier if it was used const value = `{"username":"username", "email":"email", "name": "cn"${ usedEppn ? ', "__identifier__": "eppn"' : '' }}`; - Settings.update({ _id }, { + await Settings.update({ _id }, { $set: { value, }, @@ -90,7 +90,7 @@ function updateIdentifierLocation() { function setOldLogoutResponseTemplate() { // For existing users, use a template compatible with the old SAML implementation instead of the default - Settings.upsert({ + return Settings.update({ _id: 'SAML_Custom_Default_LogoutResponse_template', }, { $set: { @@ -99,14 +99,16 @@ function setOldLogoutResponseTemplate() { `, }, + }, { + upsert: true, }); } addMigration({ version: 194, - up() { - updateFieldMap(); - updateIdentifierLocation(); - setOldLogoutResponseTemplate(); + async up() { + await updateFieldMap(); + await updateIdentifierLocation(); + await setOldLogoutResponseTemplate(); }, }); diff --git a/server/startup/migrations/v195.js b/server/startup/migrations/v195.ts similarity index 61% rename from server/startup/migrations/v195.js rename to server/startup/migrations/v195.ts index 5334f31bb2aa..89221f519bb4 100644 --- a/server/startup/migrations/v195.js +++ b/server/startup/migrations/v195.ts @@ -1,16 +1,14 @@ import moment from 'moment-timezone'; -import { ObjectId } from 'mongodb'; import { Mongo } from 'meteor/mongo'; import { addMigration } from '../../lib/migrations'; -import { Permissions, Settings } from '../../../app/models/server'; -import { LivechatBusinessHours } from '../../../app/models/server/raw'; -import { LivechatBusinessHourTypes } from '../../../definition/ILivechatBusinessHour'; +import { LivechatBusinessHours, Permissions, Settings } from '../../../app/models/server/raw'; +import { ILivechatBusinessHour, IBusinessHourWorkHour, LivechatBusinessHourTypes } from '../../../definition/ILivechatBusinessHour'; -const migrateCollection = () => { - const LivechatOfficeHour = new Mongo.Collection('rocketchat_livechat_office_hour'); +const migrateCollection = async (): Promise => { + const LivechatOfficeHour = new Mongo.Collection('rocketchat_livechat_office_hour'); const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; - const officeHours = []; + const officeHours: IBusinessHourWorkHour[] = []; days.forEach((day) => { const officeHour = LivechatOfficeHour.findOne({ day }); if (officeHour) { @@ -22,15 +20,15 @@ const migrateCollection = () => { return; } - const businessHour = { + const businessHour: Omit = { name: '', active: true, type: LivechatBusinessHourTypes.DEFAULT, ts: new Date(), - workHours: officeHours.map((officeHour) => ({ + workHours: officeHours.map((officeHour): IBusinessHourWorkHour => ({ day: officeHour.day, start: { - time: officeHour.start, + time: officeHour.start as any, utc: { dayOfWeek: moment(`${ officeHour.day }:${ officeHour.start }`, 'dddd:HH:mm').utc().format('dddd'), time: moment(`${ officeHour.day }:${ officeHour.start }`, 'dddd:HH:mm').utc().format('HH:mm'), @@ -41,7 +39,7 @@ const migrateCollection = () => { }, }, finish: { - time: officeHour.finish, + time: officeHour.finish as any, utc: { dayOfWeek: moment(`${ officeHour.day }:${ officeHour.finish }`, 'dddd:HH:mm').utc().format('dddd'), time: moment(`${ officeHour.day }:${ officeHour.finish }`, 'dddd:HH:mm').utc().format('HH:mm'), @@ -56,11 +54,10 @@ const migrateCollection = () => { })), timezone: { name: moment.tz.guess(), - utc: moment().utcOffset() / 60, + utc: String(moment().utcOffset() / 60), }, }; - if (LivechatBusinessHours.find({ type: LivechatBusinessHourTypes.DEFAULT }).count() === 0) { - businessHour._id = new ObjectId().toHexString(); + if (await LivechatBusinessHours.find({ type: LivechatBusinessHourTypes.DEFAULT }).count() === 0) { LivechatBusinessHours.insertOne(businessHour); } else { LivechatBusinessHours.update({ type: LivechatBusinessHourTypes.DEFAULT }, { $set: { ...businessHour } }); @@ -77,14 +74,14 @@ const migrateCollection = () => { addMigration({ version: 195, - up() { - Settings.remove({ _id: 'Livechat_enable_office_hours' }); - Settings.remove({ _id: 'Livechat_allow_online_agents_outside_office_hours' }); - const permission = Permissions.findOneById('view-livechat-officeHours'); + async up() { + await Settings.removeById('Livechat_enable_office_hours'); + await Settings.removeById('Livechat_allow_online_agents_outside_office_hours'); + const permission = await Permissions.findOneById('view-livechat-officeHours'); if (permission) { - Permissions.upsert({ _id: 'view-livechat-business-hours' }, { $set: { roles: permission.roles } }); - Permissions.remove({ _id: 'view-livechat-officeHours' }); + await Permissions.update({ _id: 'view-livechat-business-hours' }, { $set: { roles: permission.roles } }, { upsert: true }); + await Permissions.deleteOne({ _id: 'view-livechat-officeHours' }); } - Promise.await(migrateCollection()); + await migrateCollection(); }, }); diff --git a/server/startup/migrations/v198.js b/server/startup/migrations/v198.ts similarity index 54% rename from server/startup/migrations/v198.js rename to server/startup/migrations/v198.ts index e2e2bf16fdb5..1a979fe3e948 100644 --- a/server/startup/migrations/v198.js +++ b/server/startup/migrations/v198.ts @@ -1,44 +1,50 @@ -import { Settings } from '../../../app/models/server'; +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; addMigration({ version: 198, - up: () => { - const discussion = Settings.findOneById('RetentionPolicy_DoNotExcludeDiscussion'); - const thread = Settings.findOneById('RetentionPolicy_DoNotExcludeThreads'); - const pinned = Settings.findOneById('RetentionPolicy_ExcludePinned'); + up: async () => { + const discussion = await Settings.findOneById('RetentionPolicy_DoNotExcludeDiscussion'); + const thread = await Settings.findOneById('RetentionPolicy_DoNotExcludeThreads'); + const pinned = await Settings.findOneById('RetentionPolicy_ExcludePinned'); if (discussion) { - Settings.upsert({ + await Settings.update({ _id: 'RetentionPolicy_DoNotPruneDiscussion', }, { $set: { value: discussion.value, }, + }, { + upsert: true, }); } if (thread) { - Settings.upsert({ + await Settings.update({ _id: 'RetentionPolicy_DoNotPruneThreads', }, { $set: { value: thread.value, }, + }, { + upsert: true, }); } if (pinned) { - Settings.upsert({ + await Settings.update({ _id: 'RetentionPolicy_DoNotPrunePinned', }, { $set: { value: pinned.value, }, + }, { + upsert: true, }); } - Settings.remove({ + return Settings.deleteMany({ _id: { $in: ['RetentionPolicy_DoNotExcludeDiscussion', 'RetentionPolicy_DoNotExcludeThreads', 'RetentionPolicy_ExcludePinned'] }, }); }, diff --git a/server/startup/migrations/v201.js b/server/startup/migrations/v201.js index 0fb5ae254412..0a74087cf773 100644 --- a/server/startup/migrations/v201.js +++ b/server/startup/migrations/v201.js @@ -1,17 +1,17 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { Settings } from '../../../app/models/server'; +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; import { sendMessagesToAdmins } from '../../lib/sendMessagesToAdmins'; addMigration({ version: 201, - up: () => { - const pushEnabled = Settings.findOneById('Push_enable'); - const pushGatewayEnabled = Settings.findOneById('Push_enable_gateway'); - const registerServer = Settings.findOneById('Register_Server'); - const cloudAgreement = Settings.findOneById('Cloud_Service_Agree_PrivacyTerms'); + up: async () => { + const pushEnabled = await Settings.findOneById('Push_enable'); + const pushGatewayEnabled = await Settings.findOneById('Push_enable_gateway'); + const registerServer = await Settings.findOneById('Register_Server'); + const cloudAgreement = await Settings.findOneById('Cloud_Service_Agree_PrivacyTerms'); if (!pushEnabled?.value) { return; @@ -24,12 +24,14 @@ addMigration({ } // if push gateway is enabled but server is not registered or cloud terms not agreed, disable gateway and alert admin - Settings.upsert({ + Settings.update({ _id: 'Push_enable_gateway', }, { $set: { value: false, }, + }, { + update: true, }); const id = 'push-gateway-disabled'; diff --git a/server/startup/migrations/v202.js b/server/startup/migrations/v202.js index 02362a2d165e..7cb0e457f2cf 100644 --- a/server/startup/migrations/v202.js +++ b/server/startup/migrations/v202.js @@ -1,15 +1,15 @@ import { addMigration } from '../../lib/migrations'; -import Uploads from '../../../app/models/server/models/Uploads'; +import { Uploads } from '../../../app/models/server/raw'; addMigration({ version: 202, - up() { - Promise.await(Uploads.model.rawCollection().updateMany({ + async up() { + await Uploads.updateMany({ type: 'audio/mp3', }, { $set: { type: 'audio/mpeg', }, - })); + }); }, }); diff --git a/server/startup/migrations/v203.js b/server/startup/migrations/v203.js deleted file mode 100644 index ed70284f278d..000000000000 --- a/server/startup/migrations/v203.js +++ /dev/null @@ -1,10 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Avatars } from '../../../app/models/server'; - -addMigration({ - version: 203, - up() { - Avatars.tryDropIndex({ name: 1 }); - Avatars.tryEnsureIndex({ name: 1 }, { sparse: true }); - }, -}); diff --git a/server/startup/migrations/v203.ts b/server/startup/migrations/v203.ts new file mode 100644 index 000000000000..d5594ece72bb --- /dev/null +++ b/server/startup/migrations/v203.ts @@ -0,0 +1,10 @@ +import { addMigration } from '../../lib/migrations'; +import { Avatars } from '../../../app/models/server/raw'; + +addMigration({ + version: 203, + async up() { + await Avatars.col.dropIndex('name_1'); + await Avatars.col.createIndex({ name: 1 }, { sparse: true }); + }, +}); diff --git a/server/startup/migrations/v205.js b/server/startup/migrations/v205.js index 061075e17ece..2feed878dcbc 100644 --- a/server/startup/migrations/v205.js +++ b/server/startup/migrations/v205.js @@ -1,16 +1,18 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 205, up() { // Disable this new enforcement setting for existent installations. - Settings.upsert({ + Settings.update({ _id: 'Accounts_TwoFactorAuthentication_Enforce_Password_Fallback', }, { $set: { value: false, }, + }, { + upsert: true, }); }, }); diff --git a/server/startup/migrations/v207.js b/server/startup/migrations/v207.js deleted file mode 100644 index 556f66394632..000000000000 --- a/server/startup/migrations/v207.js +++ /dev/null @@ -1,9 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models'; - -addMigration({ - version: 207, - up() { - Settings.removeById('theme-color-tertiary-background-color'); - }, -}); diff --git a/server/startup/migrations/v207.ts b/server/startup/migrations/v207.ts new file mode 100644 index 000000000000..cac3bd875c1c --- /dev/null +++ b/server/startup/migrations/v207.ts @@ -0,0 +1,10 @@ +import { Settings } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + + +addMigration({ + version: 207, + up() { + return Settings.removeById('theme-color-tertiary-background-color'); + }, +}); diff --git a/server/startup/migrations/v208.js b/server/startup/migrations/v208.js index 9a604c6e4a88..ced4c5f2d5b3 100644 --- a/server/startup/migrations/v208.js +++ b/server/startup/migrations/v208.js @@ -1,41 +1,31 @@ -import Future from 'fibers/future'; - import { addMigration } from '../../lib/migrations'; import { Users, Sessions } from '../../../app/models/server/raw'; -async function migrateSessions(fut) { +async function migrateSessions() { const cursor = Users.find({ roles: 'anonymous' }, { projection: { _id: 1 } }); if (!cursor) { return; } - const users = await cursor.toArray(); if (users.length === 0) { - fut.return(); return; } const userIds = users.map(({ _id }) => _id); - Sessions.update({ + await Sessions.updateMany({ userId: { $in: userIds }, }, { $set: { roles: ['anonymous'], }, - }, { - multi: true, }); - - fut.return(); } addMigration({ version: 208, up() { - const fut = new Future(); - migrateSessions(fut); - fut.wait(); + Promise.await(migrateSessions()); }, }); diff --git a/server/startup/migrations/v211.js b/server/startup/migrations/v211.js index b79d155aa18f..d5ac34be0f7b 100644 --- a/server/startup/migrations/v211.js +++ b/server/startup/migrations/v211.js @@ -1,5 +1,3 @@ -import { Promise } from 'meteor/promise'; - import { addMigration } from '../../lib/migrations'; import { Sessions } from '../../../app/models/server/raw'; import { getMostImportantRole } from '../../../app/statistics/server/lib/getMostImportantRole'; diff --git a/server/startup/migrations/v214.js b/server/startup/migrations/v214.js deleted file mode 100644 index 787b8acbeb74..000000000000 --- a/server/startup/migrations/v214.js +++ /dev/null @@ -1,11 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Permissions } from '../../../app/models/server'; - -const roleName = 'admin'; - -addMigration({ - version: 214, - up() { - Permissions.update({ _id: 'toggle-room-e2e-encryption' }, { $addToSet: { roles: roleName } }); - }, -}); diff --git a/server/startup/migrations/v214.ts b/server/startup/migrations/v214.ts new file mode 100644 index 000000000000..ec0b5491e433 --- /dev/null +++ b/server/startup/migrations/v214.ts @@ -0,0 +1,12 @@ +import { Permissions } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + + +const roleName = 'admin'; + +addMigration({ + version: 214, + up() { + return Permissions.update({ _id: 'toggle-room-e2e-encryption' }, { $addToSet: { roles: roleName } }); + }, +}); diff --git a/server/startup/migrations/v215.js b/server/startup/migrations/v215.ts similarity index 52% rename from server/startup/migrations/v215.js rename to server/startup/migrations/v215.ts index e3d71a070a72..bd6f08358254 100644 --- a/server/startup/migrations/v215.js +++ b/server/startup/migrations/v215.ts @@ -1,14 +1,14 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; const removed = ['advocacy', 'industry', 'publicRelations', 'healthcarePharmaceutical', 'helpCenter']; addMigration({ version: 215, - up() { - const current = Settings.findOneById('Industry'); - if (removed.includes(current.value)) { - Settings.update({ + async up() { + const current = await Settings.findOneById('Industry'); + if (current && typeof current.value === 'string' && removed.includes(current.value)) { + await Settings.update({ _id: 'Industry', }, { $set: { diff --git a/server/startup/migrations/v216.js b/server/startup/migrations/v216.js index d004e5a32c7e..4a3225d6b251 100644 --- a/server/startup/migrations/v216.js +++ b/server/startup/migrations/v216.js @@ -1,42 +1,43 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models'; addMigration({ version: 216, - up() { - Settings.find({ _id: /Accounts_OAuth_Custom/, i18nLabel: 'Accounts_OAuth_Custom_Enable' }).forEach(function(customOauth) { + async up() { + return Promise.all((await Settings.find({ _id: /Accounts_OAuth_Custom/, i18nLabel: 'Accounts_OAuth_Custom_Enable' }).toArray()).map(async function(customOauth) { const parts = customOauth._id.split('-'); const name = parts[1]; const id = `Accounts_OAuth_Custom-${ name }-key_field`; - if (!Settings.findOne({ _id: id })) { - Settings.insert({ - _id: id, - type: 'select', - group: 'OAuth', - section: `Custom OAuth: ${ name }`, - i18nLabel: 'Accounts_OAuth_Custom_Key_Field', - persistent: true, - values: [ - { - key: 'username', - i18nLabel: 'Username', - }, - { - key: 'email', - i18nLabel: 'Email', - }, - ], - packageValue: 'username', - valueSource: 'packageValue', - ts: new Date(), - hidden: false, - blocked: false, - sorter: 103, - i18nDescription: `Accounts_OAuth_Custom-${ name }-key_field_Description`, - createdAt: new Date(), - value: 'username', - }); + if (await Settings.findOne({ _id: id })) { + return; } - }); + return Settings.insert({ + _id: id, + type: 'select', + group: 'OAuth', + section: `Custom OAuth: ${ name }`, + i18nLabel: 'Accounts_OAuth_Custom_Key_Field', + persistent: true, + values: [ + { + key: 'username', + i18nLabel: 'Username', + }, + { + key: 'email', + i18nLabel: 'Email', + }, + ], + packageValue: 'username', + valueSource: 'packageValue', + ts: new Date(), + hidden: false, + blocked: false, + sorter: 103, + i18nDescription: `Accounts_OAuth_Custom-${ name }-key_field_Description`, + createdAt: new Date(), + value: 'username', + }); + })); }, }); diff --git a/server/startup/migrations/v217.js b/server/startup/migrations/v217.js deleted file mode 100644 index 3c82aeb5931c..000000000000 --- a/server/startup/migrations/v217.js +++ /dev/null @@ -1,12 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Permissions } from '../../../app/models'; - -addMigration({ - version: 217, - up() { - const oldPermission = Permissions.findOne('view-livechat-queue'); - if (oldPermission) { - Permissions.update({ _id: 'view-livechat-queue' }, { $addToSet: { roles: 'livechat-agent' } }); - } - }, -}); diff --git a/server/startup/migrations/v217.ts b/server/startup/migrations/v217.ts new file mode 100644 index 000000000000..3ca79fccc2d5 --- /dev/null +++ b/server/startup/migrations/v217.ts @@ -0,0 +1,13 @@ +import { Permissions } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + + +addMigration({ + version: 217, + async up() { + const oldPermission = await Permissions.findOne('view-livechat-queue'); + if (oldPermission) { + return Permissions.update({ _id: 'view-livechat-queue' }, { $addToSet: { roles: 'livechat-agent' } }); + } + }, +}); diff --git a/server/startup/migrations/v218.js b/server/startup/migrations/v218.js deleted file mode 100644 index 01b2668a7cda..000000000000 --- a/server/startup/migrations/v218.js +++ /dev/null @@ -1,9 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Statistics } from '../../../app/models/server'; - -addMigration({ - version: 218, - up() { - Statistics.tryDropIndex({ createdAt: 1 }); - }, -}); diff --git a/server/startup/migrations/v218.ts b/server/startup/migrations/v218.ts new file mode 100644 index 000000000000..c17991620cb2 --- /dev/null +++ b/server/startup/migrations/v218.ts @@ -0,0 +1,10 @@ +import { addMigration } from '../../lib/migrations'; +import { Statistics } from '../../../app/models/server/raw'; + +addMigration({ + version: 218, + up() { + // TODO test if dropIndex do not raise exceptions. + Statistics.col.dropIndex('createdAt_1'); + }, +}); diff --git a/server/startup/migrations/v219.js b/server/startup/migrations/v219.ts similarity index 70% rename from server/startup/migrations/v219.js rename to server/startup/migrations/v219.ts index bd16d7c18b89..cd2d5eacc06f 100644 --- a/server/startup/migrations/v219.js +++ b/server/startup/migrations/v219.ts @@ -1,16 +1,17 @@ + +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 219, - up() { + async up() { const SettingIds = { old: 'Livechat_auto_close_abandoned_rooms', new: 'Livechat_abandoned_rooms_action', }; - const oldSetting = Settings.findOne({ _id: SettingIds.old }); + const oldSetting = await Settings.findOne({ _id: SettingIds.old }); if (!oldSetting) { return; } @@ -27,8 +28,6 @@ addMigration({ }, }); - Settings.remove({ - _id: SettingIds.old, - }); + return Settings.removeById(SettingIds.old); }, }); diff --git a/server/startup/migrations/v220.js b/server/startup/migrations/v220.ts similarity index 99% rename from server/startup/migrations/v220.js rename to server/startup/migrations/v220.ts index 59795ce4eff9..cdaa4e6c07e6 100644 --- a/server/startup/migrations/v220.js +++ b/server/startup/migrations/v220.ts @@ -1,10 +1,10 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 220, - up() { - Settings.update({ + async up() { + await Settings.update({ _id: 'Organization_Type', }, { $set: { @@ -29,7 +29,7 @@ addMigration({ }, }); - Settings.update({ + await Settings.update({ _id: 'Industry', }, { $set: { @@ -142,7 +142,7 @@ addMigration({ }, }); - Settings.update({ + await Settings.update({ _id: 'Country', }, { values: [ diff --git a/server/startup/migrations/v223.js b/server/startup/migrations/v223.js deleted file mode 100644 index 60c60180ea70..000000000000 --- a/server/startup/migrations/v223.js +++ /dev/null @@ -1,11 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Permissions } from '../../../app/models/server'; - -const roleName = 'user'; - -addMigration({ - version: 223, - up() { - Permissions.update({ _id: 'message-impersonate' }, { $addToSet: { roles: roleName } }); - }, -}); diff --git a/server/startup/migrations/v223.ts b/server/startup/migrations/v223.ts new file mode 100644 index 000000000000..ef357c554277 --- /dev/null +++ b/server/startup/migrations/v223.ts @@ -0,0 +1,11 @@ +import { Permissions } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +const roleName = 'user'; + +addMigration({ + version: 223, + up() { + return Permissions.update({ _id: 'message-impersonate' }, { $addToSet: { roles: roleName } }); + }, +}); diff --git a/server/startup/migrations/v224.js b/server/startup/migrations/v224.js deleted file mode 100644 index 5e69367f5609..000000000000 --- a/server/startup/migrations/v224.js +++ /dev/null @@ -1,11 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Permissions } from '../../../app/models/server'; - -const roleName = 'app'; - -addMigration({ - version: 224, - up() { - Permissions.update({ _id: 'message-impersonate' }, { $addToSet: { roles: roleName } }); - }, -}); diff --git a/server/startup/migrations/v224.ts b/server/startup/migrations/v224.ts new file mode 100644 index 000000000000..be266c225bd0 --- /dev/null +++ b/server/startup/migrations/v224.ts @@ -0,0 +1,9 @@ +import { Permissions } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 224, + up() { + return Permissions.update({ _id: 'message-impersonate' }, { $addToSet: { roles: 'app' } }); + }, +}); diff --git a/server/startup/migrations/v225.js b/server/startup/migrations/v225.ts similarity index 70% rename from server/startup/migrations/v225.js rename to server/startup/migrations/v225.ts index bee7349b8b64..62fabfcde620 100644 --- a/server/startup/migrations/v225.js +++ b/server/startup/migrations/v225.ts @@ -1,30 +1,35 @@ import { addMigration } from '../../lib/migrations'; -import { Settings, Users } from '../../../app/models/server'; +import { Users } from '../../../app/models/server'; +import { Settings } from '../../../app/models/server/raw'; addMigration({ version: 225, - up() { - const hideAvatarsSetting = Settings.findOneById('Accounts_Default_User_Preferences_hideAvatars'); - const hideAvatarsSidebarSetting = Settings.findOneById('Accounts_Default_User_Preferences_sidebarHideAvatar'); + async up() { + const hideAvatarsSetting = await Settings.findOneById('Accounts_Default_User_Preferences_hideAvatars'); + const hideAvatarsSidebarSetting = await Settings.findOneById('Accounts_Default_User_Preferences_sidebarHideAvatar'); Settings.removeById('Accounts_Default_User_Preferences_sidebarShowDiscussion'); Settings.removeById('Accounts_Default_User_Preferences_sidebarHideAvatar'); - Settings.upsert({ + Settings.update({ _id: 'Accounts_Default_User_Preferences_sidebarDisplayAvatar', }, { $set: { - value: !hideAvatarsSidebarSetting.value, + value: !hideAvatarsSidebarSetting?.value, }, + }, { + upsert: true, }); - Settings.removeById('Accounts_Default_User_Preferences_hideAvatars'); - Settings.upsert({ + await Settings.removeById('Accounts_Default_User_Preferences_hideAvatars'); + Settings.update({ _id: 'Accounts_Default_User_Preferences_displayAvatars', }, { $set: { - value: !hideAvatarsSetting.value, + value: !hideAvatarsSetting?.value, }, + }, { + upsert: true, }); Users.update({ 'settings.preferences.hideAvatars': true }, { diff --git a/server/startup/migrations/v226.js b/server/startup/migrations/v226.js deleted file mode 100644 index da53274e5ec4..000000000000 --- a/server/startup/migrations/v226.js +++ /dev/null @@ -1,9 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; - -addMigration({ - version: 226, - up() { - Settings.removeById('Apps_Game_Center_enabled'); - }, -}); diff --git a/server/startup/migrations/v226.ts b/server/startup/migrations/v226.ts new file mode 100644 index 000000000000..c94cc0b0d1e8 --- /dev/null +++ b/server/startup/migrations/v226.ts @@ -0,0 +1,9 @@ +import { Settings } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 226, + up() { + return Settings.removeById('Apps_Game_Center_enabled'); + }, +}); diff --git a/server/startup/migrations/v228.js b/server/startup/migrations/v228.js deleted file mode 100644 index 9ac69ef146cc..000000000000 --- a/server/startup/migrations/v228.js +++ /dev/null @@ -1,11 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Permissions } from '../../../app/models'; - -addMigration({ - version: 228, - up() { - if (Permissions) { - Permissions.update({ _id: 'manage-livechat-canned-responses' }, { $addToSet: { roles: 'livechat-monitor' } }); - } - }, -}); diff --git a/server/startup/migrations/v228.ts b/server/startup/migrations/v228.ts new file mode 100644 index 000000000000..4d49253980f3 --- /dev/null +++ b/server/startup/migrations/v228.ts @@ -0,0 +1,9 @@ +import { Permissions } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 228, + up() { + return Permissions.update({ _id: 'manage-livechat-canned-responses' }, { $addToSet: { roles: 'livechat-monitor' } }); + }, +}); diff --git a/server/startup/migrations/v229.js b/server/startup/migrations/v229.ts similarity index 62% rename from server/startup/migrations/v229.js rename to server/startup/migrations/v229.ts index 87c38d2a68f6..71c00c907909 100644 --- a/server/startup/migrations/v229.js +++ b/server/startup/migrations/v229.ts @@ -1,15 +1,15 @@ -import { Settings } from '../../../app/models/server'; +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; addMigration({ version: 229, - up() { - const oldNamesValidationSetting = Settings.findOneById( + async up() { + const oldNamesValidationSetting = await Settings.findOneById( 'UTF8_Names_Validation', ); const oldNamesValidationSettingValue = oldNamesValidationSetting?.value || '[0-9a-zA-Z-_.]+'; - Settings.upsert( + Settings.update( { _id: 'UTF8_User_Names_Validation', }, @@ -18,9 +18,12 @@ addMigration({ value: oldNamesValidationSettingValue, }, }, + { + upsert: true, + }, ); - Settings.upsert( + Settings.update( { _id: 'UTF8_Channel_Names_Validation', }, @@ -28,9 +31,11 @@ addMigration({ $set: { value: oldNamesValidationSettingValue, }, + }, { + upsert: true, }, ); - Settings.removeById('UTF8_Names_Validation'); + return Settings.removeById('UTF8_Names_Validation'); }, }); diff --git a/server/startup/migrations/v230.ts b/server/startup/migrations/v230.ts index 22c48c7762ff..c3103291bd00 100644 --- a/server/startup/migrations/v230.ts +++ b/server/startup/migrations/v230.ts @@ -1,12 +1,12 @@ +import { Permissions } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Permissions } from '../../../app/models/server'; - -const roleName = 'app'; addMigration({ version: 230, up() { - Permissions.update({ _id: 'start-discussion' }, { $addToSet: { roles: roleName } }); - Permissions.update({ _id: 'start-discussion-other-user' }, { $addToSet: { roles: roleName } }); + return Promise.all([ + Permissions.addRole('start-discussion', 'app'), + Permissions.addRole('start-discussion-other-user', 'app'), + ]); }, }); diff --git a/server/startup/migrations/v231.ts b/server/startup/migrations/v231.ts index 67a897b6e3f4..70dbd39bd854 100644 --- a/server/startup/migrations/v231.ts +++ b/server/startup/migrations/v231.ts @@ -5,12 +5,12 @@ import { addMigration } from '../../lib/migrations'; import { BannerPlatform } from '../../../definition/IBanner'; import { Banner } from '../../sdk'; import { settings } from '../../../app/settings/server'; -import { Settings } from '../../../app/models/server'; import { isEnterprise } from '../../../ee/app/license/server'; +import { Settings } from '../../../app/models/server/raw'; addMigration({ version: 231, - up() { + async up() { const LDAPEnabled = settings.get('LDAP_Enable'); const SAMLEnabled = settings.get('SAML_Custom_Default'); @@ -18,7 +18,7 @@ addMigration({ _id: { $in: [/^Accounts_OAuth_(Custom-)?([^-_]+)$/, 'Accounts_OAuth_GitHub_Enterprise'] }, value: true, }; - const CustomOauthEnabled = !!Settings.findOne(query); + const CustomOauthEnabled = !! await Settings.findOne(query); const isAuthServiceEnabled = LDAPEnabled || SAMLEnabled || CustomOauthEnabled; diff --git a/server/startup/migrations/v233.ts b/server/startup/migrations/v233.ts index 9d9068a588d8..bf51873d9e19 100644 --- a/server/startup/migrations/v233.ts +++ b/server/startup/migrations/v233.ts @@ -1,10 +1,10 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 233, up() { - Settings.remove({ _id: { $in: [ + return Settings.deleteMany({ _id: { $in: [ 'Log_Package', 'Log_File', ] } }); diff --git a/server/startup/migrations/v234.ts b/server/startup/migrations/v234.ts index 4d8dbe2db412..8909a16e21c1 100644 --- a/server/startup/migrations/v234.ts +++ b/server/startup/migrations/v234.ts @@ -1,10 +1,10 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 234, up() { - Settings.remove({ + return Settings.deleteMany({ _id: { $in: [ 'GoogleVision_Enable', diff --git a/server/startup/migrations/v235.ts b/server/startup/migrations/v235.ts index fe2308356e50..1b6fb0af8e1a 100644 --- a/server/startup/migrations/v235.ts +++ b/server/startup/migrations/v235.ts @@ -1,10 +1,11 @@ import { addMigration } from '../../lib/migrations'; -import { Settings, Subscriptions, Users } from '../../../app/models/server'; +import { Subscriptions, Users } from '../../../app/models/server'; +import { Settings } from '../../../app/models/server/raw'; addMigration({ version: 235, - up() { - Settings.removeById('Accounts_Default_User_Preferences_audioNotifications'); + async up() { + await Settings.removeById('Accounts_Default_User_Preferences_audioNotifications'); // delete field from subscriptions Subscriptions.update({ diff --git a/server/startup/migrations/v236.ts b/server/startup/migrations/v236.ts index c1f839896400..2f0ea88c17f2 100644 --- a/server/startup/migrations/v236.ts +++ b/server/startup/migrations/v236.ts @@ -1,13 +1,13 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 236, - up() { - Settings.removeById('Canned Responses'); - Settings.removeById('Canned_Responses'); + async up() { + await Settings.removeById('Canned Responses'); + await Settings.removeById('Canned_Responses'); - Settings.upsert( + await Settings.update( { _id: 'Canned_Responses_Enable', }, @@ -16,6 +16,9 @@ addMigration({ group: 'Omnichannel', }, }, + { + upsert: true, + }, ); }, }); diff --git a/server/startup/migrations/v237.ts b/server/startup/migrations/v237.ts index f53d8d0eb195..155ae0b29a3e 100644 --- a/server/startup/migrations/v237.ts +++ b/server/startup/migrations/v237.ts @@ -1,7 +1,7 @@ import { addMigration } from '../../lib/migrations'; import { settings } from '../../../app/settings/server'; -import { Settings } from '../../../app/models/server'; import { isEnterprise } from '../../../ee/app/license/server'; +import { Settings } from '../../../app/models/server/raw'; function copySettingValue(newName: string, oldName: string): void { const value = settings.get(oldName); @@ -9,12 +9,12 @@ function copySettingValue(newName: string, oldName: string): void { return; } - Settings.upsert({ _id: newName }, { $set: { value } }); + Settings.update({ _id: newName }, { $set: { value } }, { upsert: true }); } addMigration({ version: 237, - up() { + async up() { const isEE = isEnterprise(); // Override AD defaults with the previously configured values @@ -23,7 +23,9 @@ addMigration({ // If we're sure the server is AD, then select it - otherwise keep it as generic ldap const useAdDefaults = settings.get('LDAP_User_Search_Field') === 'sAMAccountName'; - Settings.upsert({ _id: 'LDAP_Server_Type' }, { $set: { value: useAdDefaults ? 'ad' : '' } }); + Settings.update({ _id: 'LDAP_Server_Type' }, { $set: { value: useAdDefaults ? 'ad' : '' } }, { + upsert: true, + }); // The setting to use the field map also determined if the user data was updated on login or not copySettingValue('LDAP_Update_Data_On_Login', 'LDAP_Sync_User_Data'); @@ -44,14 +46,14 @@ addMigration({ } if (fieldMap[key] === 'name') { - Settings.upsert({ _id: 'LDAP_Name_Field' }, { $set: { value: key } }); - Settings.upsert({ _id: 'LDAP_AD_Name_Field' }, { $set: { value: key } }); + Settings.update({ _id: 'LDAP_Name_Field' }, { $set: { value: key } }, { upsert: true }); + Settings.update({ _id: 'LDAP_AD_Name_Field' }, { $set: { value: key } }, { upsert: true }); continue; } if (fieldMap[key] === 'email') { - Settings.upsert({ _id: 'LDAP_Email_Field' }, { $set: { value: key } }); - Settings.upsert({ _id: 'LDAP_AD_Email_Field' }, { $set: { value: key } }); + Settings.update({ _id: 'LDAP_Email_Field' }, { $set: { value: key } }, { upsert: true }); + Settings.update({ _id: 'LDAP_AD_Email_Field' }, { $set: { value: key } }, { upsert: true }); continue; } @@ -60,10 +62,10 @@ addMigration({ if (isEE) { const newJson = JSON.stringify(newObject); - Settings.upsert({ _id: 'LDAP_CustomFieldMap' }, { $set: { value: newJson } }); + Settings.update({ _id: 'LDAP_CustomFieldMap' }, { $set: { value: newJson } }, { upsert: true }); const syncCustomFields = Object.keys(newObject).length > 0 && settings.get('LDAP_Sync_User_Data'); - Settings.upsert({ _id: 'LDAP_Sync_Custom_Fields' }, { $set: { value: syncCustomFields } }); + Settings.update({ _id: 'LDAP_Sync_Custom_Fields' }, { $set: { value: syncCustomFields } }, { upsert: true }); } } @@ -80,7 +82,7 @@ addMigration({ copySettingValue('LDAP_Sync_User_Data_Channels_Filter', 'LDAP_Sync_User_Data_Groups_Filter'); copySettingValue('LDAP_Sync_User_Data_Channels_BaseDN', 'LDAP_Sync_User_Data_Groups_BaseDN'); - Settings.remove({ + await Settings.deleteMany({ _id: { $in: [ 'LDAP_Sync_Now', diff --git a/server/startup/migrations/v240.ts b/server/startup/migrations/v240.ts index 4ce8f2c7d0ac..296040d649d7 100644 --- a/server/startup/migrations/v240.ts +++ b/server/startup/migrations/v240.ts @@ -1,9 +1,9 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 240, up() { - Settings.removeById('Support_Cordova_App'); + return Settings.removeById('Support_Cordova_App'); }, }); diff --git a/server/startup/migrations/v242.ts b/server/startup/migrations/v242.ts index 86419e276328..b94f3f6439d5 100644 --- a/server/startup/migrations/v242.ts +++ b/server/startup/migrations/v242.ts @@ -1,5 +1,6 @@ import { addMigration } from '../../lib/migrations'; -import { LivechatInquiry, Settings } from '../../../app/models/server'; +import { LivechatInquiry } from '../../../app/models/server'; +import { Settings } from '../../../app/models/server/raw'; function removeQueueTimeoutFromInquiries(): void { LivechatInquiry.update({ @@ -8,23 +9,23 @@ function removeQueueTimeoutFromInquiries(): void { }, { $unset: { estimatedInactivityCloseTimeAt: 1 } }, { multi: true }); } -function removeSetting(): void { - const oldSetting = Settings.findOneById('Livechat_max_queue_wait_time_action'); +async function removeSetting(): Promise { + const oldSetting = await Settings.findOneById('Livechat_max_queue_wait_time_action'); if (!oldSetting) { return; } const currentAction = oldSetting.value; if (currentAction === 'Nothing') { - Settings.upsert({ _id: 'Livechat_max_queue_wait_time' }, { $set: { value: -1 } }); + await Settings.update({ _id: 'Livechat_max_queue_wait_time' }, { $set: { value: -1 } }, { upsert: true }); } - Settings.removeById('Livechat_max_queue_wait_time_action'); + await Settings.removeById('Livechat_max_queue_wait_time_action'); } addMigration({ version: 242, up() { removeQueueTimeoutFromInquiries(); - removeSetting(); + return removeSetting(); }, }); diff --git a/server/startup/migrations/v243.ts b/server/startup/migrations/v243.ts index 89d3f9f03e70..71a2e7d97c75 100644 --- a/server/startup/migrations/v243.ts +++ b/server/startup/migrations/v243.ts @@ -1,19 +1,22 @@ import { addMigration } from '../../lib/migrations'; -import { Settings, Users } from '../../../app/models/server'; +import { Users } from '../../../app/models/server'; +import { Settings } from '../../../app/models/server/raw'; addMigration({ version: 243, - up() { - const mobileNotificationsSetting = Settings.findOneById('Accounts_Default_User_Preferences_mobileNotifications'); + async up() { + const mobileNotificationsSetting = await Settings.findOneById('Accounts_Default_User_Preferences_mobileNotifications'); - Settings.removeById('Accounts_Default_User_Preferences_mobileNotifications'); + await Settings.removeById('Accounts_Default_User_Preferences_mobileNotifications'); if (mobileNotificationsSetting && mobileNotificationsSetting.value) { - Settings.upsert({ + Settings.update({ _id: 'Accounts_Default_User_Preferences_pushNotifications', }, { $set: { value: mobileNotificationsSetting.value, }, + }, { + upsert: true, }); } diff --git a/server/startup/migrations/v244.ts b/server/startup/migrations/v244.ts new file mode 100644 index 000000000000..d5c39dffc20c --- /dev/null +++ b/server/startup/migrations/v244.ts @@ -0,0 +1,9 @@ +import { upsertPermissions } from '../../../app/authorization/server/functions/upsertPermissions'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 244, + up() { + return upsertPermissions(); + }, +}); diff --git a/server/startup/migrations/v245.ts b/server/startup/migrations/v245.ts new file mode 100644 index 000000000000..a3efd7bcc279 --- /dev/null +++ b/server/startup/migrations/v245.ts @@ -0,0 +1,10 @@ +import { addMigration } from '../../lib/migrations'; +import { Permissions } from '../../../app/models/server/raw'; + +addMigration({ + version: 245, + up() { + Permissions.create('mobile-download-file', ['user', 'admin']); + Permissions.create('mobile-upload-file', ['user', 'admin']); + }, +}); diff --git a/server/startup/migrations/v246.ts b/server/startup/migrations/v246.ts new file mode 100644 index 000000000000..103ab198f226 --- /dev/null +++ b/server/startup/migrations/v246.ts @@ -0,0 +1,25 @@ +import { addMigration } from '../../lib/migrations'; +import { Settings } from '../../../app/models/server'; +import { settings } from '../../../app/settings/server'; + +addMigration({ + version: 246, + up() { + const livechatVideoCallEnabled = settings.get('Livechat_videocall_enabled'); + if (livechatVideoCallEnabled) { + Settings.upsert({ _id: 'Omnichannel_call_provider' }, { + $set: { value: 'Jitsi' }, + }); + } + Settings.removeById('Livechat_videocall_enabled'); + + const webRTCEnableChannel = settings.get('WebRTC_Enable_Channel'); + const webRTCEnableDirect = settings.get('WebRTC_Enable_Direct'); + const webRTCEnablePrivate = settings.get('WebRTC_Enable_Private'); + if (webRTCEnableChannel || webRTCEnableDirect || webRTCEnablePrivate) { + Settings.upsert({ _id: 'WebRTC_Enabled' }, { + $set: { value: true }, + }); + } + }, +}); diff --git a/server/startup/migrations/v247.ts b/server/startup/migrations/v247.ts new file mode 100644 index 000000000000..29d67ae6413d --- /dev/null +++ b/server/startup/migrations/v247.ts @@ -0,0 +1,27 @@ +import { settings, settingsRegistry } from '../../../app/settings/server'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 247, + up() { + const customOauthServices = settings.getByRegexp(/Accounts_OAuth_Custom-[^-]+$/mi); + const serviceNames = customOauthServices.map(([key]) => key.replace('Accounts_OAuth_Custom-', '')); + + serviceNames.forEach((serviceName) => { + settingsRegistry.add(`Accounts_OAuth_Custom-${ serviceName }-roles_to_sync`, '', { + type: 'string', + group: 'OAuth', + section: `Custom OAuth: ${ serviceName }`, + i18nLabel: 'Accounts_OAuth_Custom_Roles_To_Sync', + i18nDescription: 'Accounts_OAuth_Custom_Roles_To_Sync_Description', + enterprise: true, + enableQuery: { + _id: `Accounts_OAuth_Custom-${ serviceName }-merge_roles`, + value: true, + }, + invalidValue: '', + modules: ['oauth-enterprise'], + }); + }); + }, +}); diff --git a/server/startup/migrations/v248.ts b/server/startup/migrations/v248.ts new file mode 100644 index 000000000000..d8670ecd540c --- /dev/null +++ b/server/startup/migrations/v248.ts @@ -0,0 +1,12 @@ +import { addMigration } from '../../lib/migrations'; +import { Apps } from '../../../app/apps/server/orchestrator'; + +addMigration({ + version: 248, + up() { + // we now have a compound index on appId + associations + // so we can use the index prefix instead of a separate index on appId + Apps.initialize(); + return Apps._persistModel?.tryDropIndex({ appId: 1 }); + }, +}); diff --git a/server/startup/migrations/v249.ts b/server/startup/migrations/v249.ts new file mode 100644 index 000000000000..d0aa0df19945 --- /dev/null +++ b/server/startup/migrations/v249.ts @@ -0,0 +1,16 @@ +import { Settings } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 249, + async up() { + await Settings.updateOne({ + _id: 'Industry', + value: 'blockchain', + }, { + $set: { + value: 'other', + }, + }); + }, +}); diff --git a/server/startup/presence.js b/server/startup/presence.js index 73fc47b2725c..b476d431ed6b 100644 --- a/server/startup/presence.js +++ b/server/startup/presence.js @@ -1,8 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { UserPresence } from 'meteor/konecty:user-presence'; -import InstanceStatusModel from '../../app/models/server/models/InstanceStatus'; -import UsersSessionsModel from '../../app/models/server/models/UsersSessions'; +import { InstanceStatus, UsersSessions } from '../../app/models/server/raw'; import { isPresenceMonitorEnabled } from '../lib/isPresenceMonitorEnabled'; Meteor.startup(function() { @@ -14,16 +13,8 @@ Meteor.startup(function() { // UserPresenceMonitor.start(); // Remove lost connections - const ids = InstanceStatusModel.find({}, { fields: { _id: 1 } }).fetch().map((id) => id._id); + const ids = Promise.await(InstanceStatus.find({}, { projection: { _id: 1 } }).toArray()) + .map((id) => id._id); - const update = { - $pull: { - connections: { - instanceId: { - $nin: ids, - }, - }, - }, - }; - UsersSessionsModel.update({}, update, { multi: true }); + Promise.await(UsersSessions.clearConnectionsFromInstanceId(ids)); }); diff --git a/server/stream/streamBroadcast.js b/server/stream/streamBroadcast.js index 4c94ccc59ab4..c866885b101b 100644 --- a/server/stream/streamBroadcast.js +++ b/server/stream/streamBroadcast.js @@ -5,11 +5,11 @@ import { check } from 'meteor/check'; import { DDP } from 'meteor/ddp'; import { Logger } from '../lib/logger/Logger'; -import { hasPermission } from '../../app/authorization'; +import { hasPermission } from '../../app/authorization/server'; import { settings } from '../../app/settings/server'; -import { isDocker, getURL } from '../../app/utils'; +import { isDocker, getURL } from '../../app/utils/server'; import { Users } from '../../app/models/server'; -import InstanceStatusModel from '../../app/models/server/models/InstanceStatus'; +import { InstanceStatus as InstanceStatusRaw } from '../../app/models/server/raw'; import { StreamerCentral } from '../modules/streamer/streamer.module'; import { isPresenceMonitorEnabled } from '../lib/isPresenceMonitorEnabled'; @@ -61,7 +61,7 @@ function startMatrixBroadcast() { } matrixBroadCastActions = { - added(record) { + added: Meteor.bindEnvironment((record) => { cache.set(record._id, record); const subPath = getURL('', { cdn: false, full: false }); @@ -100,7 +100,7 @@ function startMatrixBroadcast() { connections[instance].onReconnect = function() { return authorizeConnection(instance); }; - }, + }), removed(id) { const record = cache.get(id); @@ -129,19 +129,15 @@ function startMatrixBroadcast() { }, }; - const query = { + InstanceStatusRaw.find({ 'extraInformation.port': { $exists: true, }, - }; - - const options = { + }, { sort: { _createdAt: -1, }, - }; - - InstanceStatusModel.find(query, options).fetch().forEach(matrixBroadCastActions.added); + }).forEach(matrixBroadCastActions.added); } diff --git a/tests/cypress/integration/12-settings.js b/tests/cypress/integration/12-settings.js index be803c1f5c30..c413e0c3b348 100644 --- a/tests/cypress/integration/12-settings.js +++ b/tests/cypress/integration/12-settings.js @@ -46,8 +46,8 @@ describe('[Api Settings Change]', () => { }); it('/login', () => { - expect(credentials).to.have.property('X-Auth-Token').with.length.at.least(1); - expect(credentials).to.have.property('X-User-Id').with.length.at.least(1); + expect(credentials).to.have.property('X-Auth-Token').with.lengthOf.at.least(1); + expect(credentials).to.have.property('X-User-Id').with.lengthOf.at.least(1); }); describe('message edit:', () => { diff --git a/tests/cypress/integration/14-setting-permissions.js b/tests/cypress/integration/14-setting-permissions.js index a8270a0a00e4..4bcf2ac1d423 100644 --- a/tests/cypress/integration/14-setting-permissions.js +++ b/tests/cypress/integration/14-setting-permissions.js @@ -1,4 +1,3 @@ -/* eslint-env mocha */ import { assert } from 'chai'; import { adminUsername, adminEmail, adminPassword, username, email, password } from '../../data/user.js'; diff --git a/tests/cypress/integration/16-discussion.js b/tests/cypress/integration/16-discussion.js index 832a7c06fdd7..238d22dffac6 100644 --- a/tests/cypress/integration/16-discussion.js +++ b/tests/cypress/integration/16-discussion.js @@ -1,4 +1,3 @@ -/* eslint-env mocha */ /* eslint-disable func-names, prefer-arrow-callback, no-var, space-before-function-paren, quotes, prefer-template, no-undef, no-unused-vars*/ diff --git a/tests/end-to-end/api/00-miscellaneous.js b/tests/end-to-end/api/00-miscellaneous.js index 6dc6b084ae44..e8d04bda5480 100644 --- a/tests/end-to-end/api/00-miscellaneous.js +++ b/tests/end-to-end/api/00-miscellaneous.js @@ -43,8 +43,8 @@ describe('miscellaneous', function() { }); it('/login', () => { - expect(credentials).to.have.property('X-Auth-Token').with.length.at.least(1); - expect(credentials).to.have.property('X-User-Id').with.length.at.least(1); + expect(credentials).to.have.property('X-Auth-Token').with.lengthOf.at.least(1); + expect(credentials).to.have.property('X-User-Id').with.lengthOf.at.least(1); }); it('/login (wrapper username)', (done) => { diff --git a/tests/end-to-end/api/03-groups.js b/tests/end-to-end/api/03-groups.js index b20d253fb170..5cc6f531aa4a 100644 --- a/tests/end-to-end/api/03-groups.js +++ b/tests/end-to-end/api/03-groups.js @@ -3,7 +3,7 @@ import { expect } from 'chai'; import { getCredentials, api, request, credentials, group, apiPrivateChannelName } from '../../data/api-data.js'; import { adminUsername, password } from '../../data/user.js'; import { createUser, login } from '../../data/users.helper'; -import { updatePermission } from '../../data/permissions.helper'; +import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom } from '../../data/rooms.helper'; import { createIntegration, removeIntegration } from '../../data/integration.helper'; @@ -43,7 +43,49 @@ describe('[Groups]', function() { }) .end(done); }); + describe('/groups.create (encrypted)', () => { + it('should create a new encrypted group', async () => { + await request.post(api('groups.create')) + .set(credentials) + .send({ + name: `encrypted-${ apiPrivateChannelName }`, + extraData: { + encrypted: true, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group.name', `encrypted-${ apiPrivateChannelName }`); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.msgs', 0); + expect(res.body).to.have.nested.property('group.encrypted', true); + }); + }); + it('should create the encrypted room by default', async () => { + await updateSetting('E2E_Enabled_Default_PrivateRooms', true); + try { + await request.post(api('groups.create')) + .set(credentials) + .send({ + name: `default-encrypted-${ apiPrivateChannelName }`, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group.name', `default-encrypted-${ apiPrivateChannelName }`); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.msgs', 0); + expect(res.body).to.have.nested.property('group.encrypted', true); + }); + } finally { + await updateSetting('E2E_Enabled_Default_PrivateRooms', false); + } + }); + }); describe('[/groups.info]', () => { let testGroup = {}; let groupMessage = {}; @@ -177,7 +219,6 @@ describe('[Groups]', function() { it('/groups.invite', async () => { const roomInfo = await getRoomInfo(group._id); - return request.post(api('groups.invite')) .set(credentials) .send({ diff --git a/tests/end-to-end/api/09-rooms.js b/tests/end-to-end/api/09-rooms.js index 4153cfc26ca3..ca9fe2735327 100644 --- a/tests/end-to-end/api/09-rooms.js +++ b/tests/end-to-end/api/09-rooms.js @@ -923,6 +923,66 @@ describe('[Rooms]', function() { .end(done); }); }); + describe('[/rooms.autocomplete.adminRooms]', () => { + let testGroup; + const testGroupName = `channel.test.adminRoom${ Date.now() }-${ Math.random() }`; + const name = { + name: testGroupName, + }; + before((done) => { + createRoom({ type: 'p', name: testGroupName }) + .end((err, res) => { + testGroup = res.body.group; + request.post(api('rooms.createDiscussion')) + .set(credentials) + .send({ + prid: testGroup._id, + t_name: `${ testGroupName }-discussion`, + }) + .end(done); + }); + }); + it('should return an error when the required parameter "selector" is not provided', (done) => { + updatePermission('can-audit', ['admin']).then(() => { + request.get(api('rooms.autocomplete.adminRooms')) + .set(credentials) + .query({}) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('The \'selector\' param is required'); + }) + .end(done); + }); + }); + it('should return the rooms to fill auto complete', (done) => { + request.get(api('rooms.autocomplete.adminRooms?selector={}')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('items').and.to.be.an('array'); + }) + .end(done); + }); + it('should return FIX the rooms to fill auto complete', (done) => { + request.get(api('rooms.autocomplete.adminRooms?')) + .set(credentials) + .query({ + selector: JSON.stringify(name), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('items').and.to.be.an('array'); + expect(res.body).to.have.property('items').that.have.lengthOf(2); + }) + .end(done); + }); + }); describe('/rooms.adminRooms', () => { it('should throw an error when the user tries to gets a list of discussion and he cannot access the room', (done) => { updatePermission('view-room-administration', []).then(() => { diff --git a/tests/end-to-end/api/13-roles.js b/tests/end-to-end/api/13-roles.js deleted file mode 100644 index 162152a07214..000000000000 --- a/tests/end-to-end/api/13-roles.js +++ /dev/null @@ -1,440 +0,0 @@ -import { expect } from 'chai'; - -import { - getCredentials, - api, - request, - credentials, - login, - apiRoleNameUsers, - apiRoleNameSubscriptions, - apiRoleScopeUsers, - apiRoleDescription, - apiRoleScopeSubscriptions, -} from '../../data/api-data.js'; -import { password } from '../../data/user'; -import { updatePermission } from '../../data/permissions.helper'; -import { createUser, login as doLogin } from '../../data/users.helper'; - -function createRole(name, scope, description) { - return new Promise((resolve) => { - request.post(api('roles.create')) - .set(credentials) - .send({ - name, - scope, - description, - }) - .end((err, req) => { - resolve(req.body.role); - }); - }); -} - -function addUserToRole(roleName, username, scope) { - return new Promise((resolve) => { - request.post(api('roles.addUserToRole')) - .set(credentials) - .send({ - roleName, - username, - roomId: scope, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end((err, req) => { - resolve(req.body.role); - }); - }); -} - -describe('[Roles]', function() { - this.retries(0); - - before((done) => getCredentials(done)); - - describe('GET [/roles.list]', () => { - it('should return all roles', (done) => { - request.get(api('roles.list')) - .set(credentials) - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('roles').and.to.be.an('array'); - }) - .end(done); - }); - }); - - describe('GET [/roles.sync]', () => { - it('should return an array of roles which are updated after updatedSice date when search by "updatedSince" query parameter', (done) => { - request.get(api('roles.sync?updatedSince=2018-11-27T13:52:01Z')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('roles'); - expect(res.body.roles).to.have.property('update').and.to.be.an('array'); - expect(res.body.roles).to.have.property('remove').and.to.be.an('array'); - }) - .end(done); - }); - - it('should return an error when updatedSince query parameter is not a valid ISODate string', (done) => { - request.get(api('roles.sync?updatedSince=fsafdf')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - }) - .end(done); - }); - }); - - describe('POST [/roles.create]', () => { - it('should create a new role with Users scope', (done) => { - request.post(api('roles.create')) - .set(credentials) - .send({ - name: apiRoleNameUsers, - description: apiRoleDescription, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('role._id'); - expect(res.body).to.have.nested.property('role.name', apiRoleNameUsers); - expect(res.body).to.have.nested.property('role.scope', apiRoleScopeUsers); - expect(res.body).to.have.nested.property('role.description', apiRoleDescription); - }) - .end(done); - }); - - it('should create a new role with Subscriptions scope', (done) => { - request.post(api('roles.create')) - .set(credentials) - .send({ - name: apiRoleNameSubscriptions, - scope: apiRoleScopeSubscriptions, - description: apiRoleDescription, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('role._id'); - expect(res.body).to.have.nested.property('role.name', apiRoleNameSubscriptions); - expect(res.body).to.have.nested.property('role.scope', apiRoleScopeSubscriptions); - expect(res.body).to.have.nested.property('role.description', apiRoleDescription); - }) - .end(done); - }); - - it('should NOT create a new role with an existing role name', (done) => { - request.post(api('roles.create')) - .set(credentials) - .send({ - name: apiRoleNameUsers, - description: apiRoleDescription, - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.nested.property('error', 'Role name already exists [error-duplicate-role-names-not-allowed]'); - }) - .end(done); - }); - }); - - describe('POST [/roles.addUserToRole]', () => { - it('should assign a role with User scope to an user', (done) => { - request.post(api('roles.addUserToRole')) - .set(credentials) - .send({ - roleName: apiRoleNameUsers, - username: login.user, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('role._id'); - expect(res.body).to.have.nested.property('role.name', apiRoleNameUsers); - expect(res.body).to.have.nested.property('role.scope', apiRoleScopeUsers); - }) - .end(done); - }); - - it('should assign a role with Subscriptions scope to an user', (done) => { - request.post(api('roles.addUserToRole')) - .set(credentials) - .send({ - roleName: apiRoleNameSubscriptions, - username: login.user, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('role._id'); - expect(res.body).to.have.nested.property('role.name', apiRoleNameSubscriptions); - expect(res.body).to.have.nested.property('role.scope', apiRoleScopeSubscriptions); - }) - .end(done); - }); - }); - - describe('GET [/roles.getUsersInRole]', () => { - let userCredentials; - before((done) => { - createUser().then((createdUser) => { - doLogin(createdUser.username, password).then((createdUserCredentials) => { - userCredentials = createdUserCredentials; - updatePermission('access-permissions', ['admin', 'user']).then(done); - }); - }); - }); - it('should return an error when "role" query param is not provided', (done) => { - request.get(api('roles.getUsersInRole')) - .set(userCredentials) - .query({ - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.errorType).to.be.equal('error-param-not-provided'); - }) - .end(done); - }); - it('should return an error when the user does not the necessary permission', (done) => { - updatePermission('access-permissions', ['admin']).then(() => { - request.get(api('roles.getUsersInRole')) - .set(userCredentials) - .query({ - role: 'admin', - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.errorType).to.be.equal('error-not-allowed'); - }) - .end(done); - }); - }); - it('should return an error when the user try access rooms permissions and does not have the necessary permission', (done) => { - updatePermission('access-permissions', ['admin', 'user']).then(() => { - updatePermission('view-other-user-channels', []).then(() => { - request.get(api('roles.getUsersInRole')) - .set(userCredentials) - .query({ - role: 'admin', - roomId: 'GENERAL', - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.errorType).to.be.equal('error-not-allowed'); - }) - .end(done); - }); - }); - }); - it('should return the list of users', (done) => { - updatePermission('access-permissions', ['admin', 'user']).then(() => { - updatePermission('view-other-user-channels', ['admin', 'user']).then(() => { - request.get(api('roles.getUsersInRole')) - .set(userCredentials) - .query({ - role: 'admin', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body.users).to.be.an('array'); - }) - .end(done); - }); - }); - }); - it('should return the list of users when find by room Id', (done) => { - request.get(api('roles.getUsersInRole')) - .set(userCredentials) - .query({ - role: 'admin', - roomId: 'GENERAL', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body.users).to.be.an('array'); - }) - .end(done); - }); - }); - - describe('POST [/roles.update]', () => { - const roleName = `role-${ Date.now() }`; - let newRole; - before(async () => { - newRole = await createRole(roleName, 'Users', 'Role description test'); - }); - - it('should update an existing role', (done) => { - const newRoleName = `${ roleName }Updated`; - const newRoleDescription = 'New role description'; - - request.post(api('roles.update')) - .set(credentials) - .send({ - roleId: newRole._id, - name: newRoleName, - description: newRoleDescription, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('role._id', newRole._id); - expect(res.body).to.have.nested.property('role.name', newRoleName); - expect(res.body).to.have.nested.property('role.scope', newRole.scope); - expect(res.body).to.have.nested.property('role.description', newRoleDescription); - }) - .end(done); - }); - - it('should NOT update a role with an existing role name', (done) => { - request.post(api('roles.update')) - .set(credentials) - .send({ - roleId: newRole._id, - name: apiRoleNameUsers, - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.nested.property('error', 'Role name already exists [error-duplicate-role-names-not-allowed]'); - }) - .end(done); - }); - }); - - describe('POST [/roles.delete]', () => { - let roleWithUser; - let roleWithoutUser; - before(async () => { - roleWithUser = await createRole(`roleWithUser-${ Date.now() }`, 'Users'); - roleWithoutUser = await createRole(`roleWithoutUser-${ Date.now() }`, 'Users'); - - await addUserToRole(roleWithUser.name, login.user); - }); - - it('should delete a role that it is not being used', (done) => { - request.post(api('roles.delete')) - .set(credentials) - .send({ - roleId: roleWithoutUser._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - - it('should NOT delete a role that it is protected', (done) => { - request.post(api('roles.delete')) - .set(credentials) - .send({ - roleId: 'admin', - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.nested.property('error', 'Cannot delete a protected role [error-role-protected]'); - }) - .end(done); - }); - - it('should NOT delete a role that it is being used', (done) => { - request.post(api('roles.delete')) - .set(credentials) - .send({ - roleId: roleWithUser._id, - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.nested.property('error', 'Cannot delete role because it\'s in use [error-role-in-use]'); - }) - .end(done); - }); - }); - - describe('POST [/roles.removeUserFromRole]', () => { - let usersScopedRole; - let subscriptionsScopedRole; - - before(async () => { - usersScopedRole = await createRole(`usersScopedRole-${ Date.now() }`, 'Users'); - subscriptionsScopedRole = await createRole(`subscriptionsScopedRole-${ Date.now() }`, 'Subscriptions'); - - await addUserToRole(usersScopedRole.name, login.user); - await addUserToRole(subscriptionsScopedRole.name, login.user, 'GENERAL'); - }); - - it('should unassign a role with User scope from an user', (done) => { - request.post(api('roles.removeUserFromRole')) - .set(credentials) - .send({ - roleName: usersScopedRole.name, - username: login.user, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('role._id', usersScopedRole._id); - expect(res.body).to.have.nested.property('role.name', usersScopedRole.name); - expect(res.body).to.have.nested.property('role.scope', usersScopedRole.scope); - expect(res.body).to.have.nested.property('role.description', usersScopedRole.description); - }) - .end(done); - }); - - it('should unassign a role with Subscriptions scope from an user', (done) => { - request.post(api('roles.removeUserFromRole')) - .set(credentials) - .send({ - roleName: subscriptionsScopedRole.name, - username: login.user, - scope: 'GENERAL', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('role._id', subscriptionsScopedRole._id); - expect(res.body).to.have.nested.property('role.name', subscriptionsScopedRole.name); - expect(res.body).to.have.nested.property('role.scope', subscriptionsScopedRole.scope); - expect(res.body).to.have.nested.property('role.description', subscriptionsScopedRole.description); - }) - .end(done); - }); - }); -}); diff --git a/tests/end-to-end/api/21-banners.js b/tests/end-to-end/api/21-banners.js index 6d31095aced4..ede0cccc737a 100644 --- a/tests/end-to-end/api/21-banners.js +++ b/tests/end-to-end/api/21-banners.js @@ -27,7 +27,6 @@ describe('banners', function() { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'Match error: Missing key \'platform\''); }) .end(done); }); @@ -41,8 +40,6 @@ describe('banners', function() { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'Platform is unknown. [error-unknown-platform]'); - expect(res.body).to.have.property('errorType', 'error-unknown-platform'); }) .end(done); }); @@ -56,8 +53,6 @@ describe('banners', function() { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'The required "platform" param is missing. [error-missing-param]'); - expect(res.body).to.have.property('errorType', 'error-missing-param'); }) .end(done); }); @@ -112,8 +107,6 @@ describe('banners', function() { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'The required "bannerId" param is missing. [error-missing-param]'); - expect(res.body).to.have.property('errorType', 'error-missing-param'); }) .end(done); }); diff --git a/tests/end-to-end/api/26-LDAP.ts b/tests/end-to-end/api/26-LDAP.ts new file mode 100644 index 000000000000..a8dd7fe18c55 --- /dev/null +++ b/tests/end-to-end/api/26-LDAP.ts @@ -0,0 +1,24 @@ +import { expect } from 'chai'; +import type { Response } from 'supertest'; + +import { getCredentials, api, request, credentials } from '../../data/api-data.js'; + +describe('LDAP', function() { + this.retries(0); + + before((done) => getCredentials(done)); + + describe('[/ldap.syncNow]', () => { + it('should throw an error containing totp-required error ', (done) => { + request.post(api('ldap.syncNow')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'totp-required'); + }) + .end(done); + }); + }); +}); diff --git a/tests/mocks/client/RouterContextMock.tsx b/tests/mocks/client/RouterContextMock.tsx new file mode 100644 index 000000000000..71b765bde494 --- /dev/null +++ b/tests/mocks/client/RouterContextMock.tsx @@ -0,0 +1,44 @@ +import React, { ContextType, ReactElement, ReactNode, useMemo } from 'react'; +import { Subscription } from 'use-subscription'; + +import { RouterContext } from '../../../client/contexts/RouterContext'; + +type RouterContextMockProps = { + children?: ReactNode; + pushRoute?: (name: string, parameters?: Record, queryStringParameters?: Record) => void; + replaceRoute?: (name: string, parameters?: Record, queryStringParameters?: Record) => void; +}; + +const RouterContextMock = ({ children, pushRoute, replaceRoute }: RouterContextMockProps): ReactElement => { + const value = useMemo>(() => ({ + queryRoutePath: (): Subscription => ({ + getCurrentValue: (): undefined => undefined, + subscribe: () => (): void => undefined, + }), + queryRouteUrl: (): Subscription => ({ + getCurrentValue: (): undefined => undefined, + subscribe: () => (): void => undefined, + }), + pushRoute: pushRoute ?? ((): void => undefined), + replaceRoute: replaceRoute ?? ((): void => undefined), + queryRouteParameter: (): Subscription => ({ + getCurrentValue: (): undefined => undefined, + subscribe: () => (): void => undefined, + }), + queryQueryStringParameter: (): Subscription => ({ + getCurrentValue: (): undefined => undefined, + subscribe: () => (): void => undefined, + }), + queryCurrentRoute: (): Subscription<[undefined, {}, {}, undefined]> => ({ + getCurrentValue: (): [undefined, {}, {}, undefined] => [undefined, {}, {}, undefined], + subscribe: () => (): void => undefined, + }), + }), [pushRoute, replaceRoute]); + + return ; +}; + +export default RouterContextMock; diff --git a/tests/mocks/client/blobUrls.ts b/tests/mocks/client/blobUrls.ts new file mode 100644 index 000000000000..5e01a99dc927 --- /dev/null +++ b/tests/mocks/client/blobUrls.ts @@ -0,0 +1,23 @@ +import uuid from 'uuid'; + +export const enableBlobUrlsMock = (): void => { + const urlByBlob = new WeakMap(); + const blobByUrl = new Map(); + + window.URL.createObjectURL = (blob: Blob): string => { + const url = urlByBlob.get(blob) ?? `blob://${ uuid.v4() }`; + urlByBlob.set(blob, url); + blobByUrl.set(url, blob); + return url; + }; + + window.URL.revokeObjectURL = (url: string): void => { + const blob = blobByUrl.get(url); + if (!blob) { + return; + } + + urlByBlob.delete(blob); + blobByUrl.delete(url); + }; +}; diff --git a/tests/mocks/client/jsdom.ts b/tests/mocks/client/jsdom.ts new file mode 100644 index 000000000000..857dc69896ae --- /dev/null +++ b/tests/mocks/client/jsdom.ts @@ -0,0 +1,10 @@ +import globalJsdom from 'jsdom-global'; + +export const enableJsdom = (): void => { + globalJsdom( + '', + { + url: 'http://localhost:3000', + }, + ); +}; diff --git a/tests/setup/chaiPlugins.ts b/tests/setup/chaiPlugins.ts new file mode 100644 index 000000000000..a9f0a1dc5b86 --- /dev/null +++ b/tests/setup/chaiPlugins.ts @@ -0,0 +1,8 @@ +import chai from 'chai'; +import chaiSpies from 'chai-spies'; +import chaiDatetime from 'chai-datetime'; +import chaiDom from 'chai-dom'; + +chai.use(chaiSpies); +chai.use(chaiDatetime); +chai.use(chaiDom); diff --git a/tests/setup/cleanupTestingLibrary.ts b/tests/setup/cleanupTestingLibrary.ts new file mode 100644 index 000000000000..663890796ac8 --- /dev/null +++ b/tests/setup/cleanupTestingLibrary.ts @@ -0,0 +1,17 @@ +import { cleanup } from '@testing-library/react'; + +/** + * Usually the testing library attachs its `cleanup` function by itself when an `afterEach` function is present at the + * global scope. It provides a simple mechanism for, e.g., unmounting React components after tests to avoid leaking + * memory and breaking the idempotence of subsequent tests. Despite working fine at a single run, when Mocha is run in + * _watch mode_ all hooks previously attached are discarded and reloaded from **tests files only**, and its supposed to + * work that way. + * + * See https://testing-library.com/docs/react-testing-library/setup#auto-cleanup-in-mochas-watch-mode + */ + +export const mochaHooks = { + afterEach(): void { + cleanup(); + }, +}; diff --git a/tests/setup/registerWebApiMocks.ts b/tests/setup/registerWebApiMocks.ts new file mode 100644 index 000000000000..83bf826fa0d0 --- /dev/null +++ b/tests/setup/registerWebApiMocks.ts @@ -0,0 +1,5 @@ +import { enableBlobUrlsMock } from '../mocks/client/blobUrls'; +import { enableJsdom } from '../mocks/client/jsdom'; + +enableJsdom(); +enableBlobUrlsMock(); diff --git a/tsconfig.json b/tsconfig.json index 0be783b373b1..1094df397a41 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,7 +31,7 @@ "moduleResolution": "node", "resolveJsonModule": true, "esModuleInterop": true, - "preserveSymlinks": true, + "preserveSymlinks": true // "sourceMap": true, // "declaration": true, @@ -42,8 +42,12 @@ "exclude": [ "./.meteor/**", "./packages/**", - "./imports/client", + "./imports/client**", + "**/dist/**", // "./ee/server/services/**" - // "node_modules" - ] + "node_modules" + ], + "ts-node": { + "files": true + } } diff --git a/typings.d.ts b/typings.d.ts deleted file mode 100644 index 2f202dc49b0a..000000000000 --- a/typings.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -declare module 'meteor/rocketchat:tap-i18n'; -declare module 'meteor/littledata:synced-cron'; -declare module 'meteor/promise'; -declare module 'meteor/ddp-common'; -declare module 'meteor/routepolicy'; -declare module 'meteor/logging'; -declare module 'xml-encryption'; -declare module 'webdav'; - -declare module 'meteor/konecty:user-presence' { - namespace UserPresenceMonitor { - function processUserSession(userSession: any, event: string): void; - } - - namespace UserPresence { - function removeConnectionsByInstanceId(id: string): void; - } -} - -declare const Package: { - 'disable-oplog': object; -}; - -declare module 'meteor/meteorhacks:inject-initial' { - namespace Inject { - function rawBody(key: string, value: string): void; - } -}