diff --git a/.babelrc b/.babelrc index 867a790a279a5..b9359fe771b40 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 7e9589c28226d..20f7de35cc0d5 100644 --- a/.docker/Dockerfile.rhel +++ b/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/ubi8/nodejs-12 -ENV RC_VERSION 3.18.2 +ENV RC_VERSION 4.3.0-develop MAINTAINER buildmaster@rocket.chat diff --git a/.eslintignore b/.eslintignore index 24f6298dbc9df..38a10ea159b18 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 8833cddb4eecd..0d96bb0a34f80 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-manual.json b/.github/history-manual.json index de60527e3d415..6258a78a3d945 100644 --- a/.github/history-manual.json +++ b/.github/history-manual.json @@ -123,5 +123,12 @@ "sampaiodiego", "pierre-lehnen-rc" ] + }], + "4.1.1": [{ + "title": "[FIX] Security Hotfix (https://docs.rocket.chat/guides/security/security-updates)", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] }] } diff --git a/.github/history.json b/.github/history.json index 771eb8aaaa9c0..b441b4f94ab11 100644 --- a/.github/history.json +++ b/.github/history.json @@ -64551,82 +64551,3316 @@ ], "pull_requests": [] }, + "4.0.0-rc.0": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0-alpha.5428", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23218", + "title": "[FIX] Sidebar not closing when clicking in Home or Directory on mobile view", + "userLogin": "dougfabris", + "description": "### Additional fixed\r\n- Merge Burger menu components into a single component\r\n- Show a badge with no-read messages in the Burger Button:\r\n![image](https://user-images.githubusercontent.com/27704687/133679378-20fea2c0-4ac1-4b4e-886e-45154cc6afea.png)\r\n- remove useSidebarClose hook", + "contributors": [ + "dougfabris", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "23281", + "title": "Regression: wrong settings order", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "22407", + "title": "[FIX] Prevent users to edit an existing role when adding a new one with the same name used before.", + "userLogin": "dougfabris", + "description": "### before\r\n![Peek 2021-07-13 16-31](https://user-images.githubusercontent.com/27704687/125513721-953d84f4-1c95-45ca-80e1-b00992b874f6.gif)\r\n\r\n### after\r\n![Peek 2021-07-13 16-34](https://user-images.githubusercontent.com/27704687/125514098-91ee8014-51e5-4c62-9027-5538acf57d08.gif)", + "contributors": [ + null, + "lucassartor", + "dougfabris", + "ggazzo", + "web-flow", + "pierre-lehnen-rc", + "tassoevan" + ] + }, + { + "pr": "23282", + "title": "Regression: Missing i18n key", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "23201", + "title": "[BREAK] Moved advanced oAuth features to EE", + "userLogin": "ostjen", + "contributors": [ + "ostjen", + "web-flow", + "pierre-lehnen-rc" + ] + }, + { + "pr": "23256", + "title": "[IMPROVE] Better text for auth banner", + "userLogin": "g-thome", + "description": "Change the text in the banner warning for auth changes", + "contributors": [ + "g-thome", + "tassoevan", + "web-flow", + "pierre-lehnen-rc" + ] + }, + { + "pr": "23090", + "title": "[NEW] Omnichannel source identification fields", + "userLogin": "d-gubert", + "description": "This PR adds new fields to the room schema that aids in the identification of the source that created an Omnichannel room, which can be either via livechat widget, SMS, app, etc.", + "milestone": "4.0.0", + "contributors": [ + "d-gubert", + "KevLehman", + "web-flow", + "tiagoevanp", + "MartinSchoeler" + ] + }, + { + "pr": "23231", + "title": "Regression: LDAP Refactoring", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "22657", + "title": "[IMPROVE][APPS] New storage strategy for Apps-Engine file packages", + "userLogin": "d-gubert", + "description": "This is an enabler for our initiative to support NPM packages in the Apps-Engine. \r\n\r\nCurrently, the packages (zip files) for Rocket.Chat Apps are stored as a base64 encoded string in a document in the database, which constrains us due to the size limit of a document in MongoDB (16Mb).\r\n\r\nWhen we allow apps to include NPM packages, the size of the App package itself will be potentially _very large_ (I'm looking at you `node_modules`). Thus we'll be changing the strategy to store apps either with GridFS or the host's File System itself.", + "milestone": "4.0.0", + "contributors": [ + "d-gubert", + "web-flow", + "thassiov" + ] + }, + { + "pr": "23243", + "title": "[FIX] Modals is cutting pixels of the content", + "userLogin": "dougfabris", + "description": "Fuselage Dependency: [543](https://github.com/RocketChat/Rocket.Chat.Fuselage/pull/543)\r\n![image](https://user-images.githubusercontent.com/27704687/134049227-3cd1deed-34ba-454f-a95e-e99b79a7a7b9.png)", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "23232", + "title": "[IMPROVE] Load code highlighting languages on demand and fixes on new message parser", + "userLogin": "ggazzo", + "description": "Now we have this setting called 'Code highlighting languages list' where you can define the languages that you want to be loaded by default.", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "23223", + "title": "[BREAK][ENTERPRISE] Missing headers in CSV files downloaded from the Engagement Dashboard", + "userLogin": "matheusbsilva137", + "description": "- Add headers to all CSV files downloaded from the \"Messages\" and \"Channels\" tabs from the Engagement Dashboard;\r\n - Add headers to the CSV file downloaded from the \"Users by time of day\" section (in the \"Users\" tab).", + "contributors": [ + "matheusbsilva137", + "casalsgh", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "23074", + "title": "[FIX] transfer message when tranferring room by Apps Engine", + "userLogin": "cuonghuunguyen", + "milestone": "4.0.0", + "contributors": [ + "cuonghuunguyen", + "KevLehman", + "web-flow" + ] + }, + { + "pr": "22392", + "title": "[NEW] Add activity indicators for Uploading and Recording using new API; Support thread context; Deprecate the old typing API", + "userLogin": "sumukhah", + "milestone": "4.0.0", + "contributors": [ + "sumukhah", + "rodrigok" + ] + }, + { + "pr": "23236", + "title": "Bump ejson from 2.2.1 to 2.2.2 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23056", + "title": "[FIX] Remove doubled \"Canned Responses\" strings", + "userLogin": "matheusbsilva137", + "description": "- Remove doubled canned response setting introduced in #22703 (by setting id change);\r\n - Update \"Canned Responses\" keys to \"Canned_Responses\".", + "contributors": [ + "matheusbsilva137", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "23110", + "title": "[FIX] Can't edit profile information if any field update setting is disabled", + "userLogin": "matheusbsilva137", + "description": "- Check which fields have been updated before throwing errors in `validateUserEditing`.", + "contributors": [ + "matheusbsilva137", + "web-flow", + "casalsgh" + ] + }, + { + "pr": "23037", + "title": "[FIX] \"Read Only\" and \"Allow Reacting\" system messages are missing in rooms", + "userLogin": "matheusbsilva137", + "description": "- Add system message to notify changes on the **\"Read Only\"** setting;\r\n - Add system message to notify changes on the **\"Allow Reacting\"** setting;\r\n - Fix \"Allow Reacting\" setting's description (updated from \"Only authorized users can write new messages\" to \"Only authorized users can react to messages\").\r\n![system-messages](https://user-images.githubusercontent.com/36537004/130883527-9eb47fcd-c8e5-41fb-af34-5d99bd0a6780.PNG)", + "contributors": [ + "matheusbsilva137", + "web-flow", + "ostjen", + "casalsgh" + ] + }, + { + "pr": "23277", + "title": "[BREAK] Remove old migrations up to version 2.4.14", + "userLogin": "sampaiodiego", + "description": "To update to version 4.0.0 you'll need to be running at least version 3.0.0, otherwise you might loose some database migrations which might have unexpected effects.\r\n\r\nThis aims to clean up the code, since upgrades jumping 2 major versions are too risky and hard to maintain, we'll keep only migration from that last major (in this case 3.x).", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23276", + "title": "[FIX] Logging out from other clients", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "pierre-lehnen-rc", + "web-flow" + ] + }, + { + "pr": "23262", + "title": "[FIX] Avoid bots to be marked as unavailable when log off/login", + "userLogin": "KevLehman", + "milestone": "4.0.0", + "contributors": [ + "KevLehman", + "murtaza98", + "web-flow" + ] + }, + { + "pr": "23261", + "title": "[FIX] Stop queue when Omnichannel is disabled or the routing method does not support it", + "userLogin": "KevLehman", + "description": "- Add missing key logs\r\n- Stop queue (and logs) when livechat is disabled or when routing method does not support queue\r\n- Stop ignoring offline bot agents from delegation (previously, if a bot was offline, even with \"Assign new conversations to bot agent\" enabled, bot will be ignored and chat will be left in limbo (since bot was assigned, but offline).", + "milestone": "4.0.0", + "contributors": [ + "KevLehman", + "murtaza98", + "web-flow" + ] + }, + { + "pr": "23017", + "title": "[NEW] Seats Cap", + "userLogin": "tassoevan", + "description": "- Adding New Members\r\n - Awareness of seats usage while adding new members\r\n - Seats Cap about to be reached\r\n - Seats Cap reached\r\n - Request more seats\r\n- Warning Admins\r\n - System telling admins max seats are about to exceed\r\n - System telling admins max seats were exceed\r\n - Metric on Info Page\r\n - Request more seats\r\n- Warning Members\r\n - Invite link\r\n - Block creating new invite links\r\n - Block existing invite links (feedback on register process) \r\n - Register to Workspaces\r\n- Emails\r\n - System telling admins max seats are about to exceed\r\n - System telling admins max seats were exceed", + "contributors": [ + "tassoevan", + "pierre-lehnen-rc", + "web-flow", + "ggazzo", + "gabriellsh", + "g-thome" + ] + }, + { + "pr": "23269", + "title": "Chore: Update pino and pino-pretty", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23255", + "title": "Chore: Make SMTP empty on docker-compose so registration won't hang out of the box", + "userLogin": "geekgonecrazy", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "23204", + "title": "[FIX] Wrap canned-responses endpoints with ee license", + "userLogin": "tiagoevanp", + "milestone": "4.0.0", + "contributors": [ + "tiagoevanp", + "web-flow", + "KevLehman" + ] + }, + { + "pr": "23150", + "title": "[FIX] Omnichannel transcript button without user's email", + "userLogin": "tiagoevanp", + "milestone": "4.0.0", + "contributors": [ + "tiagoevanp", + "web-flow", + "KevLehman" + ] + }, + { + "pr": "23263", + "title": "Chore: Re-enable session tests on local after removal of mongo-unit", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman", + "sampaiodiego" + ] + }, + { + "pr": "23107", + "title": "[BREAK] Moved role-sync and advanced SAML settings to EE", + "userLogin": "ostjen", + "contributors": [ + "ostjen", + "web-flow", + "pierre-lehnen-rc" + ] + }, + { + "pr": "23199", + "title": "[IMPROVE] Change occurences of Livechat to Omnichannel in ES translations were applicable", + "userLogin": "KevLehman", + "milestone": "4.0.0", + "contributors": [ + "KevLehman", + "web-flow" + ] + }, + { + "pr": "23230", + "title": "Regression: Log Sections not respecting Log Level setting", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "22907", + "title": "[BREAK] Removed support of MongoDB 3.4; Deprecated MongoDB 3.6 and 4.0", + "userLogin": "ostjen", + "milestone": "4.0.0", + "contributors": [ + "ostjen", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "23254", + "title": "Regression: Fix user registration stuck", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "23219", + "title": "[FIX] Mark agents as unavailable when they logout", + "userLogin": "KevLehman", + "milestone": "4.0.0", + "contributors": [ + "KevLehman", + "renatobecker", + "web-flow", + "murtaza98" + ] + }, + { + "pr": "23244", + "title": "[FIX] Toolbox click not working on Safari(iOS)", + "userLogin": "dougfabris", + "contributors": [ + "ggazzo", + "dougfabris" + ] + }, + { + "pr": "23185", + "title": "[FIX] Omnichannel On hold chats being forwarded to offline agents", + "userLogin": "murtaza98", + "milestone": "4.0.0", + "contributors": [ + "murtaza98", + "web-flow" + ] + }, + { + "pr": "23171", + "title": "[BREAK] LDAP Refactoring", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc", + "web-flow" + ] + }, + { + "pr": "23190", + "title": "[IMPROVE] Canned response admin settings", + "userLogin": "tiagoevanp", + "milestone": "4.0.0", + "contributors": [ + "tiagoevanp", + "web-flow", + "KevLehman" + ] + }, + { + "pr": "23198", + "title": "Chore: Update Livechat widget to 1.9.4", + "userLogin": "MartinSchoeler", + "contributors": [ + "MartinSchoeler", + "KevLehman", + "web-flow" + ] + }, + { + "pr": "23209", + "title": "[FIX] Save department agents ", + "userLogin": "tiagoevanp", + "milestone": "4.0.0", + "contributors": [ + "tiagoevanp", + "web-flow", + "KevLehman", + "casalsgh" + ] + }, + { + "pr": "23117", + "title": "[FIX] Wrong docs link on Omni-Webhook page", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98", + "web-flow", + "KevLehman" + ] + }, + { + "pr": "23221", + "title": "[IMPROVE] Throw error if no appId is provided to useUIKitHandleAction", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "22957", + "title": "[IMPROVE] Do not re-create General room on every server start", + "userLogin": "matheusbsilva137", + "description": "- Check the `Show_Setup_Wizard` Setting's value to control whether the general room should be created. This channel will only be created if the `Show_Setup_Wizard` Setting is 'pending'.", + "contributors": [ + "matheusbsilva137", + "web-flow", + "casalsgh" + ] + }, + { + "pr": "22985", + "title": "[NEW][APPS] Get livechat's room transcript via bridge method", + "userLogin": "thassiov", + "description": "Adds a new method for retrieving a room's transcript via a new method in the Livechat bridge", + "milestone": "4.0.0", + "contributors": [ + "thassiov", + "web-flow", + "d-gubert" + ] + }, + { + "pr": "23212", + "title": "Regression: `renderEmoji` helper referred as a template", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "22542", + "title": "Chore: Convert VerticalBar component to typescript", + "userLogin": "dougfabris", + "contributors": [ + "dougfabris", + "gabriellsh", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "21176", + "title": "[FIX] Add missing custom fields to apps' users converter", + "userLogin": "cuonghuunguyen", + "milestone": "4.0.0", + "contributors": [ + "cuonghuunguyen", + "web-flow", + "d-gubert", + "thassiov", + "casalsgh" + ] + }, + { + "pr": "23194", + "title": "Regression: Fix view logs admin screen", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "23162", + "title": "[BREAK] Remove deprecated endpoints", + "userLogin": "sampaiodiego", + "description": "The following REST endpoints were removed:\r\n\r\n- `/api/v1/emoji-custom`\r\n- `/api/v1/info`\r\n- `/api/v1/permissions`\r\n- `/api/v1/permissions.list`\r\n\r\nThe following Real time API Methods were removed:\r\n\r\n- `getFullUserData`\r\n- `getServerInfo`\r\n- `livechat:saveOfficeHours`", + "contributors": [ + "sampaiodiego", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "23205", + "title": "Regression: View Logs administration page crashing", + "userLogin": "tassoevan", + "description": "Fixes the `stdout.queue` endpoint; makes the components type-safe.", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "23178", + "title": "Chore: Move client helpers", + "userLogin": "tassoevan", + "description": "Moves helper modules under `app/` to `client/lib/utils/`.", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "23200", + "title": "Chore: Change Ubuntu version to 20.04 on all GitHub Actions", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman", + "sampaiodiego" + ] + }, + { + "pr": "23196", + "title": "Regression: Properly trickle-down state from UsersPage to UsersTable", + "userLogin": "tassoevan", + "description": "Spotted by @gabriellsh.", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "23176", + "title": "[IMPROVE] Add missing pt-BR translations, fix typos and unify language", + "userLogin": "gabrieloliverio", + "contributors": [ + "gabrieloliverio", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "23032", + "title": "[FIX] User list not being updated after creation/deletion of user", + "userLogin": "ostjen", + "contributors": [ + "ostjen", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "23187", + "title": "Chore: Upgrade limax", + "userLogin": "tassoevan", + "description": "Upgrades `limax` for faster slugify algorithm.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23076", + "title": "[FIX] \"Parent channel or group\" search in discussions' creation throws \"Unexpected end of JSON input\" error", + "userLogin": "matheusbsilva137", + "description": "- Use `encodeURIComponent()` to encode values received by `_generateQueryFromParams()`.", + "contributors": [ + "matheusbsilva137", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "23108", + "title": "[BREAK] Stop sending audio notifications via stream", + "userLogin": "sampaiodiego", + "description": "Remove audio preferences and make them tied to desktop notification preferences.\r\n\r\nTL;DR: new message sounds will play only if you receive a desktop notification. you'll still be able to chose to not play any sound though", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23184", + "title": "i18n: Language update from LingoHub 🤖 on 2021-09-13Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null + ] + }, + { + "pr": "21779", + "title": "[FIX] Remove margin from quote inside quote", + "userLogin": "tiagoevanp", + "description": "![image](https://user-images.githubusercontent.com/17487063/116253926-4a89e600-a747-11eb-9172-f2ed1245fa1b.png)", + "contributors": [ + "tiagoevanp", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "23160", + "title": "[BREAK] Remove Google Vision features", + "userLogin": "sampaiodiego", + "description": "Google Vision features like \"block adult images\" or label detection were not being maintained and totally broken. So we decided to remove its feature and maybe in the future release the same features as an app.", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23165", + "title": "Bump @storybook/react from 6.3.6 to 6.3.8", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23163", + "title": "Bump jsrsasign from 10.3.0 to 10.4.0", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23013", + "title": "[BREAK][ENTERPRISE] \"Download CSV\" button doesn't work in the Engagement Dashboard's Active Users section", + "userLogin": "tassoevan", + "description": "- Fix \"Download CSV\" button in the Engagement Dashboard's Active Users section;\r\n- Add column headers to the CSV file downloaded from the Engagement Dashboard's Active Users section;\r\n- Split the data in multiple CSV files.", + "contributors": [ + "matheusbsilva137", + "dougfabris", + "gabriellsh", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "23014", + "title": "[BREAK][ENTERPRISE] CSV file downloaded in the Engagement Dashboard's New Users section contains undefined data", + "userLogin": "tassoevan", + "description": "- Fix CSV file downloaded in the Engagement Dashboard's New Users section;\r\n - Add column headers to the CSV file downloaded from the Engagement Dashboard's New Users section.", + "contributors": [ + "matheusbsilva137", + "gabriellsh", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "23139", + "title": "Bump supertest from 6.1.3 to 6.1.6", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23152", + "title": "Chore: client endpoints typings", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "23157", + "title": "Chore: Update pino and pino-pretty", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23138", + "title": "Bump @rocket.chat/string-helpers from 0.27.0 to 0.29.0 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "22978", + "title": "[FIX] Inaccurate use of 'Mobile notifications' instead of 'Push notifications' in i18n strings", + "userLogin": "matheusbsilva137", + "description": "- Fix inaccurate use of 'Mobile notifications' (which is misleading in German) by 'Push notifications';\r\n - Update `'Notification_Mobile_Default_For'` key to `'Notification_Push_Default_For'` (and text to 'Send Push Notifications For' for English Language);\r\n - Update `'Accounts_Default_User_Preferences_mobileNotifications'` key to `'Accounts_Default_User_Preferences_pushNotifications'`;\r\n - Update `'Mobile_Notifications_Default_Alert'` key to `'Mobile_Push_Notifications_Default_Alert'`;", + "contributors": [ + "matheusbsilva137", + "web-flow", + "ostjen" + ] + }, + { + "pr": "23141", + "title": "Bump xml-crypto from 2.1.2 to 2.1.3", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "22975", + "title": "[IMPROVE] Change log format to JSON", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "23091", + "title": "Regression: Auth banner for EE", + "userLogin": "g-thome", + "description": "Dimisses auth banners assigned to EE admins and prevents new ones from appearing.", + "milestone": "3.18.1", + "contributors": [ + "g-thome", + "casalsgh", + "web-flow" + ] + }, + { + "pr": "23023", + "title": "[IMPROVE][APPS] Return task ids when using the scheduler api", + "userLogin": "thassiov", + "description": "In the methods that create tasks (`scheduleRecurring` and `scheduleOnce`) return the `id` of the document created in the database so the user can cancel each task individually.", + "milestone": "4.0.0", + "contributors": [ + "thassiov", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "23104", + "title": "[FIX] Update bugsnag package", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23128", + "title": "Bump pm2 from 5.1.0 to 5.1.1 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23126", + "title": "Bump @types/ejson from 2.1.2 to 2.1.3 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23109", + "title": "Chore: Remove non-used dependencies", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23095", + "title": "Bump @types/ws from 7.4.6 to 7.4.7 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23068", + "title": "Bump tar from 6.1.0 to 6.1.11 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23122", + "title": "Bump @types/imap from 0.8.34 to 0.8.35", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23120", + "title": "Bump csv-parse from 4.16.0 to 4.16.3", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23123", + "title": "i18n: Language update from LingoHub 🤖 on 2021-09-06Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null + ] + }, + { + "pr": "22177", + "title": "Bump juice from 5.2.0 to 8.0.0", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23089", + "title": "[FIX] Change way emails are validated on livechat registerGuest method", + "userLogin": "KevLehman", + "milestone": "3.18.1", + "contributors": [ + "KevLehman", + "web-flow" + ] + }, + { + "pr": "23054", + "title": "[IMPROVE] Use PaginatedSelectFiltered in department edition", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98", + "tiagoevanp", + "web-flow", + "KevLehman" + ] + }, + { + "pr": "23053", + "title": "[FIX] Add check before placing chat on-hold to confirm that contact sent last message", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98", + "web-flow", + "KevLehman" + ] + }, + { + "pr": "22036", + "title": "Bump stylelint-order from 2.2.1 to 4.1.0", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "22527", + "title": "Bump iconv-lite from 0.4.24 to 0.6.3", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "22528", + "title": "Bump image-size from 0.6.3 to 1.0.0", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "22532", + "title": "Bump ip-range-check from 0.0.2 to 0.2.0", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23100", + "title": "[IMPROVE] Change HTTP and Method logs to level INFO", + "userLogin": "sampaiodiego", + "milestone": "3.18.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "16050", + "title": "[BREAK] Remove patch info from endpoint /api/info for non-logged in users", + "userLogin": "MarcosSpessatto", + "milestone": "4.0.0", + "contributors": [ + "MarcosSpessatto", + "tassoevan", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "23088", + "title": "Bump object-path from 0.11.5 to 0.11.6", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "22922", + "title": "Chore: Environmental variable for marketplace url", + "userLogin": "graywolf336", + "contributors": [ + "graywolf336" + ] + }, + { + "pr": "22600", + "title": "Bump @types/cookie from 0.4.0 to 0.4.1 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "22598", + "title": "Bump @types/express from 4.17.12 to 4.17.13 in /ee/server/services", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "22673", + "title": "Bump actions/stale from 3.0.19 to 4", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23061", + "title": "i18n: Language update from LingoHub 🤖 on 2021-08-30Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null + ] + }, + { + "pr": "23086", + "title": "Merge master into develop & Set version to 4.0.0", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "23079", + "title": "Chore: Remove wrong usages of `Meteor.wrapAsync`", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, "3.18.1": { "node_version": "12.22.1", "npm_version": "6.14.1", - "apps_engine_version": "1.27.1", + "apps_engine_version": "1.27.1", + "mongo_versions": [ + "3.4", + "3.6", + "4.0", + "4.2" + ], + "pull_requests": [ + { + "pr": "23135", + "title": "Release 3.18.1", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "KevLehman", + "g-thome" + ] + }, + { + "pr": "23091", + "title": "Regression: Auth banner for EE", + "userLogin": "g-thome", + "description": "Dimisses auth banners assigned to EE admins and prevents new ones from appearing.", + "milestone": "3.18.1", + "contributors": [ + "g-thome", + "casalsgh", + "web-flow" + ] + }, + { + "pr": "23089", + "title": "[FIX] Change way emails are validated on livechat registerGuest method", + "userLogin": "KevLehman", + "milestone": "3.18.1", + "contributors": [ + "KevLehman", + "web-flow" + ] + }, + { + "pr": "23100", + "title": "[IMPROVE] Change HTTP and Method logs to level INFO", + "userLogin": "sampaiodiego", + "milestone": "3.18.1", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "4.0.0-rc.1": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0-alpha.5428", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23286", + "title": "Regression: Fix app storage migration", + "userLogin": "thassiov", + "description": "The previous version of this migration didn't take into consideration apps that were installed prior to [Rocket.Chat@3.8.0](https://github.com/RocketChat/Rocket.Chat/releases/tag/3.8.0), which [removed the typescript compiler from the server](https://github.com/RocketChat/Rocket.Chat/pull/18687) and into the CLI. As a result, the zip files inside each installed app's document in the database had typescript files in them instead of the now required javascript files.\r\n\r\nAs the new strategy of source code storage for apps changes the way the app is loaded, those zip files containing the source code are read everytime the app is started (or [in this particular case, updated](https://github.com/RocketChat/Rocket.Chat/pull/23286/files#diff-caf9f7a22478639e58d6514be039140a42ce1ab2d999c3efe5678c38ee36d0ccR43)), and as the zips' contents were wrong, the operation was failing.\r\n\r\nThe fix extract the data from old apps and creates new zip files with the compiled `js` already present.", + "contributors": [ + "thassiov" + ] + }, + { + "pr": "23278", + "title": "Regression: Seats Cap banner not being disabled if not enterprise", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + } + ] + }, + "4.0.0-rc.2": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0-alpha.5428", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23297", + "title": "Regression: Create new loggers based on server log level", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23304", + "title": "Regression: Fix channel icons on queue", + "userLogin": "MartinSchoeler", + "contributors": [ + "KevLehman", + "MartinSchoeler" + ] + }, + { + "pr": "23280", + "title": "[FIX] Update visitor info on email reception based on current inbox settings", + "userLogin": "KevLehman", + "milestone": "4.0.0", + "contributors": [ + "KevLehman", + "murtaza98", + "web-flow" + ] + } + ] + }, + "4.0.0-rc.3": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0-alpha.5428", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23306", + "title": "Regression: LDAP Issues", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "23302", + "title": "[BREAK] Remove cordova compatibility setting", + "userLogin": "ostjen", + "contributors": [ + "ostjen" + ] + }, + { + "pr": "23308", + "title": "Regression: Fix Bugsnag not started error", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23307", + "title": "Regression: Change some logs to new format", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + } + ] + }, + "4.0.0-rc.4": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0-alpha.5428", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23319", + "title": "[BREAK] Moved SAML custom field map to EE", + "userLogin": "ostjen", + "contributors": [ + "ostjen", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "23320", + "title": "Regression: \"Join\" button not working", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23318", + "title": "Regression: Add default value when no cookies are present", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23317", + "title": "Regression: Request seats url", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + }, + { + "pr": "23310", + "title": "[BREAK] Webhook will fail if user is not part of the channel", + "userLogin": "sampaiodiego", + "description": "Remove deprecated behavior added by https://github.com/RocketChat/Rocket.Chat/pull/18024 that accepts webhook integrations sending messages even if the user is not part of the channel.\r\n\r\nStarting from 4.0.0 the webhook request will fail with `error-not-allowed` error:\r\n\r\n```\r\n{\"success\":false,\"error\":\"error-not-allowed\"}\r\n```", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23311", + "title": "Regression: LDAP Channel/Role Sync not working", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "23312", + "title": "Regression: Request seats link", + "userLogin": "gabriellsh", + "contributors": [ + "gabriellsh" + ] + } + ] + }, + "3.16.5": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.27.1", + "mongo_versions": [ + "3.4", + "3.6", + "4.0", + "4.2" + ], + "pull_requests": [] + }, + "3.17.3": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.27.1", + "mongo_versions": [ + "3.4", + "3.6", + "4.0", + "4.2" + ], + "pull_requests": [] + }, + "3.18.2": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.27.1", + "mongo_versions": [ + "3.4", + "3.6", + "4.0", + "4.2" + ], + "pull_requests": [ + { + "pr": "23338", + "title": "Release 3.18.2", + "userLogin": "sampaiodiego", + "contributors": [ + "KevLehman", + "sampaiodiego" + ] + }, + { + "pr": "23307", + "title": "Regression: Change some logs to new format", + "userLogin": "KevLehman", + "milestone": "3.18.2", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23280", + "title": "[FIX] Update visitor info on email reception based on current inbox settings", + "userLogin": "KevLehman", + "milestone": "3.18.2", + "contributors": [ + "KevLehman", + "murtaza98", + "web-flow" + ] + } + ] + }, + "4.0.0-rc.5": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0-alpha.5428", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23338", + "title": "Release 3.18.2", + "userLogin": "sampaiodiego", + "contributors": [ + "KevLehman", + "sampaiodiego" + ] + }, + { + "pr": "23307", + "title": "Regression: Change some logs to new format", + "userLogin": "KevLehman", + "milestone": "3.18.2", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23280", + "title": "[FIX] Update visitor info on email reception based on current inbox settings", + "userLogin": "KevLehman", + "milestone": "3.18.2", + "contributors": [ + "KevLehman", + "murtaza98", + "web-flow" + ] + }, + { + "pr": "23135", + "title": "Release 3.18.1", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "KevLehman", + "g-thome" + ] + }, + { + "pr": "23091", + "title": "Regression: Auth banner for EE", + "userLogin": "g-thome", + "description": "Dimisses auth banners assigned to EE admins and prevents new ones from appearing.", + "milestone": "3.18.1", + "contributors": [ + "g-thome", + "casalsgh", + "web-flow" + ] + }, + { + "pr": "23089", + "title": "[FIX] Change way emails are validated on livechat registerGuest method", + "userLogin": "KevLehman", + "milestone": "3.18.1", + "contributors": [ + "KevLehman", + "web-flow" + ] + }, + { + "pr": "23100", + "title": "[IMPROVE] Change HTTP and Method logs to level INFO", + "userLogin": "sampaiodiego", + "milestone": "3.18.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23328", + "title": "Regression: invalid `call` import", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23331", + "title": "Regression: LDAP: Handle base authentication and prevent crash", + "userLogin": "rodrigok", + "description": "When AD requires TLS the auth crashes the server if StartTLS is not set, the error shows at the end because the code was not waiting on this operation.", + "milestone": "4.0.0", + "contributors": [ + "rodrigok", + "pierre-lehnen-rc", + "web-flow" + ] + }, + { + "pr": "23334", + "title": "Regression: invalid `call` import", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "23321", + "title": "Regression: LDAP User Data Sync not always working", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "23333", + "title": "Regression: Removed exclusive tests statement", + "userLogin": "ostjen", + "contributors": [ + "ostjen" + ] + }, + { + "pr": "23322", + "title": "Regression: Blank screen in Jitsi video calls", + "userLogin": "matheusbsilva137", + "description": "- Fix Jitsi calls being disposed even when \"Open in new window\" setting is disabled;\r\n - Fix misspelling on `CallJitsWithData.js` file name.", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "23330", + "title": "Regression: SAML identifier mapping", + "userLogin": "pierre-lehnen-rc", + "contributors": [ + "pierre-lehnen-rc" + ] + } + ] + }, + "4.0.0": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0-alpha.5428", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [] + }, + "4.1.0-rc.0": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23524", + "title": "Chore: Fix some TS warnings", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23521", + "title": "[FIX] Delay start of email inbox", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23495", + "title": "Chore: Make omnichannel settings dependent on omnichannel being enabled", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "23523", + "title": "Chore: Update Livechat Package", + "userLogin": "MartinSchoeler", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "23411", + "title": "[FIX] SAML Users' roles being reset to default on login", + "userLogin": "matheusbsilva137", + "description": "- Remove `roles` field update on `insertOrUpdateSAMLUser` function;\r\n- Add SAML `syncRoles` event;", + "milestone": "4.0.4", + "contributors": [ + "matheusbsilva137", + "pierre-lehnen-rc" + ] + }, + { + "pr": "23522", + "title": "[FIX] Queue error handling and unlocking behavior", + "userLogin": "KevLehman", + "milestone": "4.0.4", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23314", + "title": "[FIX] MONGO_OPTIONS being ignored for oplog connection", + "userLogin": "cuonghuunguyen", + "contributors": [ + "cuonghuunguyen", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "23392", + "title": "[IMPROVE] Allow Omnichannel to handle huge queues ", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman", + "web-flow" + ] + }, + { + "pr": "23515", + "title": "[IMPROVE] Make Livechat Instructions setting multi-line", + "userLogin": "murtaza98", + "description": "Since now we're supporting markdown text on this field (via this PR - https://github.com/RocketChat/Rocket.Chat.Livechat/pull/648), it would be nice to make this setting multiline so users can have more space to edit the text\r\n![image](https://user-images.githubusercontent.com/34130764/138146712-13e4968b-5312-4d53-b44c-b5699c5e49c1.png)", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "23505", + "title": "Chore: Improve watch OAuth settings logic", + "userLogin": "ggazzo", + "description": "Just prevent to perform 200 deletions for registers that not even exist", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "23519", + "title": "Regression: Fix enterprise setting validation", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "23514", + "title": "Chore: Ensure all permissions are created up to this point", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23469", + "title": "[FIX] useEndpointAction replace by useEndpointActionExperimental", + "userLogin": "tiagoevanp", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "23394", + "title": "[FIX] Omni-Webhook's retry mechanism going in infinite loop", + "userLogin": "murtaza98", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "23511", + "title": "Regression: Fix user typings style", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "23510", + "title": "Chore: Update pino and pino-pretty", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23506", + "title": "Regression: Prevent Settings Unit Test Error ", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "23486", + "title": "i18n: Language update from LingoHub 🤖 on 2021-10-18Z", + "userLogin": "lingohub[bot]", + "contributors": [ + null, + "KevLehman" + ] + }, + { + "pr": "23376", + "title": "Bump url-parse from 1.4.7 to 1.5.3", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "23172", + "title": "[FIX] Rewrite missing webRTC feature", + "userLogin": "dougfabris", + "contributors": [ + "dougfabris", + "tassoevan" + ] + }, + { + "pr": "23488", + "title": "Chore: Replace `promises` helper", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23210", + "title": "Chore: Startup Time", + "userLogin": "ggazzo", + "description": "The settings logic has been improved as a whole.\r\n\r\nAll the logic to get the data from the env var was confusing.\r\n\r\nSetting default values was tricky to understand.\r\n\r\nEvery time the server booted, all settings were updated and callbacks were called 2x or more (horrible for environments with multiple instances and generating a turbulent startup).\r\n\r\n`Settings.get(......, callback);` was deprecated. We now have better methods for each case.", + "milestone": "4.1.0", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "23491", + "title": "Chore: Move `isJSON` helper", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23497", + "title": "Update the community open call link in README", + "userLogin": "Sing-Li", + "contributors": [ + "Sing-Li", + "web-flow", + "geekgonecrazy" + ] + }, + { + "pr": "23490", + "title": "Chore: Move `addMinutesToADate` helper", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23489", + "title": "Chore: Move `isEmail` helper", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23228", + "title": "[FIX] Admins can't update or reset user avatars when the \"Allow User Avatar Change\" setting is off", + "userLogin": "matheusbsilva137", + "description": "- Allow admins (or any other user with the `edit-other-user-avatar` permission) to update or reset user avatars even when the \"Allow User Avatar Change\" setting is off.", + "contributors": [ + "matheusbsilva137", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "23473", + "title": "[FIX] Server crashing when Routing method is not available at start", + "userLogin": "KevLehman", + "milestone": "4.0.3", + "contributors": [ + "KevLehman", + "web-flow" + ] + }, + { + "pr": "22949", + "title": "[FIX] Avoid last admin deactivate itself", + "userLogin": "ostjen", + "description": "Co-authored-by: @Kartik18g", + "contributors": [ + "ostjen", + "web-flow", + null + ] + }, + { + "pr": "23418", + "title": "[FIX][APPS] Communication problem when updating and uninstalling apps in cluster", + "userLogin": "thassiov", + "description": "- Make the hook responsible for receiving app update events inside a cluster fetch the app's package (zip file) in the correct place.\r\n- Also shows a warning message on uninstalls inside a cluster. As there are many servers writing to the same place, some race conditions may occur. This prevents problems related to terminating the process in the middle due to errors being thrown and leaving the server in a faulty state.", + "milestone": "4.0.3", + "contributors": [ + "thassiov" + ] + }, + { + "pr": "23462", + "title": "[FIX] Markdown quote message style", + "userLogin": "tiagoevanp", + "description": "Before:\r\n![image](https://user-images.githubusercontent.com/17487063/137496669-3abecab4-cf90-45cb-8b1b-d9411a5682dd.png)\r\n\r\nAfter:\r\n![image](https://user-images.githubusercontent.com/17487063/137496905-fd727f90-f707-4ec6-8139-ba2eb1a2146e.png)", + "contributors": [ + "tiagoevanp" + ] + }, + { + "pr": "22950", + "title": "[NEW] Stream to get individual presence updates", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "23396", + "title": "[FIX] Prevent starting Omni-Queue if Omnichannel is disabled", + "userLogin": "murtaza98", + "description": "Whenever the Routing system setting changes, and omnichannel is disabled, then we shouldn't start the queue.", + "milestone": "4.0.2", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "23404", + "title": "[FIX][ENTERPRISE] Omnichannel agent is not leaving the room when a forwarded chat is queued", + "userLogin": "murtaza98", + "milestone": "4.0.2", + "contributors": [ + "murtaza98", + "web-flow" + ] + }, + { + "pr": "23419", + "title": "Chore: Partially migrate 2FA client code to TypeScript", + "userLogin": "tassoevan", + "description": "Additionally, hides `toastr` behind an module to handle UI's toast notifications.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23342", + "title": "Chore: clean README", + "userLogin": "AbhJ", + "contributors": [ + "AbhJ", + "web-flow" + ] + }, + { + "pr": "23355", + "title": "Chore: Fixed a Typo in 11-admin.js test", + "userLogin": "badbart", + "contributors": [ + "badbart", + "web-flow" + ] + }, + { + "pr": "23405", + "title": "Chore: Document REST API endpoints (DNS)", + "userLogin": "tassoevan", + "description": "Describes endpoints for DNS on REST API using a JSDoc annotation compatible with OpenAPI spec.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23430", + "title": "Chore: Document REST API endpoints (E2E)", + "userLogin": "tassoevan", + "description": "Describes endpoints for end-to-end encryption on REST API using a JSDoc annotation compatible with OpenAPI spec.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23428", + "title": "Chore: Document REST API endpoints (Misc)", + "userLogin": "tassoevan", + "description": "Describes miscellaneous endpoints on REST API using a JSDoc annotation compatible with OpenAPI spec.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "20947", + "title": "[IMPROVE] Add markdown to custom fields in user Info", + "userLogin": "yash-rajpal", + "description": "Added markdown to custom fields to render links", + "contributors": [ + "yash-rajpal", + "dougfabris" + ] + }, + { + "pr": "23393", + "title": "[FIX] user/agent upload not working via Apps Engine after 3.16.0", + "userLogin": "murtaza98", + "description": "Fixes #22974", + "milestone": "4.0.2", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "23377", + "title": "[FIX] Attachment buttons overlap in mobile view", + "userLogin": "Aman-Maheshwari", + "milestone": "4.0.2", + "contributors": [ + "Aman-Maheshwari" + ] + }, + { + "pr": "23378", + "title": "[FIX] Users' `roles` and `type` being reset to default on LDAP DataSync", + "userLogin": "matheusbsilva137", + "description": "- Update `roles` and `type` fields only if they are specified in the data imported from LDAP (otherwise, no changes are applied).", + "milestone": "4.0.1", + "contributors": [ + "matheusbsilva137", + "sampaiodiego" + ] + }, + { + "pr": "23382", + "title": "[FIX] LDAP not stoping after wrong password", + "userLogin": "rodrigok", + "milestone": "4.0.1", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "23381", + "title": "[FIX] MongoDB deprecation link", + "userLogin": "sampaiodiego", + "milestone": "4.0.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23385", + "title": "Chore: Remove dangling README file", + "userLogin": "tassoevan", + "description": "Removes the elderly `server/restapi/README.md`.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23379", + "title": "[FIX] resumeToken not working", + "userLogin": "sampaiodiego", + "milestone": "4.0.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23372", + "title": "[FIX] unwanted toastr error message when deleting user", + "userLogin": "ostjen", + "milestone": "4.0.1", + "contributors": [ + "ostjen", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "23370", + "title": "Chore: Migrate some React components/hooks to TypeScript", + "userLogin": "tassoevan", + "description": "Just low-hanging fruits.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23366", + "title": "[FIX] BigBlueButton integration error due to missing file import", + "userLogin": "wolbernd", + "description": "Fixes BigBlueButton integration", + "milestone": "4.0.1", + "contributors": [ + "wolbernd", + "web-flow" + ] + }, + { + "pr": "23375", + "title": "Chore: Update Apps-Engine version", + "userLogin": "d-gubert", + "milestone": "4.0.1", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "23374", + "title": "[FIX] imported migration v240", + "userLogin": "ostjen", + "milestone": "4.0.1", + "contributors": [ + "ostjen" + ] + }, + { + "pr": "22941", + "title": "[IMPROVE] optimized groups.listAll response time", + "userLogin": "ostjen", + "description": "groups.listAll endpoint was having performance issues, specially when the total number of groups was high. This happened because the endpoint was loading all objects in memory then using splice to paginate, instead of paginating beforehand.\r\n\r\nConsidering 70k groups, this was the performance improvement:\r\n\r\nbefore\r\n![image](https://user-images.githubusercontent.com/28611993/129601314-bdf89337-79fa-4446-9f44-95264af4adb3.png)\r\n\r\nafter\r\n![image](https://user-images.githubusercontent.com/28611993/129601358-5872e166-f923-4c1c-b21d-eb9507365ecf.png)", + "contributors": [ + "ostjen", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "23213", + "title": "[FIX] Read only description in team creation", + "userLogin": "dougfabris", + "description": "![image](https://user-images.githubusercontent.com/27704687/133608433-8ca788a3-71a8-4d40-8c40-8156ab03c606.png)\r\n\r\n![image](https://user-images.githubusercontent.com/27704687/133608400-4cdc7a67-95e5-46c6-8c65-29ab107cd314.png)", + "contributors": [ + "dougfabris" + ] + }, + { + "pr": "23364", + "title": "Chore: Upgrade Storybook", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23360", + "title": "Chore: Move components away from /app/", + "userLogin": "tassoevan", + "description": "We currently do NOT recommend placing React components under `/app`.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23361", + "title": "Chore: Document REST API endpoints (banners)", + "userLogin": "tassoevan", + "description": "Describes endpoints for banners on REST API using a JSDoc annotation compatible with OpenAPI spec.", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "23362", + "title": "Merge master into develop & Set version to 4.1.0-develop", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "ggazzo", + "web-flow" + ] + } + ] + }, + "4.0.1": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23386", + "title": "Release 4.0.1", + "userLogin": "sampaiodiego", + "contributors": [ + "rodrigok", + "sampaiodiego", + "ostjen", + "wolbernd", + "d-gubert", + "matheusbsilva137" + ] + }, + { + "pr": "23378", + "title": "[FIX] Users' `roles` and `type` being reset to default on LDAP DataSync", + "userLogin": "matheusbsilva137", + "description": "- Update `roles` and `type` fields only if they are specified in the data imported from LDAP (otherwise, no changes are applied).", + "milestone": "4.0.1", + "contributors": [ + "matheusbsilva137", + "sampaiodiego" + ] + }, + { + "pr": "23374", + "title": "[FIX] imported migration v240", + "userLogin": "ostjen", + "milestone": "4.0.1", + "contributors": [ + "ostjen" + ] + }, + { + "pr": "23375", + "title": "Chore: Update Apps-Engine version", + "userLogin": "d-gubert", + "milestone": "4.0.1", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "23366", + "title": "[FIX] BigBlueButton integration error due to missing file import", + "userLogin": "wolbernd", + "description": "Fixes BigBlueButton integration", + "milestone": "4.0.1", + "contributors": [ + "wolbernd", + "web-flow" + ] + }, + { + "pr": "23372", + "title": "[FIX] unwanted toastr error message when deleting user", + "userLogin": "ostjen", + "milestone": "4.0.1", + "contributors": [ + "ostjen", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "23379", + "title": "[FIX] resumeToken not working", + "userLogin": "sampaiodiego", + "milestone": "4.0.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23381", + "title": "[FIX] MongoDB deprecation link", + "userLogin": "sampaiodiego", + "milestone": "4.0.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23382", + "title": "[FIX] LDAP not stoping after wrong password", + "userLogin": "rodrigok", + "milestone": "4.0.1", + "contributors": [ + "rodrigok" + ] + } + ] + }, + "4.0.2": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23460", + "title": "Release 4.0.2", + "userLogin": "sampaiodiego", + "contributors": [ + "murtaza98", + "sampaiodiego", + "Aman-Maheshwari" + ] + }, + { + "pr": "23377", + "title": "[FIX] Attachment buttons overlap in mobile view", + "userLogin": "Aman-Maheshwari", + "milestone": "4.0.2", + "contributors": [ + "Aman-Maheshwari" + ] + }, + { + "pr": "23393", + "title": "[FIX] user/agent upload not working via Apps Engine after 3.16.0", + "userLogin": "murtaza98", + "description": "Fixes #22974", + "milestone": "4.0.2", + "contributors": [ + "murtaza98" + ] + }, + { + "pr": "23404", + "title": "[FIX][ENTERPRISE] Omnichannel agent is not leaving the room when a forwarded chat is queued", + "userLogin": "murtaza98", + "milestone": "4.0.2", + "contributors": [ + "murtaza98", + "web-flow" + ] + }, + { + "pr": "23396", + "title": "[FIX] Prevent starting Omni-Queue if Omnichannel is disabled", + "userLogin": "murtaza98", + "description": "Whenever the Routing system setting changes, and omnichannel is disabled, then we shouldn't start the queue.", + "milestone": "4.0.2", + "contributors": [ + "murtaza98" + ] + } + ] + }, + "4.0.3": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23496", + "title": "Release 4.0.3", + "userLogin": "sampaiodiego", + "contributors": [ + "KevLehman", + "sampaiodiego", + "thassiov" + ] + }, + { + "pr": "23418", + "title": "[FIX][APPS] Communication problem when updating and uninstalling apps in cluster", + "userLogin": "thassiov", + "description": "- Make the hook responsible for receiving app update events inside a cluster fetch the app's package (zip file) in the correct place.\r\n- Also shows a warning message on uninstalls inside a cluster. As there are many servers writing to the same place, some race conditions may occur. This prevents problems related to terminating the process in the middle due to errors being thrown and leaving the server in a faulty state.", + "milestone": "4.0.3", + "contributors": [ + "thassiov" + ] + }, + { + "pr": "23473", + "title": "[FIX] Server crashing when Routing method is not available at start", + "userLogin": "KevLehman", + "milestone": "4.0.3", + "contributors": [ + "KevLehman", + "web-flow" + ] + } + ] + }, + "4.1.0-rc.1": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23531", + "title": "Regression: Waiting_queue setting not being applied due to missing module key", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23528", + "title": "Regression: Settings order", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "23529", + "title": "Regression: watchByRegex without Fibers", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + } + ] + }, + "4.0.4": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23532", + "title": "Release 4.0.4", + "userLogin": "sampaiodiego", + "contributors": [ + "KevLehman", + "sampaiodiego", + "matheusbsilva137" + ] + }, + { + "pr": "23411", + "title": "[FIX] SAML Users' roles being reset to default on login", + "userLogin": "matheusbsilva137", + "description": "- Remove `roles` field update on `insertOrUpdateSAMLUser` function;\r\n- Add SAML `syncRoles` event;", + "milestone": "4.0.4", + "contributors": [ + "matheusbsilva137", + "pierre-lehnen-rc" + ] + }, + { + "pr": "23522", + "title": "[FIX] Queue error handling and unlocking behavior", + "userLogin": "KevLehman", + "milestone": "4.0.4", + "contributors": [ + "KevLehman" + ] + } + ] + }, + "4.0.5": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23554", + "title": "Release 4.0.5", + "userLogin": "sampaiodiego", + "contributors": [ + "pierre-lehnen-rc", + "sampaiodiego" + ] + }, + { + "pr": "23541", + "title": "[FIX] OAuth login not working on mobile app", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.0.5", + "contributors": [ + "pierre-lehnen-rc" + ] + } + ] + }, + "4.1.0-rc.2": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23552", + "title": "Regression: Mail body contains `undefined` text", + "userLogin": "tassoevan", + "description": "### Before\r\n![image](https://user-images.githubusercontent.com/2263066/138733018-10449892-5c2d-46fb-9355-00e98e0d6c9f.png)\r\n\r\n### After\r\n![image](https://user-images.githubusercontent.com/2263066/138733074-a1b88a77-bf64-41c3-a6c3-ac9e1cb63de1.png)", + "contributors": [ + "tassoevan", + "sampaiodiego" + ] + }, + { + "pr": "23541", + "title": "[FIX] OAuth login not working on mobile app", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.0.5", + "contributors": [ + "pierre-lehnen-rc" + ] + } + ] + }, + "4.1.0-rc.3": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23556", + "title": "Regression: Prevent settings from getting updated", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "23568", + "title": "Regression: Routing method not available when called from listeners at startup", + "userLogin": "KevLehman", + "milestone": "4.1.0", + "contributors": [ + "KevLehman" + ] + }, + { + "pr": "23391", + "title": "Bump: fuselage 0.30.1", + "userLogin": "ggazzo", + "contributors": [ + "dougfabris" + ] + } + ] + }, + "4.1.0-rc.4": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23577", + "title": "Regression: Debounce call based on params on omnichannel queue dispatch", + "userLogin": "KevLehman", + "contributors": [ + "KevLehman" + ] + } + ] + }, + "4.1.0": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [] + }, + "4.1.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": "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": "23627", + "title": "[FIX] LDAP users not being re-activated on login", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.1.1", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "23608", + "title": "[FIX] Advanced LDAP Sync Features", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.1.1", + "contributors": [ + "pierre-lehnen-rc", + "web-flow" + ] + } + ] + }, + "3.18.3": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.27.1", + "mongo_versions": [ + "3.4", + "3.6", + "4.0", + "4.2" + ], + "pull_requests": [] + }, + "4.0.6": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [] + }, + "4.1.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": "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": "23661", + "title": "[FIX] Performance issues when running Omnichannel job queue dispatcher", + "userLogin": "renatobecker", + "milestone": "4.1.2", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "23587", + "title": "[FIX] Omnichannel status being changed on page refresh", + "userLogin": "KevLehman", + "milestone": "4.1.2", + "contributors": [ + "KevLehman" + ] + } + ] + }, + "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.4", "3.6", "4.0", - "4.2" + "4.2", + "4.4", + "5.0" ], "pull_requests": [ { - "pr": "23091", - "title": "Regression: Auth banner for EE", - "userLogin": "g-thome", - "description": "Dimisses auth banners assigned to EE admins and prevents new ones from appearing.", - "milestone": "3.18.1", + "pr": "23778", + "title": "Regression: Fix incorrect API path for livechat calls", + "userLogin": "murtaza98", + "milestone": "4.2.0", "contributors": [ - "g-thome", - "casalsgh", - "web-flow" + "murtaza98" ] }, { - "pr": "23089", - "title": "[FIX] Change way emails are validated on livechat registerGuest method", - "userLogin": "KevLehman", - "milestone": "3.18.1", + "pr": "23775", + "title": "Regression: Fix LDAP sync route", + "userLogin": "ggazzo", "contributors": [ - "KevLehman", - "web-flow" + "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": "23100", - "title": "[IMPROVE] Change HTTP and Method logs to level INFO", + "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", - "milestone": "3.18.1", "contributors": [ "sampaiodiego" ] } ] }, - "3.18.2": { + "4.2.0-rc.4": { "node_version": "12.22.1", "npm_version": "6.14.1", - "apps_engine_version": "1.27.1", + "apps_engine_version": "1.28.1", "mongo_versions": [ - "3.4", "3.6", "4.0", - "4.2" + "4.2", + "4.4", + "5.0" ], "pull_requests": [ { - "pr": "23307", - "title": "Regression: Change some logs to new format", - "userLogin": "KevLehman", - "milestone": "3.18.2", + "pr": "23774", + "title": "Regression: Add trash to raw models", + "userLogin": "sampaiodiego", + "milestone": "4.2.0", "contributors": [ - "KevLehman" + "sampaiodiego", + "ggazzo" ] }, { - "pr": "23280", - "title": "[FIX] Update visitor info on email reception based on current inbox settings", - "userLogin": "KevLehman", - "milestone": "3.18.2", + "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": [ - "KevLehman", "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/pr-title-checker-config.json b/.github/pr-title-checker-config.json index 905cb66a34a00..3b992e8918840 100644 --- a/.github/pr-title-checker-config.json +++ b/.github/pr-title-checker-config.json @@ -1,10 +1,10 @@ { "LABEL": { - "name": "Invalid PR Title", - "color": "B60205" + "name": "Invalid PR Title", + "color": "B60205" }, "CHECKS": { - "regexp": "^(?:(?:\\[(NEW|BREAK|IMPROVE|FIX)\\](\\[(ENTERPRISE|APPS)\\])?|(?:Regression|Chore|Bump|Revert|i18n):) .+|Release [0-9]+\\.[0-9]+\\.[0-9]+|Merge master into develop)", - "ignoreLabels" : ["[ignore-title]"] + "regexp": "^(?:(?:\\[(NEW|BREAK|IMPROVE|FIX)\\](\\[(ENTERPRISE|APPS)\\])?|(?:Regression|Chore|Revert|i18n):)|(?:Bump) .+|Release [0-9]+\\.[0-9]+\\.[0-9]+|Merge master into develop)", + "ignoreLabels" : ["[ignore-title]"] } - } +} diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index b846d006dadb0..be8b2b3941096 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -16,7 +16,7 @@ env: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: @@ -117,13 +117,6 @@ jobs: - run: meteor npm run translation-check - - name: Launch MongoDB - uses: wbari/start-mongoDB@v0.2 - with: - mongoDBVersion: "4.0" - - - run: meteor npm run testunit - - run: meteor npm run typecheck - name: Build Storybook to sanity check components @@ -134,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' @@ -149,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 @@ -192,7 +187,7 @@ jobs: strategy: matrix: node-version: ["12.22.1"] - mongodb-version: ["3.4", "3.6", "4.0", "4.2"] + mongodb-version: ["3.6", "4.0", "4.2", "4.4","5.0"] steps: - name: Launch MongoDB @@ -249,7 +244,16 @@ jobs: run: | npm install - - name: Test + - 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" MONGO_URL: mongodb://localhost:27017/rocketchat @@ -260,7 +264,7 @@ jobs: for i in $(seq 1 5); do (docker exec mongo mongo rocketchat --eval 'db.dropDatabase()') && npm run testci && s=0 && break || s=$? && sleep 1; done; (exit $s) # notification: -# runs-on: ubuntu-latest +# runs-on: ubuntu-20.04 # needs: test # steps: @@ -274,7 +278,7 @@ jobs: # token: ${{ secrets.GITHUB_TOKEN }} build-image-pr: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 if: github.event.pull_request.head.repo.full_name == github.repository strategy: @@ -362,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: | @@ -401,7 +405,7 @@ jobs: docker push $IMAGE_NAME deploy: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 if: github.event_name == 'release' || github.ref == 'refs/heads/develop' needs: test @@ -479,7 +483,7 @@ jobs: fi image-build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: deploy strategy: @@ -556,7 +560,7 @@ jobs: docker push ${IMAGE}:develop services-image-build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: deploy strategy: diff --git a/.github/workflows/pr-title-checker.yml b/.github/workflows/pr-title-checker.yml index 4cd045587a3e3..c31a3aef54d28 100644 --- a/.github/workflows/pr-title-checker.yml +++ b/.github/workflows/pr-title-checker.yml @@ -7,6 +7,6 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: thehanimo/pr-title-checker@v1.2 + - uses: thehanimo/pr-title-checker@v1.3.4 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index bd7459c7025f1..b7970cf1bf5b5 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -8,7 +8,7 @@ jobs: no-response: runs-on: ubuntu-latest steps: - - uses: actions/stale@v3.0.19 + - uses: actions/stale@v4 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 10 @@ -26,7 +26,7 @@ jobs: # stale: # runs-on: ubuntu-latest # steps: -# - uses: actions/stale@v3.0.19 +# - uses: actions/stale@v4 # with: # repo-token: ${{ secrets.GITHUB_TOKEN }} # days-before-stale: 60 diff --git a/.husky/pre-push b/.husky/pre-push index 5ccfd361beed1..3c9fedc8460ae 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,2 +1,3 @@ meteor npm run lint && \ -meteor npm run testunit -- --exclude app/models/server/models/Sessions.tests.js +meteor npm run testunit && \ +meteor npm run testunit-client diff --git a/.meteor/packages b/.meteor/packages index 12471574ad2b9..7ce41971bcc17 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -21,7 +21,6 @@ ecmascript@0.15.1 typescript@4.2.2 ejson@1.1.1 email@2.0.0 -fastclick@1.0.13 http@1.4.2 logging@1.2.0 meteor-base@1.4.0 diff --git a/.meteor/versions b/.meteor/versions index 70a4d6140cc33..1a2fef729ad12 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -43,7 +43,6 @@ email@2.0.0 es5-shim@4.8.0 facebook-oauth@1.8.0 facts-base@1.0.1 -fastclick@1.0.13 fetch@0.1.1 geojson-utils@1.0.10 github-oauth@1.2.3 @@ -95,7 +94,7 @@ mystor:device-detection@0.2.0 nimble:restivus@0.8.12 nooitaf:colors@1.1.2_1 npm-bcrypt@0.9.4 -npm-mongo@3.9.0 +npm-mongo@3.9.1 oauth@1.3.2 oauth1@1.3.0 oauth2@1.3.0 diff --git a/.mocharc.api.js b/.mocharc.api.js new file mode 100644 index 0000000000000..cef49fb74933a --- /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 0000000000000..ac8a2bcce8b7f --- /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 0000000000000..e4279a9a63565 --- /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 0000000000000..efffe16964d5c --- /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 bd3bd56e3c0ed..a71a3020cf4ba 100644 --- a/.mocharc.js +++ b/.mocharc.js @@ -1,17 +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', 'server/**/*.tests.ts', - 'client/**/*.spec.ts', ], }; diff --git a/.scripts/make-migration.ts b/.scripts/make-migration.ts new file mode 100644 index 0000000000000..d9df274539af8 --- /dev/null +++ b/.scripts/make-migration.ts @@ -0,0 +1,50 @@ +import { readdirSync, readFileSync, writeFileSync } from 'fs'; + +import { renderFile } from 'template-file'; + +function main(number: string, comment: string): void { + if (!(Number(number) >= 0)) { + console.error(`1st param must be a valid number. ${ number } provided`); + return; + } + + if (comment.trim()) { + comment = `// ${ comment }`; + } + + // check if migration will conflict with current on-branch migrations + const migrationName = `v${ number }`; + const fileList = readdirSync('./server/startup/migrations'); + if (fileList.includes(`${ migrationName }.ts`)) { + console.error('Migration with specified number already exists'); + return; + } + + renderFile('./.scripts/migration.template', { number, comment }) + .then((renderedMigration) => { + // generate new migration file + writeFileSync(`./server/startup/migrations/${ migrationName }.ts`, renderedMigration); + + // get contents of index.ts to append new migration + const indexFile = readFileSync('./server/startup/migrations/index.ts'); + const splittedIndexLines = indexFile.toString().split('\n'); + + // remove end line + xrun import + splittedIndexLines.splice(splittedIndexLines.length - 2, 0, `import './${ migrationName }';`); + const data = splittedIndexLines.join('\n'); + + // append migration import to indexfile + writeFileSync('./server/startup/migrations/index.ts', data); + console.log(`Migration ${ migrationName } created`); + }) + .catch(console.error); +} + +const [, , number, comment = ''] = process.argv; + +if (!number || (comment && !comment.trim())) { + console.error('Usage:\n\tmeteor npm run migration:add [migration comment: optional]\n'); + process.exit(1); +} + +main(number, comment); diff --git a/.scripts/migration.template b/.scripts/migration.template new file mode 100644 index 0000000000000..0b1ba7b550e2c --- /dev/null +++ b/.scripts/migration.template @@ -0,0 +1,9 @@ +import { addMigration } from '../../lib/migrations'; + +{{ comment }} +addMigration({ + version: {{ number }}, + up() { + + }, +}); diff --git a/.snapcraft/resources/prepareRocketChat b/.snapcraft/resources/prepareRocketChat index 92bdb1d67c780..2573d0855ee3a 100755 --- a/.snapcraft/resources/prepareRocketChat +++ b/.snapcraft/resources/prepareRocketChat @@ -1,6 +1,6 @@ #!/bin/bash -curl -SLf "https://releases.rocket.chat/3.18.2/download/" -o rocket.chat.tgz +curl -SLf "https://releases.rocket.chat/4.3.0-develop/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 9c07110867ead..0f18d6ee45e23 100644 --- a/.snapcraft/snap/snapcraft.yaml +++ b/.snapcraft/snap/snapcraft.yaml @@ -7,7 +7,7 @@ # 5. `snapcraft snap` name: rocketchat-server -version: 3.18.2 +version: 4.3.0-develop 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 0000000000000..8589dc8c53248 --- /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 0000000000000..4031483e531f1 --- /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 01b7b4f93cb78..9314684c128f3 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 0000000000000..ca0d1db71f7f5 --- /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 7ac1da4c927f6..58531e40b22b2 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 e4746cb59f0e0..ef22c95f67a7e 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.tsx b/.storybook/mocks/providers.tsx deleted file mode 100644 index 31a66433c753d..0000000000000 --- a/.storybook/mocks/providers.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import i18next from 'i18next'; -import React, { PropsWithChildren, ReactElement } from 'react'; - -import { TranslationContext, TranslationContextValue } from '../../client/contexts/TranslationContext'; -import ServerProvider from '../../client/providers/ServerProvider'; - -let contextValue: TranslationContextValue; - -const getContextValue = (): TranslationContextValue => { - if (contextValue) { - return contextValue; - } - - i18next.init({ - fallbackLng: 'en', - defaultNS: 'project', - resources: { - en: { - project: require('../../packages/rocketchat-i18n/i18n/en.i18n.json'), - }, - }, - interpolation: { - prefix: '__', - suffix: '__', - }, - initImmediate: false, - }); - - const translate = (key: string, ...replaces: unknown[]): string => { - if (typeof replaces[0] === 'object' && replaces[0] !== null) { - const [options] = replaces; - return i18next.t(key, options); - } - - if (replaces.length === 0) { - return i18next.t(key); - } - - return i18next.t(key, { - postProcess: 'sprintf', - sprintf: replaces, - }); - }; - - translate.has = (key: string): boolean => !!key && i18next.exists(key); - - contextValue = { - languages: [{ - name: 'English', - en: 'English', - key: 'en', - }], - language: 'en', - translate, - loadLanguage: async (): Promise => undefined, - }; - - return contextValue; -}; - -function TranslationProviderMock({ children }: PropsWithChildren<{}>): ReactElement { - return ; -} - -export function MeteorProviderMock({ children }: PropsWithChildren<{}>): ReactElement { - return - - {children} - - ; -} diff --git a/.storybook/mocks/providers/QueryClientProviderMock.tsx b/.storybook/mocks/providers/QueryClientProviderMock.tsx new file mode 100644 index 0000000000000..d44ea0e9d0798 --- /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 0000000000000..7cc9a273b0651 --- /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/index.tsx b/.storybook/mocks/providers/index.tsx new file mode 100644 index 0000000000000..9cecb32ff8b94 --- /dev/null +++ b/.storybook/mocks/providers/index.tsx @@ -0,0 +1,72 @@ +import i18next from 'i18next'; +import React, { PropsWithChildren, ReactElement } from 'react'; + +import { + TranslationContext, + TranslationContextValue, +} from '../../../client/contexts/TranslationContext'; + +let contextValue: TranslationContextValue; + +const getContextValue = (): TranslationContextValue => { + if (contextValue) { + return contextValue; + } + + i18next.init({ + fallbackLng: 'en', + defaultNS: 'project', + resources: { + en: { + project: require('../../../packages/rocketchat-i18n/i18n/en.i18n.json'), + }, + }, + interpolation: { + prefix: '__', + suffix: '__', + }, + initImmediate: false, + }); + + const translate = (key: string, ...replaces: unknown[]): string => { + if (typeof replaces[0] === 'object' && replaces[0] !== null) { + const [options] = replaces; + return i18next.t(key, options); + } + + if (replaces.length === 0) { + return i18next.t(key); + } + + return i18next.t(key, { + postProcess: 'sprintf', + sprintf: replaces, + }); + }; + + translate.has = (key: string): boolean => !!key && i18next.exists(key); + + contextValue = { + languages: [ + { + name: 'English', + en: 'English', + key: 'en', + }, + ], + language: 'en', + translate, + loadLanguage: async (): Promise => undefined, + }; + + return contextValue; +}; + +function TranslationProviderMock({ children }: PropsWithChildren<{}>): ReactElement { + return ; +} + +// eslint-disable-next-line react/no-multi-comp +export function MeteorProviderMock({ children }: PropsWithChildren<{}>): ReactElement { + return {children}; +} diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 2839f538c25ef..ab9bd5a3220d1 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 ef3164583e009..453efbf632d33 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,1243 @@ +# 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 👩‍💻👨‍💻` + +### 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` + +### 🐛 Bug fixes + + +- 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 status being changed on page refresh ([#23587](https://github.com/RocketChat/Rocket.Chat/pull/23587)) + +- Performance issues when running Omnichannel job queue dispatcher ([#23661](https://github.com/RocketChat/Rocket.Chat/pull/23661)) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@matheusbsilva137](https://github.com/matheusbsilva137) +- [@renatobecker](https://github.com/renatobecker) + +# 4.1.1 +`2021-11-05 · 4 🐛 · 3 👩‍💻👨‍💻` + +### 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` + +### 🐛 Bug fixes + + +- 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 + +- LDAP users not being re-activated on login ([#23627](https://github.com/RocketChat/Rocket.Chat/pull/23627)) + +- Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@d-gubert](https://github.com/d-gubert) +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 4.1.0 +`2021-10-28 · 1 🎉 · 4 🚀 · 25 🐛 · 38 🔍 · 23 👩‍💻👨‍💻` + +### 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.0` + +### 🎉 New features + + +- Stream to get individual presence updates ([#22950](https://github.com/RocketChat/Rocket.Chat/pull/22950)) + +### 🚀 Improvements + + +- 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 + +- Allow Omnichannel to handle huge queues ([#23392](https://github.com/RocketChat/Rocket.Chat/pull/23392)) + +- Make Livechat Instructions setting multi-line ([#23515](https://github.com/RocketChat/Rocket.Chat/pull/23515)) + + Since now we're supporting markdown text on this field (via this PR - https://github.com/RocketChat/Rocket.Chat.Livechat/pull/648), it would be nice to make this setting multiline so users can have more space to edit the text + ![image](https://user-images.githubusercontent.com/34130764/138146712-13e4968b-5312-4d53-b44c-b5699c5e49c1.png) + +- optimized groups.listAll response time ([#22941](https://github.com/RocketChat/Rocket.Chat/pull/22941)) + + groups.listAll endpoint was having performance issues, specially when the total number of groups was high. This happened because the endpoint was loading all objects in memory then using splice to paginate, instead of paginating beforehand. + + Considering 70k groups, this was the performance improvement: + + before + ![image](https://user-images.githubusercontent.com/28611993/129601314-bdf89337-79fa-4446-9f44-95264af4adb3.png) + + after + ![image](https://user-images.githubusercontent.com/28611993/129601358-5872e166-f923-4c1c-b21d-eb9507365ecf.png) + +### 🐛 Bug fixes + + +- **APPS:** Communication problem when updating and uninstalling apps in cluster ([#23418](https://github.com/RocketChat/Rocket.Chat/pull/23418)) + + - Make the hook responsible for receiving app update events inside a cluster fetch the app's package (zip file) in the correct place. + - Also shows a warning message on uninstalls inside a cluster. As there are many servers writing to the same place, some race conditions may occur. This prevents problems related to terminating the process in the middle due to errors being thrown and leaving the server in a faulty state. + +- **ENTERPRISE:** Omnichannel agent is not leaving the room when a forwarded chat is queued ([#23404](https://github.com/RocketChat/Rocket.Chat/pull/23404)) + +- Admins can't update or reset user avatars when the "Allow User Avatar Change" setting is off ([#23228](https://github.com/RocketChat/Rocket.Chat/pull/23228)) + + - Allow admins (or any other user with the `edit-other-user-avatar` permission) to update or reset user avatars even when the "Allow User Avatar Change" setting is off. + +- Attachment buttons overlap in mobile view ([#23377](https://github.com/RocketChat/Rocket.Chat/pull/23377) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari)) + +- Avoid last admin deactivate itself ([#22949](https://github.com/RocketChat/Rocket.Chat/pull/22949)) + + Co-authored-by: @Kartik18g + +- BigBlueButton integration error due to missing file import ([#23366](https://github.com/RocketChat/Rocket.Chat/pull/23366) by [@wolbernd](https://github.com/wolbernd)) + + Fixes BigBlueButton integration + +- Delay start of email inbox ([#23521](https://github.com/RocketChat/Rocket.Chat/pull/23521)) + +- imported migration v240 ([#23374](https://github.com/RocketChat/Rocket.Chat/pull/23374)) + +- LDAP not stoping after wrong password ([#23382](https://github.com/RocketChat/Rocket.Chat/pull/23382)) + +- Markdown quote message style ([#23462](https://github.com/RocketChat/Rocket.Chat/pull/23462)) + + Before: + ![image](https://user-images.githubusercontent.com/17487063/137496669-3abecab4-cf90-45cb-8b1b-d9411a5682dd.png) + + After: + ![image](https://user-images.githubusercontent.com/17487063/137496905-fd727f90-f707-4ec6-8139-ba2eb1a2146e.png) + +- MONGO_OPTIONS being ignored for oplog connection ([#23314](https://github.com/RocketChat/Rocket.Chat/pull/23314) by [@cuonghuunguyen](https://github.com/cuonghuunguyen)) + +- MongoDB deprecation link ([#23381](https://github.com/RocketChat/Rocket.Chat/pull/23381)) + +- OAuth login not working on mobile app ([#23541](https://github.com/RocketChat/Rocket.Chat/pull/23541)) + +- Omni-Webhook's retry mechanism going in infinite loop ([#23394](https://github.com/RocketChat/Rocket.Chat/pull/23394)) + +- Prevent starting Omni-Queue if Omnichannel is disabled ([#23396](https://github.com/RocketChat/Rocket.Chat/pull/23396)) + + Whenever the Routing system setting changes, and omnichannel is disabled, then we shouldn't start the queue. + +- Queue error handling and unlocking behavior ([#23522](https://github.com/RocketChat/Rocket.Chat/pull/23522)) + +- Read only description in team creation ([#23213](https://github.com/RocketChat/Rocket.Chat/pull/23213)) + + ![image](https://user-images.githubusercontent.com/27704687/133608433-8ca788a3-71a8-4d40-8c40-8156ab03c606.png) + + ![image](https://user-images.githubusercontent.com/27704687/133608400-4cdc7a67-95e5-46c6-8c65-29ab107cd314.png) + +- resumeToken not working ([#23379](https://github.com/RocketChat/Rocket.Chat/pull/23379)) + +- Rewrite missing webRTC feature ([#23172](https://github.com/RocketChat/Rocket.Chat/pull/23172)) + +- SAML Users' roles being reset to default on login ([#23411](https://github.com/RocketChat/Rocket.Chat/pull/23411)) + + - Remove `roles` field update on `insertOrUpdateSAMLUser` function; + - Add SAML `syncRoles` event; + +- Server crashing when Routing method is not available at start ([#23473](https://github.com/RocketChat/Rocket.Chat/pull/23473)) + +- unwanted toastr error message when deleting user ([#23372](https://github.com/RocketChat/Rocket.Chat/pull/23372)) + +- useEndpointAction replace by useEndpointActionExperimental ([#23469](https://github.com/RocketChat/Rocket.Chat/pull/23469)) + +- user/agent upload not working via Apps Engine after 3.16.0 ([#23393](https://github.com/RocketChat/Rocket.Chat/pull/23393)) + + Fixes #22974 + +- Users' `roles` and `type` being reset to default on LDAP DataSync ([#23378](https://github.com/RocketChat/Rocket.Chat/pull/23378)) + + - Update `roles` and `type` fields only if they are specified in the data imported from LDAP (otherwise, no changes are applied). + +
+🔍 Minor changes + + +- Bump url-parse from 1.4.7 to 1.5.3 ([#23376](https://github.com/RocketChat/Rocket.Chat/pull/23376) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump: fuselage 0.30.1 ([#23391](https://github.com/RocketChat/Rocket.Chat/pull/23391)) + +- Chore: clean README ([#23342](https://github.com/RocketChat/Rocket.Chat/pull/23342) by [@AbhJ](https://github.com/AbhJ)) + +- Chore: Document REST API endpoints (banners) ([#23361](https://github.com/RocketChat/Rocket.Chat/pull/23361)) + + Describes endpoints for banners on REST API using a JSDoc annotation compatible with OpenAPI spec. + +- Chore: Document REST API endpoints (DNS) ([#23405](https://github.com/RocketChat/Rocket.Chat/pull/23405)) + + Describes endpoints for DNS on REST API using a JSDoc annotation compatible with OpenAPI spec. + +- Chore: Document REST API endpoints (E2E) ([#23430](https://github.com/RocketChat/Rocket.Chat/pull/23430)) + + Describes endpoints for end-to-end encryption on REST API using a JSDoc annotation compatible with OpenAPI spec. + +- Chore: Document REST API endpoints (Misc) ([#23428](https://github.com/RocketChat/Rocket.Chat/pull/23428)) + + Describes miscellaneous endpoints on REST API using a JSDoc annotation compatible with OpenAPI spec. + +- Chore: Ensure all permissions are created up to this point ([#23514](https://github.com/RocketChat/Rocket.Chat/pull/23514)) + +- Chore: Fix some TS warnings ([#23524](https://github.com/RocketChat/Rocket.Chat/pull/23524)) + +- Chore: Fixed a Typo in 11-admin.js test ([#23355](https://github.com/RocketChat/Rocket.Chat/pull/23355) by [@badbart](https://github.com/badbart)) + +- Chore: Improve watch OAuth settings logic ([#23505](https://github.com/RocketChat/Rocket.Chat/pull/23505)) + + Just prevent to perform 200 deletions for registers that not even exist + +- Chore: Make omnichannel settings dependent on omnichannel being enabled ([#23495](https://github.com/RocketChat/Rocket.Chat/pull/23495)) + +- Chore: Migrate some React components/hooks to TypeScript ([#23370](https://github.com/RocketChat/Rocket.Chat/pull/23370)) + + Just low-hanging fruits. + +- Chore: Move `addMinutesToADate` helper ([#23490](https://github.com/RocketChat/Rocket.Chat/pull/23490)) + +- Chore: Move `isEmail` helper ([#23489](https://github.com/RocketChat/Rocket.Chat/pull/23489)) + +- Chore: Move `isJSON` helper ([#23491](https://github.com/RocketChat/Rocket.Chat/pull/23491)) + +- Chore: Move components away from /app/ ([#23360](https://github.com/RocketChat/Rocket.Chat/pull/23360)) + + We currently do NOT recommend placing React components under `/app`. + +- Chore: Partially migrate 2FA client code to TypeScript ([#23419](https://github.com/RocketChat/Rocket.Chat/pull/23419)) + + Additionally, hides `toastr` behind an module to handle UI's toast notifications. + +- Chore: Remove dangling README file ([#23385](https://github.com/RocketChat/Rocket.Chat/pull/23385)) + + Removes the elderly `server/restapi/README.md`. + +- Chore: Replace `promises` helper ([#23488](https://github.com/RocketChat/Rocket.Chat/pull/23488)) + +- Chore: Startup Time ([#23210](https://github.com/RocketChat/Rocket.Chat/pull/23210)) + + The settings logic has been improved as a whole. + + All the logic to get the data from the env var was confusing. + + Setting default values was tricky to understand. + + Every time the server booted, all settings were updated and callbacks were called 2x or more (horrible for environments with multiple instances and generating a turbulent startup). + + `Settings.get(......, callback);` was deprecated. We now have better methods for each case. + +- Chore: Update Apps-Engine version ([#23375](https://github.com/RocketChat/Rocket.Chat/pull/23375)) + +- Chore: Update Livechat Package ([#23523](https://github.com/RocketChat/Rocket.Chat/pull/23523)) + +- Chore: Update pino and pino-pretty ([#23510](https://github.com/RocketChat/Rocket.Chat/pull/23510)) + +- Chore: Upgrade Storybook ([#23364](https://github.com/RocketChat/Rocket.Chat/pull/23364)) + +- i18n: Language update from LingoHub 🤖 on 2021-10-18Z ([#23486](https://github.com/RocketChat/Rocket.Chat/pull/23486)) + +- Merge master into develop & Set version to 4.1.0-develop ([#23362](https://github.com/RocketChat/Rocket.Chat/pull/23362)) + +- Regression: Debounce call based on params on omnichannel queue dispatch ([#23577](https://github.com/RocketChat/Rocket.Chat/pull/23577)) + +- Regression: Fix enterprise setting validation ([#23519](https://github.com/RocketChat/Rocket.Chat/pull/23519)) + +- Regression: Fix user typings style ([#23511](https://github.com/RocketChat/Rocket.Chat/pull/23511)) + +- Regression: Mail body contains `undefined` text ([#23552](https://github.com/RocketChat/Rocket.Chat/pull/23552)) + + ### Before + ![image](https://user-images.githubusercontent.com/2263066/138733018-10449892-5c2d-46fb-9355-00e98e0d6c9f.png) + + ### After + ![image](https://user-images.githubusercontent.com/2263066/138733074-a1b88a77-bf64-41c3-a6c3-ac9e1cb63de1.png) + +- Regression: Prevent settings from getting updated ([#23556](https://github.com/RocketChat/Rocket.Chat/pull/23556)) + +- Regression: Prevent Settings Unit Test Error ([#23506](https://github.com/RocketChat/Rocket.Chat/pull/23506)) + +- Regression: Routing method not available when called from listeners at startup ([#23568](https://github.com/RocketChat/Rocket.Chat/pull/23568)) + +- Regression: Settings order ([#23528](https://github.com/RocketChat/Rocket.Chat/pull/23528)) + +- Regression: Waiting_queue setting not being applied due to missing module key ([#23531](https://github.com/RocketChat/Rocket.Chat/pull/23531)) + +- Regression: watchByRegex without Fibers ([#23529](https://github.com/RocketChat/Rocket.Chat/pull/23529)) + +- Update the community open call link in README ([#23497](https://github.com/RocketChat/Rocket.Chat/pull/23497)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@AbhJ](https://github.com/AbhJ) +- [@Aman-Maheshwari](https://github.com/Aman-Maheshwari) +- [@badbart](https://github.com/badbart) +- [@cuonghuunguyen](https://github.com/cuonghuunguyen) +- [@dependabot[bot]](https://github.com/dependabot[bot]) +- [@wolbernd](https://github.com/wolbernd) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@Sing-Li](https://github.com/Sing-Li) +- [@d-gubert](https://github.com/d-gubert) +- [@dougfabris](https://github.com/dougfabris) +- [@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) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@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 👩‍💻👨‍💻` + +### 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.0` + +### 🐛 Bug fixes + + +- OAuth login not working on mobile app ([#23541](https://github.com/RocketChat/Rocket.Chat/pull/23541)) + +
+🔍 Minor changes + + +- Release 4.0.5 ([#23554](https://github.com/RocketChat/Rocket.Chat/pull/23554)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 4.0.4 +`2021-10-21 · 2 🐛 · 1 🔍 · 4 👩‍💻👨‍💻` + +### 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.0` + +### 🐛 Bug fixes + + +- Queue error handling and unlocking behavior ([#23522](https://github.com/RocketChat/Rocket.Chat/pull/23522)) + +- SAML Users' roles being reset to default on login ([#23411](https://github.com/RocketChat/Rocket.Chat/pull/23411)) + + - Remove `roles` field update on `insertOrUpdateSAMLUser` function; + - Add SAML `syncRoles` event; + +
+🔍 Minor changes + + +- Release 4.0.4 ([#23532](https://github.com/RocketChat/Rocket.Chat/pull/23532)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@matheusbsilva137](https://github.com/matheusbsilva137) +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 4.0.3 +`2021-10-18 · 2 🐛 · 1 🔍 · 3 👩‍💻👨‍💻` + +### 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.0` + +### 🐛 Bug fixes + + +- **APPS:** Communication problem when updating and uninstalling apps in cluster ([#23418](https://github.com/RocketChat/Rocket.Chat/pull/23418)) + + - Make the hook responsible for receiving app update events inside a cluster fetch the app's package (zip file) in the correct place. + - Also shows a warning message on uninstalls inside a cluster. As there are many servers writing to the same place, some race conditions may occur. This prevents problems related to terminating the process in the middle due to errors being thrown and leaving the server in a faulty state. + +- Server crashing when Routing method is not available at start ([#23473](https://github.com/RocketChat/Rocket.Chat/pull/23473)) + +
+🔍 Minor changes + + +- Release 4.0.3 ([#23496](https://github.com/RocketChat/Rocket.Chat/pull/23496)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@thassiov](https://github.com/thassiov) + +# 4.0.2 +`2021-10-14 · 4 🐛 · 1 🔍 · 3 👩‍💻👨‍💻` + +### 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.0` + +### 🐛 Bug fixes + + +- **ENTERPRISE:** Omnichannel agent is not leaving the room when a forwarded chat is queued ([#23404](https://github.com/RocketChat/Rocket.Chat/pull/23404)) + +- Attachment buttons overlap in mobile view ([#23377](https://github.com/RocketChat/Rocket.Chat/pull/23377) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari)) + +- Prevent starting Omni-Queue if Omnichannel is disabled ([#23396](https://github.com/RocketChat/Rocket.Chat/pull/23396)) + + Whenever the Routing system setting changes, and omnichannel is disabled, then we shouldn't start the queue. + +- user/agent upload not working via Apps Engine after 3.16.0 ([#23393](https://github.com/RocketChat/Rocket.Chat/pull/23393)) + + Fixes #22974 + +
+🔍 Minor changes + + +- Release 4.0.2 ([#23460](https://github.com/RocketChat/Rocket.Chat/pull/23460) by [@Aman-Maheshwari](https://github.com/Aman-Maheshwari)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@Aman-Maheshwari](https://github.com/Aman-Maheshwari) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@murtaza98](https://github.com/murtaza98) +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 4.0.1 +`2021-10-06 · 7 🐛 · 2 🔍 · 7 👩‍💻👨‍💻` + +### 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.0` + +### 🐛 Bug fixes + + +- BigBlueButton integration error due to missing file import ([#23366](https://github.com/RocketChat/Rocket.Chat/pull/23366) by [@wolbernd](https://github.com/wolbernd)) + + Fixes BigBlueButton integration + +- imported migration v240 ([#23374](https://github.com/RocketChat/Rocket.Chat/pull/23374)) + +- LDAP not stoping after wrong password ([#23382](https://github.com/RocketChat/Rocket.Chat/pull/23382)) + +- MongoDB deprecation link ([#23381](https://github.com/RocketChat/Rocket.Chat/pull/23381)) + +- resumeToken not working ([#23379](https://github.com/RocketChat/Rocket.Chat/pull/23379)) + +- unwanted toastr error message when deleting user ([#23372](https://github.com/RocketChat/Rocket.Chat/pull/23372)) + +- Users' `roles` and `type` being reset to default on LDAP DataSync ([#23378](https://github.com/RocketChat/Rocket.Chat/pull/23378)) + + - Update `roles` and `type` fields only if they are specified in the data imported from LDAP (otherwise, no changes are applied). + +
+🔍 Minor changes + + +- Chore: Update Apps-Engine version ([#23375](https://github.com/RocketChat/Rocket.Chat/pull/23375)) + +- Release 4.0.1 ([#23386](https://github.com/RocketChat/Rocket.Chat/pull/23386) by [@wolbernd](https://github.com/wolbernd)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@wolbernd](https://github.com/wolbernd) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@d-gubert](https://github.com/d-gubert) +- [@matheusbsilva137](https://github.com/matheusbsilva137) +- [@ostjen](https://github.com/ostjen) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) + +# 4.0.0 +`2021-10-01 · 15 ️️️⚠️ · 4 🎉 · 11 🚀 · 24 🐛 · 67 🔍 · 26 👩‍💻👨‍💻` + +### 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.0-alpha.5428` + +### ⚠️ BREAKING CHANGES + + +- **ENTERPRISE:** "Download CSV" button doesn't work in the Engagement Dashboard's Active Users section ([#23013](https://github.com/RocketChat/Rocket.Chat/pull/23013)) + + - Fix "Download CSV" button in the Engagement Dashboard's Active Users section; + - Add column headers to the CSV file downloaded from the Engagement Dashboard's Active Users section; + - Split the data in multiple CSV files. + +- **ENTERPRISE:** CSV file downloaded in the Engagement Dashboard's New Users section contains undefined data ([#23014](https://github.com/RocketChat/Rocket.Chat/pull/23014)) + + - Fix CSV file downloaded in the Engagement Dashboard's New Users section; + - Add column headers to the CSV file downloaded from the Engagement Dashboard's New Users section. + +- **ENTERPRISE:** Missing headers in CSV files downloaded from the Engagement Dashboard ([#23223](https://github.com/RocketChat/Rocket.Chat/pull/23223)) + + - Add headers to all CSV files downloaded from the "Messages" and "Channels" tabs from the Engagement Dashboard; + - Add headers to the CSV file downloaded from the "Users by time of day" section (in the "Users" tab). + +- LDAP Refactoring ([#23171](https://github.com/RocketChat/Rocket.Chat/pull/23171)) + +- Moved advanced oAuth features to EE ([#23201](https://github.com/RocketChat/Rocket.Chat/pull/23201)) + +- Moved role-sync and advanced SAML settings to EE ([#23107](https://github.com/RocketChat/Rocket.Chat/pull/23107)) + +- Moved SAML custom field map to EE ([#23319](https://github.com/RocketChat/Rocket.Chat/pull/23319)) + +- Remove cordova compatibility setting ([#23302](https://github.com/RocketChat/Rocket.Chat/pull/23302)) + +- Remove deprecated endpoints ([#23162](https://github.com/RocketChat/Rocket.Chat/pull/23162)) + + The following REST endpoints were removed: + + - `/api/v1/emoji-custom` + - `/api/v1/info` + - `/api/v1/permissions` + - `/api/v1/permissions.list` + + The following Real time API Methods were removed: + + - `getFullUserData` + - `getServerInfo` + - `livechat:saveOfficeHours` + +- Remove Google Vision features ([#23160](https://github.com/RocketChat/Rocket.Chat/pull/23160)) + + Google Vision features like "block adult images" or label detection were not being maintained and totally broken. So we decided to remove its feature and maybe in the future release the same features as an app. + +- Remove old migrations up to version 2.4.14 ([#23277](https://github.com/RocketChat/Rocket.Chat/pull/23277)) + + To update to version 4.0.0 you'll need to be running at least version 3.0.0, otherwise you might loose some database migrations which might have unexpected effects. + + This aims to clean up the code, since upgrades jumping 2 major versions are too risky and hard to maintain, we'll keep only migration from that last major (in this case 3.x). + +- Remove patch info from endpoint /api/info for non-logged in users ([#16050](https://github.com/RocketChat/Rocket.Chat/pull/16050) by [@MarcosSpessatto](https://github.com/MarcosSpessatto)) + +- Removed support of MongoDB 3.4; Deprecated MongoDB 3.6 and 4.0 ([#22907](https://github.com/RocketChat/Rocket.Chat/pull/22907)) + +- Stop sending audio notifications via stream ([#23108](https://github.com/RocketChat/Rocket.Chat/pull/23108)) + + Remove audio preferences and make them tied to desktop notification preferences. + + TL;DR: new message sounds will play only if you receive a desktop notification. you'll still be able to chose to not play any sound though + +- Webhook will fail if user is not part of the channel ([#23310](https://github.com/RocketChat/Rocket.Chat/pull/23310)) + + Remove deprecated behavior added by https://github.com/RocketChat/Rocket.Chat/pull/18024 that accepts webhook integrations sending messages even if the user is not part of the channel. + + Starting from 4.0.0 the webhook request will fail with `error-not-allowed` error: + + ``` + {"success":false,"error":"error-not-allowed"} + ``` + +### 🎉 New features + + +- **APPS:** Get livechat's room transcript via bridge method ([#22985](https://github.com/RocketChat/Rocket.Chat/pull/22985)) + + Adds a new method for retrieving a room's transcript via a new method in the Livechat bridge + +- Add activity indicators for Uploading and Recording using new API; Support thread context; Deprecate the old typing API ([#22392](https://github.com/RocketChat/Rocket.Chat/pull/22392) by [@sumukhah](https://github.com/sumukhah)) + +- Omnichannel source identification fields ([#23090](https://github.com/RocketChat/Rocket.Chat/pull/23090)) + + This PR adds new fields to the room schema that aids in the identification of the source that created an Omnichannel room, which can be either via livechat widget, SMS, app, etc. + +- Seats Cap ([#23017](https://github.com/RocketChat/Rocket.Chat/pull/23017) by [@g-thome](https://github.com/g-thome)) + + - Adding New Members + - Awareness of seats usage while adding new members + - Seats Cap about to be reached + - Seats Cap reached + - Request more seats + - Warning Admins + - System telling admins max seats are about to exceed + - System telling admins max seats were exceed + - Metric on Info Page + - Request more seats + - Warning Members + - Invite link + - Block creating new invite links + - Block existing invite links (feedback on register process) + - Register to Workspaces + - Emails + - System telling admins max seats are about to exceed + - System telling admins max seats were exceed + +### 🚀 Improvements + + +- **APPS:** New storage strategy for Apps-Engine file packages ([#22657](https://github.com/RocketChat/Rocket.Chat/pull/22657)) + + This is an enabler for our initiative to support NPM packages in the Apps-Engine. + + Currently, the packages (zip files) for Rocket.Chat Apps are stored as a base64 encoded string in a document in the database, which constrains us due to the size limit of a document in MongoDB (16Mb). + + When we allow apps to include NPM packages, the size of the App package itself will be potentially _very large_ (I'm looking at you `node_modules`). Thus we'll be changing the strategy to store apps either with GridFS or the host's File System itself. + +- **APPS:** Return task ids when using the scheduler api ([#23023](https://github.com/RocketChat/Rocket.Chat/pull/23023)) + + In the methods that create tasks (`scheduleRecurring` and `scheduleOnce`) return the `id` of the document created in the database so the user can cancel each task individually. + +- Add missing pt-BR translations, fix typos and unify language ([#23176](https://github.com/RocketChat/Rocket.Chat/pull/23176) by [@gabrieloliverio](https://github.com/gabrieloliverio)) + +- Better text for auth banner ([#23256](https://github.com/RocketChat/Rocket.Chat/pull/23256) by [@g-thome](https://github.com/g-thome)) + + Change the text in the banner warning for auth changes + +- Canned response admin settings ([#23190](https://github.com/RocketChat/Rocket.Chat/pull/23190)) + +- Change log format to JSON ([#22975](https://github.com/RocketChat/Rocket.Chat/pull/22975)) + +- Change occurences of Livechat to Omnichannel in ES translations were applicable ([#23199](https://github.com/RocketChat/Rocket.Chat/pull/23199)) + +- Do not re-create General room on every server start ([#22957](https://github.com/RocketChat/Rocket.Chat/pull/22957)) + + - Check the `Show_Setup_Wizard` Setting's value to control whether the general room should be created. This channel will only be created if the `Show_Setup_Wizard` Setting is 'pending'. + +- Load code highlighting languages on demand and fixes on new message parser ([#23232](https://github.com/RocketChat/Rocket.Chat/pull/23232)) + + Now we have this setting called 'Code highlighting languages list' where you can define the languages that you want to be loaded by default. + +- Throw error if no appId is provided to useUIKitHandleAction ([#23221](https://github.com/RocketChat/Rocket.Chat/pull/23221)) + +- Use PaginatedSelectFiltered in department edition ([#23054](https://github.com/RocketChat/Rocket.Chat/pull/23054)) + +### 🐛 Bug fixes + + +- "Parent channel or group" search in discussions' creation throws "Unexpected end of JSON input" error ([#23076](https://github.com/RocketChat/Rocket.Chat/pull/23076)) + + - Use `encodeURIComponent()` to encode values received by `_generateQueryFromParams()`. + +- "Read Only" and "Allow Reacting" system messages are missing in rooms ([#23037](https://github.com/RocketChat/Rocket.Chat/pull/23037)) + + - Add system message to notify changes on the **"Read Only"** setting; + - Add system message to notify changes on the **"Allow Reacting"** setting; + - Fix "Allow Reacting" setting's description (updated from "Only authorized users can write new messages" to "Only authorized users can react to messages"). + ![system-messages](https://user-images.githubusercontent.com/36537004/130883527-9eb47fcd-c8e5-41fb-af34-5d99bd0a6780.PNG) + +- Add check before placing chat on-hold to confirm that contact sent last message ([#23053](https://github.com/RocketChat/Rocket.Chat/pull/23053)) + +- Add missing custom fields to apps' users converter ([#21176](https://github.com/RocketChat/Rocket.Chat/pull/21176) by [@cuonghuunguyen](https://github.com/cuonghuunguyen)) + +- Avoid bots to be marked as unavailable when log off/login ([#23262](https://github.com/RocketChat/Rocket.Chat/pull/23262)) + +- Can't edit profile information if any field update setting is disabled ([#23110](https://github.com/RocketChat/Rocket.Chat/pull/23110)) + + - Check which fields have been updated before throwing errors in `validateUserEditing`. + +- Inaccurate use of 'Mobile notifications' instead of 'Push notifications' in i18n strings ([#22978](https://github.com/RocketChat/Rocket.Chat/pull/22978)) + + - Fix inaccurate use of 'Mobile notifications' (which is misleading in German) by 'Push notifications'; + - Update `'Notification_Mobile_Default_For'` key to `'Notification_Push_Default_For'` (and text to 'Send Push Notifications For' for English Language); + - Update `'Accounts_Default_User_Preferences_mobileNotifications'` key to `'Accounts_Default_User_Preferences_pushNotifications'`; + - Update `'Mobile_Notifications_Default_Alert'` key to `'Mobile_Push_Notifications_Default_Alert'`; + +- Logging out from other clients ([#23276](https://github.com/RocketChat/Rocket.Chat/pull/23276)) + +- Mark agents as unavailable when they logout ([#23219](https://github.com/RocketChat/Rocket.Chat/pull/23219)) + +- Modals is cutting pixels of the content ([#23243](https://github.com/RocketChat/Rocket.Chat/pull/23243)) + + Fuselage Dependency: [543](https://github.com/RocketChat/Rocket.Chat.Fuselage/pull/543) + ![image](https://user-images.githubusercontent.com/27704687/134049227-3cd1deed-34ba-454f-a95e-e99b79a7a7b9.png) + +- Omnichannel On hold chats being forwarded to offline agents ([#23185](https://github.com/RocketChat/Rocket.Chat/pull/23185)) + +- Omnichannel transcript button without user's email ([#23150](https://github.com/RocketChat/Rocket.Chat/pull/23150)) + +- Prevent users to edit an existing role when adding a new one with the same name used before. ([#22407](https://github.com/RocketChat/Rocket.Chat/pull/22407) by [@lucassartor](https://github.com/lucassartor)) + + ### before + ![Peek 2021-07-13 16-31](https://user-images.githubusercontent.com/27704687/125513721-953d84f4-1c95-45ca-80e1-b00992b874f6.gif) + + ### after + ![Peek 2021-07-13 16-34](https://user-images.githubusercontent.com/27704687/125514098-91ee8014-51e5-4c62-9027-5538acf57d08.gif) + +- Remove doubled "Canned Responses" strings ([#23056](https://github.com/RocketChat/Rocket.Chat/pull/23056)) + + - Remove doubled canned response setting introduced in #22703 (by setting id change); + - Update "Canned Responses" keys to "Canned_Responses". + +- Remove margin from quote inside quote ([#21779](https://github.com/RocketChat/Rocket.Chat/pull/21779)) + + ![image](https://user-images.githubusercontent.com/17487063/116253926-4a89e600-a747-11eb-9172-f2ed1245fa1b.png) + +- Save department agents ([#23209](https://github.com/RocketChat/Rocket.Chat/pull/23209)) + +- Sidebar not closing when clicking in Home or Directory on mobile view ([#23218](https://github.com/RocketChat/Rocket.Chat/pull/23218)) + + ### Additional fixed + - Merge Burger menu components into a single component + - Show a badge with no-read messages in the Burger Button: + ![image](https://user-images.githubusercontent.com/27704687/133679378-20fea2c0-4ac1-4b4e-886e-45154cc6afea.png) + - remove useSidebarClose hook + +- Stop queue when Omnichannel is disabled or the routing method does not support it ([#23261](https://github.com/RocketChat/Rocket.Chat/pull/23261)) + + - Add missing key logs + - Stop queue (and logs) when livechat is disabled or when routing method does not support queue + - Stop ignoring offline bot agents from delegation (previously, if a bot was offline, even with "Assign new conversations to bot agent" enabled, bot will be ignored and chat will be left in limbo (since bot was assigned, but offline). + +- Toolbox click not working on Safari(iOS) ([#23244](https://github.com/RocketChat/Rocket.Chat/pull/23244)) + +- transfer message when tranferring room by Apps Engine ([#23074](https://github.com/RocketChat/Rocket.Chat/pull/23074) by [@cuonghuunguyen](https://github.com/cuonghuunguyen)) + +- Update bugsnag package ([#23104](https://github.com/RocketChat/Rocket.Chat/pull/23104)) + +- User list not being updated after creation/deletion of user ([#23032](https://github.com/RocketChat/Rocket.Chat/pull/23032)) + +- Wrap canned-responses endpoints with ee license ([#23204](https://github.com/RocketChat/Rocket.Chat/pull/23204)) + +- Wrong docs link on Omni-Webhook page ([#23117](https://github.com/RocketChat/Rocket.Chat/pull/23117)) + +
+🔍 Minor changes + + +- Bump @rocket.chat/string-helpers from 0.27.0 to 0.29.0 in /ee/server/services ([#23138](https://github.com/RocketChat/Rocket.Chat/pull/23138) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @storybook/react from 6.3.6 to 6.3.8 ([#23165](https://github.com/RocketChat/Rocket.Chat/pull/23165) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @types/cookie from 0.4.0 to 0.4.1 in /ee/server/services ([#22600](https://github.com/RocketChat/Rocket.Chat/pull/22600) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @types/ejson from 2.1.2 to 2.1.3 in /ee/server/services ([#23126](https://github.com/RocketChat/Rocket.Chat/pull/23126) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @types/express from 4.17.12 to 4.17.13 in /ee/server/services ([#22598](https://github.com/RocketChat/Rocket.Chat/pull/22598) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @types/imap from 0.8.34 to 0.8.35 ([#23122](https://github.com/RocketChat/Rocket.Chat/pull/23122) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump @types/ws from 7.4.6 to 7.4.7 in /ee/server/services ([#23095](https://github.com/RocketChat/Rocket.Chat/pull/23095) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump actions/stale from 3.0.19 to 4 ([#22673](https://github.com/RocketChat/Rocket.Chat/pull/22673) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump csv-parse from 4.16.0 to 4.16.3 ([#23120](https://github.com/RocketChat/Rocket.Chat/pull/23120) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump ejson from 2.2.1 to 2.2.2 in /ee/server/services ([#23236](https://github.com/RocketChat/Rocket.Chat/pull/23236) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump iconv-lite from 0.4.24 to 0.6.3 ([#22527](https://github.com/RocketChat/Rocket.Chat/pull/22527) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump image-size from 0.6.3 to 1.0.0 ([#22528](https://github.com/RocketChat/Rocket.Chat/pull/22528) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump ip-range-check from 0.0.2 to 0.2.0 ([#22532](https://github.com/RocketChat/Rocket.Chat/pull/22532) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump jsrsasign from 10.3.0 to 10.4.0 ([#23163](https://github.com/RocketChat/Rocket.Chat/pull/23163) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump juice from 5.2.0 to 8.0.0 ([#22177](https://github.com/RocketChat/Rocket.Chat/pull/22177) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump object-path from 0.11.5 to 0.11.6 ([#23088](https://github.com/RocketChat/Rocket.Chat/pull/23088) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump pm2 from 5.1.0 to 5.1.1 in /ee/server/services ([#23128](https://github.com/RocketChat/Rocket.Chat/pull/23128) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump stylelint-order from 2.2.1 to 4.1.0 ([#22036](https://github.com/RocketChat/Rocket.Chat/pull/22036) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump supertest from 6.1.3 to 6.1.6 ([#23139](https://github.com/RocketChat/Rocket.Chat/pull/23139) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump tar from 6.1.0 to 6.1.11 in /ee/server/services ([#23068](https://github.com/RocketChat/Rocket.Chat/pull/23068) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Bump xml-crypto from 2.1.2 to 2.1.3 ([#23141](https://github.com/RocketChat/Rocket.Chat/pull/23141) by [@dependabot[bot]](https://github.com/dependabot[bot])) + +- Chore: Change Ubuntu version to 20.04 on all GitHub Actions ([#23200](https://github.com/RocketChat/Rocket.Chat/pull/23200)) + +- Chore: client endpoints typings ([#23152](https://github.com/RocketChat/Rocket.Chat/pull/23152)) + +- Chore: Convert VerticalBar component to typescript ([#22542](https://github.com/RocketChat/Rocket.Chat/pull/22542)) + +- Chore: Environmental variable for marketplace url ([#22922](https://github.com/RocketChat/Rocket.Chat/pull/22922)) + +- Chore: Make SMTP empty on docker-compose so registration won't hang out of the box ([#23255](https://github.com/RocketChat/Rocket.Chat/pull/23255)) + +- Chore: Move client helpers ([#23178](https://github.com/RocketChat/Rocket.Chat/pull/23178)) + + Moves helper modules under `app/` to `client/lib/utils/`. + +- Chore: Re-enable session tests on local after removal of mongo-unit ([#23263](https://github.com/RocketChat/Rocket.Chat/pull/23263)) + +- Chore: Remove non-used dependencies ([#23109](https://github.com/RocketChat/Rocket.Chat/pull/23109)) + +- Chore: Remove wrong usages of `Meteor.wrapAsync` ([#23079](https://github.com/RocketChat/Rocket.Chat/pull/23079)) + +- Chore: Update Livechat widget to 1.9.4 ([#23198](https://github.com/RocketChat/Rocket.Chat/pull/23198)) + +- Chore: Update pino and pino-pretty ([#23269](https://github.com/RocketChat/Rocket.Chat/pull/23269)) + +- Chore: Update pino and pino-pretty ([#23157](https://github.com/RocketChat/Rocket.Chat/pull/23157)) + +- Chore: Upgrade limax ([#23187](https://github.com/RocketChat/Rocket.Chat/pull/23187)) + + Upgrades `limax` for faster slugify algorithm. + +- i18n: Language update from LingoHub 🤖 on 2021-08-30Z ([#23061](https://github.com/RocketChat/Rocket.Chat/pull/23061)) + +- i18n: Language update from LingoHub 🤖 on 2021-09-06Z ([#23123](https://github.com/RocketChat/Rocket.Chat/pull/23123)) + +- i18n: Language update from LingoHub 🤖 on 2021-09-13Z ([#23184](https://github.com/RocketChat/Rocket.Chat/pull/23184)) + +- Merge master into develop & Set version to 4.0.0 ([#23086](https://github.com/RocketChat/Rocket.Chat/pull/23086)) + +- Regression: "Join" button not working ([#23320](https://github.com/RocketChat/Rocket.Chat/pull/23320)) + +- Regression: `renderEmoji` helper referred as a template ([#23212](https://github.com/RocketChat/Rocket.Chat/pull/23212)) + +- Regression: Add default value when no cookies are present ([#23318](https://github.com/RocketChat/Rocket.Chat/pull/23318)) + +- Regression: Blank screen in Jitsi video calls ([#23322](https://github.com/RocketChat/Rocket.Chat/pull/23322)) + + - Fix Jitsi calls being disposed even when "Open in new window" setting is disabled; + - Fix misspelling on `CallJitsWithData.js` file name. + +- Regression: Create new loggers based on server log level ([#23297](https://github.com/RocketChat/Rocket.Chat/pull/23297)) + +- Regression: Fix app storage migration ([#23286](https://github.com/RocketChat/Rocket.Chat/pull/23286)) + + The previous version of this migration didn't take into consideration apps that were installed prior to [Rocket.Chat@3.8.0](https://github.com/RocketChat/Rocket.Chat/releases/tag/3.8.0), which [removed the typescript compiler from the server](https://github.com/RocketChat/Rocket.Chat/pull/18687) and into the CLI. As a result, the zip files inside each installed app's document in the database had typescript files in them instead of the now required javascript files. + + As the new strategy of source code storage for apps changes the way the app is loaded, those zip files containing the source code are read everytime the app is started (or [in this particular case, updated](https://github.com/RocketChat/Rocket.Chat/pull/23286/files#diff-caf9f7a22478639e58d6514be039140a42ce1ab2d999c3efe5678c38ee36d0ccR43)), and as the zips' contents were wrong, the operation was failing. + + The fix extract the data from old apps and creates new zip files with the compiled `js` already present. + +- Regression: Fix Bugsnag not started error ([#23308](https://github.com/RocketChat/Rocket.Chat/pull/23308)) + +- Regression: Fix channel icons on queue ([#23304](https://github.com/RocketChat/Rocket.Chat/pull/23304)) + +- Regression: Fix user registration stuck ([#23254](https://github.com/RocketChat/Rocket.Chat/pull/23254)) + +- Regression: Fix view logs admin screen ([#23194](https://github.com/RocketChat/Rocket.Chat/pull/23194)) + +- Regression: invalid `call` import ([#23328](https://github.com/RocketChat/Rocket.Chat/pull/23328)) + +- Regression: invalid `call` import ([#23334](https://github.com/RocketChat/Rocket.Chat/pull/23334)) + +- Regression: LDAP Channel/Role Sync not working ([#23311](https://github.com/RocketChat/Rocket.Chat/pull/23311)) + +- Regression: LDAP Issues ([#23306](https://github.com/RocketChat/Rocket.Chat/pull/23306)) + +- Regression: LDAP Refactoring ([#23231](https://github.com/RocketChat/Rocket.Chat/pull/23231)) + +- Regression: LDAP User Data Sync not always working ([#23321](https://github.com/RocketChat/Rocket.Chat/pull/23321)) + +- Regression: LDAP: Handle base authentication and prevent crash ([#23331](https://github.com/RocketChat/Rocket.Chat/pull/23331)) + + When AD requires TLS the auth crashes the server if StartTLS is not set, the error shows at the end because the code was not waiting on this operation. + +- Regression: Log Sections not respecting Log Level setting ([#23230](https://github.com/RocketChat/Rocket.Chat/pull/23230)) + +- Regression: Missing i18n key ([#23282](https://github.com/RocketChat/Rocket.Chat/pull/23282)) + +- Regression: Properly trickle-down state from UsersPage to UsersTable ([#23196](https://github.com/RocketChat/Rocket.Chat/pull/23196)) + + Spotted by @gabriellsh. + +- Regression: Removed exclusive tests statement ([#23333](https://github.com/RocketChat/Rocket.Chat/pull/23333)) + +- Regression: Request seats link ([#23312](https://github.com/RocketChat/Rocket.Chat/pull/23312)) + +- Regression: Request seats url ([#23317](https://github.com/RocketChat/Rocket.Chat/pull/23317)) + +- Regression: SAML identifier mapping ([#23330](https://github.com/RocketChat/Rocket.Chat/pull/23330)) + +- Regression: Seats Cap banner not being disabled if not enterprise ([#23278](https://github.com/RocketChat/Rocket.Chat/pull/23278)) + +- Regression: View Logs administration page crashing ([#23205](https://github.com/RocketChat/Rocket.Chat/pull/23205)) + + Fixes the `stdout.queue` endpoint; makes the components type-safe. + +- Regression: wrong settings order ([#23281](https://github.com/RocketChat/Rocket.Chat/pull/23281)) + +- Release 3.18.1 ([#23135](https://github.com/RocketChat/Rocket.Chat/pull/23135) by [@g-thome](https://github.com/g-thome)) + +- Release 3.18.2 ([#23338](https://github.com/RocketChat/Rocket.Chat/pull/23338)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@cuonghuunguyen](https://github.com/cuonghuunguyen) +- [@dependabot[bot]](https://github.com/dependabot[bot]) +- [@g-thome](https://github.com/g-thome) +- [@gabrieloliverio](https://github.com/gabrieloliverio) +- [@lucassartor](https://github.com/lucassartor) +- [@sumukhah](https://github.com/sumukhah) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@casalsgh](https://github.com/casalsgh) +- [@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) +- [@graywolf336](https://github.com/graywolf336) +- [@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) +- [@thassiov](https://github.com/thassiov) +- [@tiagoevanp](https://github.com/tiagoevanp) + # 3.18.2 -`2021-10-01 · 2 🐛 · 2 🔍 · 5 👩‍💻👨‍💻` +`2021-10-01 · 2 🐛 · 2 🔍 · 4 👩‍💻👨‍💻` ### Engine versions - Node: `12.22.1` @@ -21,6 +1258,7 @@ - Regression: Change some logs to new format ([#23307](https://github.com/RocketChat/Rocket.Chat/pull/23307)) +- Release 3.18.2 ([#23338](https://github.com/RocketChat/Rocket.Chat/pull/23338)) @@ -32,7 +1270,7 @@ - [@sampaiodiego](https://github.com/sampaiodiego) # 3.18.1 -`2021-09-06 · 1 🚀 · 1 🐛 · 1 🔍 · 4 👩‍💻👨‍💻` +`2021-09-06 · 1 🚀 · 1 🐛 · 2 🔍 · 4 👩‍💻👨‍💻` ### Engine versions - Node: `12.22.1` @@ -58,6 +1296,8 @@ Dimisses auth banners assigned to EE admins and prevents new ones from appearing. +- Release 3.18.1 ([#23135](https://github.com/RocketChat/Rocket.Chat/pull/23135) by [@g-thome](https://github.com/g-thome)) + ### 👩‍💻👨‍💻 Contributors 😍 @@ -84,10 +1324,8 @@ - **ENTERPRISE:** Maximum waiting time for chats in Omnichannel queue ([#22955](https://github.com/RocketChat/Rocket.Chat/pull/22955)) - - Add new settings to support closing chats that have been too long on waiting queue - - - Moved old settings to new "Queue Management" section - + - Add new settings to support closing chats that have been too long on waiting queue + - Moved old settings to new "Queue Management" section - Fix issue when closing a livechat room that caused client to not to know if room was open or not - Banner for the updates regarding authentication services ([#23055](https://github.com/RocketChat/Rocket.Chat/pull/23055) by [@g-thome](https://github.com/g-thome)) @@ -102,10 +1340,10 @@ - Separate RegEx Settings for Channels and Usernames validation ([#21937](https://github.com/RocketChat/Rocket.Chat/pull/21937) by [@aditya-mitra](https://github.com/aditya-mitra)) - Now, there are 2 separate settings for validating names - One for **channels** and another for **usernames**. - - This change also removes the old `UTF8_Names_Validation` setting and adds 2 new settings `UTF8_User_Names_Validation` and `UTF8_Channel_Names_Validation`. - + Now, there are 2 separate settings for validating names - One for **channels** and another for **usernames**. + + This change also removes the old `UTF8_Names_Validation` setting and adds 2 new settings `UTF8_User_Names_Validation` and `UTF8_Channel_Names_Validation`. + https://user-images.githubusercontent.com/55396651/116969904-af5bb800-acd4-11eb-9fc4-dacac60cb08f.mp4 ### 🚀 Improvements @@ -121,13 +1359,13 @@ - Rewrite File Upload Modal ([#22750](https://github.com/RocketChat/Rocket.Chat/pull/22750)) - Image preview: - ![image](https://user-images.githubusercontent.com/40830821/127223432-dccd2182-aec0-430f-8d70-03ac88aec791.png) - - Video preview: - ![image](https://user-images.githubusercontent.com/40830821/127225982-f8b21840-0d9c-4aff-a354-16188c7ed66e.png) - - Files larger than 10mb: + Image preview: + ![image](https://user-images.githubusercontent.com/40830821/127223432-dccd2182-aec0-430f-8d70-03ac88aec791.png) + + Video preview: + ![image](https://user-images.githubusercontent.com/40830821/127225982-f8b21840-0d9c-4aff-a354-16188c7ed66e.png) + + Files larger than 10mb: ![image](https://user-images.githubusercontent.com/40830821/127222611-5265040f-a06b-4ec5-b528-89b40e6a9072.png) - Types from currentChatsPage.tsx ([#22967](https://github.com/RocketChat/Rocket.Chat/pull/22967)) @@ -143,14 +1381,14 @@ - "Users By Time of the Day" chart displays incorrect data for Local Timezone ([#22836](https://github.com/RocketChat/Rocket.Chat/pull/22836)) - - Add local timezone conversion to the "Users By Time of the Day" chart in the Engagement Dashboard; + - Add local timezone conversion to the "Users By Time of the Day" chart in the Engagement Dashboard; - Simplify date creations by using `endOf` and `startOf` methods. - Atlassian Crowd connection not working ([#22996](https://github.com/RocketChat/Rocket.Chat/pull/22996) by [@piotrkochan](https://github.com/piotrkochan)) - Audio recording doesn't stop in direct messages on channel switch ([#22880](https://github.com/RocketChat/Rocket.Chat/pull/22880)) - - Cancel audio recordings on message bar destroy event. + - Cancel audio recordings on message bar destroy event. ![test-22372](https://user-images.githubusercontent.com/36537004/128569780-d83747b0-fb9c-4dc6-9bc5-7ae573e720c8.gif) - Bad words falling if message is empty ([#22930](https://github.com/RocketChat/Rocket.Chat/pull/22930)) @@ -175,23 +1413,21 @@ - Return transcript/dashboards based on timezone settings ([#22850](https://github.com/RocketChat/Rocket.Chat/pull/22850)) - - Added new setting to manage timezones - - - Applied new setting to omnichannel dashboards (realtime, analytics) [NOTE: Other dashboards aren't using this setting actually) - + - Added new setting to manage timezones + - Applied new setting to omnichannel dashboards (realtime, analytics) [NOTE: Other dashboards aren't using this setting actually) - Change getAnalyticsBetweenDate query to filter out system messages instead of substracting them - Tab margin style ([#22851](https://github.com/RocketChat/Rocket.Chat/pull/22851)) - ### before - ![image](https://user-images.githubusercontent.com/27704687/128103848-2a25ba7e-0e59-4502-9bcd-2569cad9379a.png) - - ### after + ### before + ![image](https://user-images.githubusercontent.com/27704687/128103848-2a25ba7e-0e59-4502-9bcd-2569cad9379a.png) + + ### after ![image](https://user-images.githubusercontent.com/27704687/128103633-ec7b93fc-4667-4dc9-bad3-bfffaff3974e.png) - Threads and discussions searches don't display proper results ([#22914](https://github.com/RocketChat/Rocket.Chat/pull/22914)) - - _Fix_ issue in discussions search (which wasn't working after a search with no results was made); + - _Fix_ issue in discussions search (which wasn't working after a search with no results was made); - _Improve_ discussions and threads searches: both searches (`chat.getDiscussions` and `chat.getThreadsList`) are now case insensitive (do NOT differ capital from lower letters) and match incomplete words or terms. - Threads List being requested more than expected ([#22879](https://github.com/RocketChat/Rocket.Chat/pull/22879)) @@ -226,8 +1462,8 @@ - Chore: Script to start Rocket.Chat in HA mode during development ([#22398](https://github.com/RocketChat/Rocket.Chat/pull/22398)) - Sometimes we need to start Rocket.Chat in High-Availability mode (cluster) during development to test how a feature behaves or hunt down a bug. Currently, this involves a lot of commands with details that might be lost if you haven't done it in a while. - + Sometimes we need to start Rocket.Chat in High-Availability mode (cluster) during development to test how a feature behaves or hunt down a bug. Currently, this involves a lot of commands with details that might be lost if you haven't done it in a while. + This PR intends to provide a really simple way for us to start many instances of Rocket.Chat connected in a cluster. - Chore: Update Livechat widget to 1.9.4 ([#22990](https://github.com/RocketChat/Rocket.Chat/pull/22990)) @@ -244,13 +1480,13 @@ - Regression: File upload name suggestion ([#22953](https://github.com/RocketChat/Rocket.Chat/pull/22953)) - Before: - ![image](https://user-images.githubusercontent.com/40830821/129774936-ecdbe9a1-5e3f-4a0a-ad1e-6f13eb15c60b.png) - ![image](https://user-images.githubusercontent.com/40830821/129775011-fb0df01d-74e4-41ae-bb47-dcf4cc17735e.png) - - - After: - ![image](https://user-images.githubusercontent.com/40830821/129774877-928a8aa0-c003-4e57-8b33-ea6accc32774.png) + Before: + ![image](https://user-images.githubusercontent.com/40830821/129774936-ecdbe9a1-5e3f-4a0a-ad1e-6f13eb15c60b.png) + ![image](https://user-images.githubusercontent.com/40830821/129775011-fb0df01d-74e4-41ae-bb47-dcf4cc17735e.png) + + + After: + ![image](https://user-images.githubusercontent.com/40830821/129774877-928a8aa0-c003-4e57-8b33-ea6accc32774.png) ![image](https://user-images.githubusercontent.com/40830821/129774972-d67debaf-0ce9-44fb-93cb-d7612dd18edf.png) - Regression: Fix creation of self-DMs ([#23015](https://github.com/RocketChat/Rocket.Chat/pull/23015)) @@ -318,8 +1554,7 @@ - Fix Auto Selection algorithm on community edition ([#22991](https://github.com/RocketChat/Rocket.Chat/pull/22991)) - - When using the autoselection algo on community editions, all agents were marked as unavailable due to an unapplied filter - + - When using the autoselection algo on community editions, all agents were marked as unavailable due to an unapplied filter - Fixed an issue when both user & system setting to manange EE max number of chats allowed were set to 0
@@ -359,7 +1594,7 @@ - Apps-Engine's scheduler failing to update run tasks ([#22882](https://github.com/RocketChat/Rocket.Chat/pull/22882)) - [Agenda](https://github.com/agenda/agenda), the library that manages scheduling, depended on setting a job property named `nextRunAt` as `undefined` to signal whether it should be run on schedule or not. [Rocket.Chat's current Mongo driver](https://github.com/RocketChat/Rocket.Chat/pull/22399) ignores `undefined` values when updating documents and this was causing jobs to never stop running as Agenda couldn't clear that property (set them as `undefined`). + [Agenda](https://github.com/agenda/agenda), the library that manages scheduling, depended on setting a job property named `nextRunAt` as `undefined` to signal whether it should be run on schedule or not. [Rocket.Chat's current Mongo driver](https://github.com/RocketChat/Rocket.Chat/pull/22399) ignores `undefined` values when updating documents and this was causing jobs to never stop running as Agenda couldn't clear that property (set them as `undefined`). This updates Rocket.Chat's dependency on Agenda.js to point to [a fork that fixes the problem](https://github.com/RocketChat/agenda/releases/tag/3.1.2). - Close omnichannel conversations when agent is deactivated ([#22917](https://github.com/RocketChat/Rocket.Chat/pull/22917)) @@ -413,7 +1648,7 @@ - Monitoring Track messages' round trip time ([#22676](https://github.com/RocketChat/Rocket.Chat/pull/22676)) - Track messages' roundtrip time from backend saves time to the time when received back from the oplog allowing track of oplog slowness. + Track messages' roundtrip time from backend saves time to the time when received back from the oplog allowing track of oplog slowness. Prometheus metric: `rocketchat_messages_roundtrip_time` - REST endpoint to remove User from Role ([#20485](https://github.com/RocketChat/Rocket.Chat/pull/20485) by [@Cosnavel](https://github.com/Cosnavel) & [@lucassartor](https://github.com/lucassartor)) @@ -425,22 +1660,19 @@ - Change message deletion confirmation modal to toast ([#22544](https://github.com/RocketChat/Rocket.Chat/pull/22544)) - Changed a timed modal for a toast message + Changed a timed modal for a toast message ![image](https://user-images.githubusercontent.com/40830821/124192670-0646f900-da9c-11eb-941c-9ae35421f6ef.png) - Configuration for indices in Apps-Engine models ([#22705](https://github.com/RocketChat/Rocket.Chat/pull/22705)) - * Add `appId` field to the data saved by the Scheduler - - * Add `appId` index to `rocketchat_apps_persistence` model - - * Skip "trash collection" when deleting records from `rocketchat_apps_persistence` - - * Add a new setting to control for how long we should keep logs from the apps - - ![image](https://user-images.githubusercontent.com/1810309/126246666-907f9d98-1d84-4dfe-a80a-7dd874d36fa8.png) - - + * Add `appId` field to the data saved by the Scheduler + * Add `appId` index to `rocketchat_apps_persistence` model + * Skip "trash collection" when deleting records from `rocketchat_apps_persistence` + * Add a new setting to control for how long we should keep logs from the apps + + ![image](https://user-images.githubusercontent.com/1810309/126246666-907f9d98-1d84-4dfe-a80a-7dd874d36fa8.png) + + ![image](https://user-images.githubusercontent.com/1810309/126246655-2ce3cb5f-b2f5-456e-a9c4-beccd9b3ef41.png) - Make `shortcut` field of canned responses unique ([#22700](https://github.com/RocketChat/Rocket.Chat/pull/22700)) @@ -463,38 +1695,37 @@ - Replace remaing discussion creation modals with React modal. ([#22448](https://github.com/RocketChat/Rocket.Chat/pull/22448)) - ### before - ![image](https://user-images.githubusercontent.com/27704687/123840524-cbe72b80-d8e4-11eb-9ddb-23a9f9d90aac.png) - - ### after + ### before + ![image](https://user-images.githubusercontent.com/27704687/123840524-cbe72b80-d8e4-11eb-9ddb-23a9f9d90aac.png) + + ### after ![image](https://user-images.githubusercontent.com/27704687/123840219-74e15680-d8e4-11eb-95aa-00a990ffe0e7.png) - Return open room if available for visitors ([#22742](https://github.com/RocketChat/Rocket.Chat/pull/22742)) - Rewrite Enter Encryption Password Modal ([#22456](https://github.com/RocketChat/Rocket.Chat/pull/22456)) - ### before - ![image](https://user-images.githubusercontent.com/27704687/123182889-bbf3c580-d466-11eb-8d4d-9cfc3d224e33.png) - - ### after - ![image](https://user-images.githubusercontent.com/27704687/123182916-cada7800-d466-11eb-96ee-850be190d419.png) - - ### Aditional Improves: - + ### before + ![image](https://user-images.githubusercontent.com/27704687/123182889-bbf3c580-d466-11eb-8d4d-9cfc3d224e33.png) + + ### after + ![image](https://user-images.githubusercontent.com/27704687/123182916-cada7800-d466-11eb-96ee-850be190d419.png) + + ### Aditional Improves: - Added a visual validation in the password field - Rewrite OTR modals ([#22583](https://github.com/RocketChat/Rocket.Chat/pull/22583)) - ![image](https://user-images.githubusercontent.com/40830821/124513267-cb510800-ddb0-11eb-8165-f103029c348f.png) - ![image](https://user-images.githubusercontent.com/40830821/124513354-04897800-ddb1-11eb-96f4-41fe906ca0d7.png) + ![image](https://user-images.githubusercontent.com/40830821/124513267-cb510800-ddb0-11eb-8165-f103029c348f.png) + ![image](https://user-images.githubusercontent.com/40830821/124513354-04897800-ddb1-11eb-96f4-41fe906ca0d7.png) ![image](https://user-images.githubusercontent.com/40830821/124513395-1b2fcf00-ddb1-11eb-83e4-3f8f9b4676ba.png) - Rewrite Save Encryption Password Modal ([#22447](https://github.com/RocketChat/Rocket.Chat/pull/22447)) - ### before - ![image](https://user-images.githubusercontent.com/27704687/122980201-c337a800-d36e-11eb-8e2b-68534cea8e1e.png) - - ### after + ### before + ![image](https://user-images.githubusercontent.com/27704687/122980201-c337a800-d36e-11eb-8e2b-68534cea8e1e.png) + + ### after ![image](https://user-images.githubusercontent.com/27704687/122980409-f8dc9100-d36e-11eb-9c15-aff779c84a91.png) - Rewrite sidebar footer as React Component ([#22687](https://github.com/RocketChat/Rocket.Chat/pull/22687)) @@ -509,12 +1740,12 @@ - Wrong error message when trying to create a blocked username ([#22452](https://github.com/RocketChat/Rocket.Chat/pull/22452) by [@lucassartor](https://github.com/lucassartor)) - When trying to create a user with a blocked username, the UI was showing generic error message that it wasn't very detailed. - - Old error message: - ![image](https://user-images.githubusercontent.com/49413772/123120080-6d203e80-d41a-11eb-8c87-64e34334c856.png) - - New error message: + When trying to create a user with a blocked username, the UI was showing generic error message that it wasn't very detailed. + + Old error message: + ![image](https://user-images.githubusercontent.com/49413772/123120080-6d203e80-d41a-11eb-8c87-64e34334c856.png) + + New error message: ![aaa](https://user-images.githubusercontent.com/49413772/123120251-8c1ed080-d41a-11eb-8dc2-d7484923d851.PNG) ### 🐛 Bug fixes @@ -522,19 +1753,19 @@ - **ENTERPRISE:** Engagement Dashboard displaying incorrect data about active users ([#22381](https://github.com/RocketChat/Rocket.Chat/pull/22381)) - - Fix sessions' and users' grouping in the Engagement Dashboard API endpoints; - - Fix the data displayed in the charts from the "Active users", "Users by time of day" and "When is the chat busier?" sections of the Engagement Dashboard; + - Fix sessions' and users' grouping in the Engagement Dashboard API endpoints; + - Fix the data displayed in the charts from the "Active users", "Users by time of day" and "When is the chat busier?" sections of the Engagement Dashboard; - Replace label used to describe the amount of Active Users in the License section of the Info page. - **ENTERPRISE:** Make AutoSelect algo take current agent load in consideration ([#22611](https://github.com/RocketChat/Rocket.Chat/pull/22611)) - **ENTERPRISE:** Race condition on Omnichannel visitor abandoned callback ([#22413](https://github.com/RocketChat/Rocket.Chat/pull/22413)) - As you can see [here](https://github.com/RocketChat/Rocket.Chat/blob/857791c39c97b51b5b6fd3718e0c816959a81c3b/ee/app/livechat-enterprise/server/lib/Helper.js#L127) the `predictedVisitorAbandonment` flag is not set if the room object doesn't have `v.lastMessageTs` property. So we need to always make sure the `v.lastMessageTs` is set before this method is called. - - Currently the `v.lastMessageTs` is being set in [this](https://github.com/RocketChat/Rocket.Chat/blob/857791c39c97b51b5b6fd3718e0c816959a81c3b/app/livechat/server/hooks/saveLastVisitorMessageTs.js#L4) (lets call this **hook-1**) hook which has `HIGH` priority - and the `predictedVisitorAbandonment` check is inturn performed in [this](https://github.com/RocketChat/Rocket.Chat/blob/857791c39c97b51b5b6fd3718e0c816959a81c3b/ee/app/livechat-enterprise/server/hooks/setPredictedVisitorAbandonmentTime.js#L5) (let call this **hook-2**) hook which is also `HIGH` priority. - + As you can see [here](https://github.com/RocketChat/Rocket.Chat/blob/857791c39c97b51b5b6fd3718e0c816959a81c3b/ee/app/livechat-enterprise/server/lib/Helper.js#L127) the `predictedVisitorAbandonment` flag is not set if the room object doesn't have `v.lastMessageTs` property. So we need to always make sure the `v.lastMessageTs` is set before this method is called. + + Currently the `v.lastMessageTs` is being set in [this](https://github.com/RocketChat/Rocket.Chat/blob/857791c39c97b51b5b6fd3718e0c816959a81c3b/app/livechat/server/hooks/saveLastVisitorMessageTs.js#L4) (lets call this **hook-1**) hook which has `HIGH` priority + and the `predictedVisitorAbandonment` check is inturn performed in [this](https://github.com/RocketChat/Rocket.Chat/blob/857791c39c97b51b5b6fd3718e0c816959a81c3b/ee/app/livechat-enterprise/server/hooks/setPredictedVisitorAbandonmentTime.js#L5) (let call this **hook-2**) hook which is also `HIGH` priority. + So ideally we'd except the **hook-1** to be called b4 **hook-2**, however currently since both of them are at same priority, there is no way to control which one is executed first. Hence in this PR, I'm making the priority of **hook-2** as `MEDIUM` to keeping the priority of **hook-1** the same as b4, i.e. `HIGH`. This should make sure that the **hook-1** is always executed b4 **hook-2** - Admin page crashing when commit hash is null ([#22057](https://github.com/RocketChat/Rocket.Chat/pull/22057) by [@cprice-kgi](https://github.com/cprice-kgi)) @@ -543,41 +1774,39 @@ - Blank screen in message auditing DM tab ([#22763](https://github.com/RocketChat/Rocket.Chat/pull/22763)) - The DM tab in message auditing was displaying a blank screen, instead of the actual tab. - + The DM tab in message auditing was displaying a blank screen, instead of the actual tab. + ![image](https://user-images.githubusercontent.com/28611993/127041404-dfca7f6a-2b8b-4c15-9cbd-c6238fac0063.png) - Bugs in AutoCompleteDepartment ([#22414](https://github.com/RocketChat/Rocket.Chat/pull/22414)) - Call button is still displayed when the user doesn't have permission to use it ([#22170](https://github.com/RocketChat/Rocket.Chat/pull/22170)) - - Hide 'Call' buttons from the tab bar for muted users; - + - Hide 'Call' buttons from the tab bar for muted users; - Display an error when a muted user attempts to enter a call using the 'Click to Join!' button. - Can't see full user profile on team's room ([#22355](https://github.com/RocketChat/Rocket.Chat/pull/22355)) - ### before - ![before](https://user-images.githubusercontent.com/27704687/121966860-bbac4980-cd45-11eb-8d48-2b0457110fc7.gif) - - ### after - ![after](https://user-images.githubusercontent.com/27704687/121966870-bea73a00-cd45-11eb-9c89-ec52ac17e20f.gif) - - ### aditional fix :rocket: - + ### before + ![before](https://user-images.githubusercontent.com/27704687/121966860-bbac4980-cd45-11eb-8d48-2b0457110fc7.gif) + + ### after + ![after](https://user-images.githubusercontent.com/27704687/121966870-bea73a00-cd45-11eb-9c89-ec52ac17e20f.gif) + + ### aditional fix :rocket: - unnecessary `TeamsMembers` component removed - Cannot create a discussion from top left sidebar as a user ([#22618](https://github.com/RocketChat/Rocket.Chat/pull/22618) by [@lucassartor](https://github.com/lucassartor)) - When trying to create a discussion using the top left sidebar modal with an role that don't have the `view-other-user-channels ` permission, an empty list would be shown, which is a wrong behavior. - Also, when being able to use this modal, discussions were listed as options, which is also a wrong behavior as there can't be nested discussions. - - This PR looks to fix both these issues. - - **Old behavior:** - ![old](https://user-images.githubusercontent.com/49413772/124960017-3c333280-dff2-11eb-86cd-b2638311517e.png) - - **New behavior:** + When trying to create a discussion using the top left sidebar modal with an role that don't have the `view-other-user-channels ` permission, an empty list would be shown, which is a wrong behavior. + Also, when being able to use this modal, discussions were listed as options, which is also a wrong behavior as there can't be nested discussions. + + This PR looks to fix both these issues. + + **Old behavior:** + ![old](https://user-images.githubusercontent.com/49413772/124960017-3c333280-dff2-11eb-86cd-b2638311517e.png) + + **New behavior:** ![image](https://user-images.githubusercontent.com/49413772/124958882-05a8e800-dff1-11eb-8203-b34a4f1c98a0.png) - Channel is automatically getting added to the first option in move to team feature ([#22670](https://github.com/RocketChat/Rocket.Chat/pull/22670)) @@ -592,12 +1821,12 @@ - Create discussion modal - cancel button and invite users alignment ([#22718](https://github.com/RocketChat/Rocket.Chat/pull/22718)) - Changes in "open discussion" modal - - > Added cancel button - > Fixed alignment in invite user - - + Changes in "open discussion" modal + + > Added cancel button + > Fixed alignment in invite user + + ![image](https://user-images.githubusercontent.com/28611993/126388304-6ac76574-6924-426e-843d-afd53dc1c874.png) - crush in the getChannelHistory method ([#22667](https://github.com/RocketChat/Rocket.Chat/pull/22667) by [@MaestroArt](https://github.com/MaestroArt)) @@ -632,29 +1861,27 @@ - Quote message not working for Livechat visitors ([#22586](https://github.com/RocketChat/Rocket.Chat/pull/22586)) - ### Before: - ![image](https://user-images.githubusercontent.com/34130764/124583613-de2b1180-de70-11eb-82aa-18564b317626.png) - ### After: + ### Before: + ![image](https://user-images.githubusercontent.com/34130764/124583613-de2b1180-de70-11eb-82aa-18564b317626.png) + ### After: ![image](https://user-images.githubusercontent.com/34130764/124583775-12063700-de71-11eb-8ab5-b0169fac2d40.png) - Redirect to login after delete own account ([#22499](https://github.com/RocketChat/Rocket.Chat/pull/22499)) - Redirect the user to login after delete own account - - ### Aditional fixes: - - - Visual issue in password input on Delete Own Account Modal - - ### before - ![image](https://user-images.githubusercontent.com/27704687/123711503-f5ea1080-d846-11eb-96aa-8ed638ca665c.png) - - ### after + Redirect the user to login after delete own account + + ### Aditional fixes: + - Visual issue in password input on Delete Own Account Modal + + ### before + ![image](https://user-images.githubusercontent.com/27704687/123711503-f5ea1080-d846-11eb-96aa-8ed638ca665c.png) + + ### after ![image](https://user-images.githubusercontent.com/27704687/123711336-b3c0cf00-d846-11eb-9408-a686d8668ba5.png) - Remove stack traces from Meteor errors when debug setting is disabled ([#22699](https://github.com/RocketChat/Rocket.Chat/pull/22699)) - - Fix 'not iterable' errors in the `normalizeMessage` function; - + - Fix 'not iterable' errors in the `normalizeMessage` function; - Remove stack traces from errors thrown by the `jitsi:updateTimeout` (and other `Meteor.Error`s) method. - Rewrite CurrentChats to TS ([#22424](https://github.com/RocketChat/Rocket.Chat/pull/22424)) @@ -743,16 +1970,15 @@ - Regression: Data in the "Active Users" section is delayed in 1 day ([#22794](https://github.com/RocketChat/Rocket.Chat/pull/22794)) - - Fix 1 day delay in the Engagement Dashboard's "Active Users" section; - - - Downgrade `@nivo/line` version. - **Expected behavior:** + - Fix 1 day delay in the Engagement Dashboard's "Active Users" section; + - Downgrade `@nivo/line` version. + **Expected behavior:** ![active-users-engagement-dashboard](https://user-images.githubusercontent.com/36537004/127372185-390dc42f-bc90-4841-a22b-731f0aafcafe.PNG) - Regression: Data in the "New Users" section is delayed in 1 day ([#22751](https://github.com/RocketChat/Rocket.Chat/pull/22751)) - - Update nivo version (which was causing errors in the bar chart); - - Fix 1 day delay in '7 days' and '30 days' periods; + - Update nivo version (which was causing errors in the bar chart); + - Fix 1 day delay in '7 days' and '30 days' periods; - Update tooltip theme. - Regression: Federation warnings on ci ([#22765](https://github.com/RocketChat/Rocket.Chat/pull/22765) by [@g-thome](https://github.com/g-thome)) @@ -777,9 +2003,9 @@ - Regression: Fix tooltip style in the "Busiest Chat Times" chart ([#22813](https://github.com/RocketChat/Rocket.Chat/pull/22813)) - - Fix tooltip in the Engagement Dashboard's "Busiest Chat Times" chart (Hours). - - **Expected behavior:** + - Fix tooltip in the Engagement Dashboard's "Busiest Chat Times" chart (Hours). + + **Expected behavior:** ![busiest-times-ed](https://user-images.githubusercontent.com/36537004/127527827-465397ed-f089-4fb7-9ab2-6fa8cea6abdf.PNG) - Regression: Fix users not being able to see the scope of the canned m… ([#22760](https://github.com/RocketChat/Rocket.Chat/pull/22760)) @@ -796,10 +2022,10 @@ - Regression: Prevent custom status from being visible in sequential messages ([#22733](https://github.com/RocketChat/Rocket.Chat/pull/22733)) - ### before - ![image](https://user-images.githubusercontent.com/27704687/126641946-866dae96-1983-43a5-b689-b24670473ad0.png) - - ### after + ### before + ![image](https://user-images.githubusercontent.com/27704687/126641946-866dae96-1983-43a5-b689-b24670473ad0.png) + + ### after ![image](https://user-images.githubusercontent.com/27704687/126641752-3163eb95-1cd4-4d99-a61a-4d06d9e7e13e.png) - Regression: Properly force newline in attachment fields ([#22727](https://github.com/RocketChat/Rocket.Chat/pull/22727)) @@ -980,32 +2206,30 @@ - Add `teams.convertToChannel` endpoint ([#22188](https://github.com/RocketChat/Rocket.Chat/pull/22188)) - - Add new `teams.converToChannel` endpoint; - - - Update `ConvertToTeam` modal text (since this action can now be reversed); - + - Add new `teams.converToChannel` endpoint; + - Update `ConvertToTeam` modal text (since this action can now be reversed); - Remove corresponding team memberships when a team is deleted or converted to a channel; - Add setting to configure default role for user on manual registration ([#20650](https://github.com/RocketChat/Rocket.Chat/pull/20650) by [@lucassartor](https://github.com/lucassartor)) - Add an `admin` setting to determine the initial `role` for new users who registered manually (through the register form and via API, not using an authentication service), normally all new users are assigned to the `user` role. - - The setting can be found in `Admin`->`Accounts`->`Registration`. - - ![image](https://user-images.githubusercontent.com/49413772/107252603-47b70900-6a14-11eb-9cc6-df76720b7365.png) - The setting initial value is false, so the default behaviour stays the same while creating a new server or upgrading one. - - https://user-images.githubusercontent.com/49413772/107253220-ddeb2f00-6a14-11eb-85b4-f770dbbe4970.mp4 - + Add an `admin` setting to determine the initial `role` for new users who registered manually (through the register form and via API, not using an authentication service), normally all new users are assigned to the `user` role. + + The setting can be found in `Admin`->`Accounts`->`Registration`. + + ![image](https://user-images.githubusercontent.com/49413772/107252603-47b70900-6a14-11eb-9cc6-df76720b7365.png) + The setting initial value is false, so the default behaviour stays the same while creating a new server or upgrading one. + + https://user-images.githubusercontent.com/49413772/107253220-ddeb2f00-6a14-11eb-85b4-f770dbbe4970.mp4 + Video showing an example of the setting being used and creating an new user with the default roles via API. - Content-Security-Policy for inline scripts ([#20724](https://github.com/RocketChat/Rocket.Chat/pull/20724)) - Security policies were applied for inline scripts cases. Due to the libraries and components we use it is not possible to disable inline styles and images as they would break Oembeds and other libraries. - - - basically the inline scripts were moved to a js file - + Security policies were applied for inline scripts cases. Due to the libraries and components we use it is not possible to disable inline styles and images as they would break Oembeds and other libraries. + + + basically the inline scripts were moved to a js file + and besides that some suggars syntax like `addScript` and `addStyle` were added, this way the application already takes care of inserting the elements and providing the content automatically. - Open modals in side effects outside React ([#22247](https://github.com/RocketChat/Rocket.Chat/pull/22247)) @@ -1021,17 +2245,15 @@ - Add BBB and Jitsi to Team ([#22312](https://github.com/RocketChat/Rocket.Chat/pull/22312)) - Added 2 new settings: - - - `Admin > Video Conference > Big Blue Button > Enable for teams` - + Added 2 new settings: + - `Admin > Video Conference > Big Blue Button > Enable for teams` - `Admin > Video Conference > Jitsi > Enable in teams` - Add debouncing to units selects filters ([#22097](https://github.com/RocketChat/Rocket.Chat/pull/22097)) - Add modal to close chats when tags/comments are not required ([#22245](https://github.com/RocketChat/Rocket.Chat/pull/22245) by [@rafaelblink](https://github.com/rafaelblink)) - When neither tags or comments are required to close a livechat, show this modal instead: + When neither tags or comments are required to close a livechat, show this modal instead: ![Screen Shot 2021-05-20 at 7 33 19 PM](https://user-images.githubusercontent.com/20868078/119057741-6af23c80-b9a3-11eb-902f-f8a7458ad11c.png) - Fallback messages on contextual bar ([#22376](https://github.com/RocketChat/Rocket.Chat/pull/22376)) @@ -1054,10 +2276,10 @@ - Remove differentiation between public x private channels in sidebar ([#22160](https://github.com/RocketChat/Rocket.Chat/pull/22160)) - ### before - ![image](https://user-images.githubusercontent.com/27704687/119752184-e7d55880-be72-11eb-9167-be2f305ddb3f.png) - - ### after + ### before + ![image](https://user-images.githubusercontent.com/27704687/119752184-e7d55880-be72-11eb-9167-be2f305ddb3f.png) + + ### after ![image](https://user-images.githubusercontent.com/27704687/119752125-c8d6c680-be72-11eb-8444-2e0c7cb1c600.png) - Rewrite create direct modal ([#22209](https://github.com/RocketChat/Rocket.Chat/pull/22209)) @@ -1066,8 +2288,8 @@ - Rewrite Create Discussion Modal (only through sidebar) ([#22224](https://github.com/RocketChat/Rocket.Chat/pull/22224)) - This is only available by creating a new discussion when clicking on the sidebar button. Other places will be implemented afterwards. - + This is only available by creating a new discussion when clicking on the sidebar button. Other places will be implemented afterwards. + ![image](https://user-images.githubusercontent.com/40830821/120556093-6af63180-c3d2-11eb-97ea-63c5423049dc.png) - Send only relevant data via WebSocket ([#22258](https://github.com/RocketChat/Rocket.Chat/pull/22258)) @@ -1081,12 +2303,12 @@ - **EE:** Canned responses can't be deleted ([#22095](https://github.com/RocketChat/Rocket.Chat/pull/22095) by [@rafaelblink](https://github.com/rafaelblink)) - Deletion button has been removed from the edition option. - - ## Before - ![image](https://user-images.githubusercontent.com/2493803/119059416-9f1b2c80-b9a6-11eb-933a-4efa1ac0552a.png) - - ### After + Deletion button has been removed from the edition option. + + ## Before + ![image](https://user-images.githubusercontent.com/2493803/119059416-9f1b2c80-b9a6-11eb-933a-4efa1ac0552a.png) + + ### After ![Rocket Chat (2)](https://user-images.githubusercontent.com/2493803/119172517-72b1ef80-ba3c-11eb-9178-04a12176f312.gif) - **ENTERPRISE:** Omnichannel enterprise permissions being added back to its default roles ([#22322](https://github.com/RocketChat/Rocket.Chat/pull/22322)) @@ -1095,19 +2317,19 @@ - **ENTERPRISE:** Prevent Visitor Abandonment after forwarding chat ([#22243](https://github.com/RocketChat/Rocket.Chat/pull/22243)) - Currently the Visitor Abandonment timer isn't affected when the chat is forwarded. However this is affecting the UX in certain situations like eg: A bot forwarding a chat to an human agent - ![image](https://user-images.githubusercontent.com/34130764/120896383-e4925780-c63e-11eb-937e-ffd7c4836159.png) - + Currently the Visitor Abandonment timer isn't affected when the chat is forwarded. However this is affecting the UX in certain situations like eg: A bot forwarding a chat to an human agent + ![image](https://user-images.githubusercontent.com/34130764/120896383-e4925780-c63e-11eb-937e-ffd7c4836159.png) + To solve this issue, we'll now be stoping the Visitor Abandonment timer once a chat is forwarded. - **IMPROVE:** Prevent creation of duplicated roles and new `roles.update` endpoint ([#22279](https://github.com/RocketChat/Rocket.Chat/pull/22279) by [@lucassartor](https://github.com/lucassartor)) - Currently, the action of updating a role is broken: because roles have their `_id` = `name`, when updating a role there's no way to validate if the user is trying to update or create a new role with a name that already exists - which causes wrong behaviors, such as roles with the same name and not being able to update them. - - To proper fix this, this PR looks to change the creation of roles. Now, roles have a unique `_id` value and there's a endpoint to update roles: `/api/v1/roles.update`. - - Doing so, it's possible to validate on both endpoints (`roles.create` and `roles.update`) to not allow roles with duplicated names. - + Currently, the action of updating a role is broken: because roles have their `_id` = `name`, when updating a role there's no way to validate if the user is trying to update or create a new role with a name that already exists - which causes wrong behaviors, such as roles with the same name and not being able to update them. + + To proper fix this, this PR looks to change the creation of roles. Now, roles have a unique `_id` value and there's a endpoint to update roles: `/api/v1/roles.update`. + + Doing so, it's possible to validate on both endpoints (`roles.create` and `roles.update`) to not allow roles with duplicated names. + **OBS:** The unique id changes only reflect new roles, the standard roles (such as admin and user) still have `_id` = `name`, but new roles now **can't** have the same name as them. - `channels.history`, `groups.history` and `im.history` REST endpoints not respecting hide system message config ([#22364](https://github.com/RocketChat/Rocket.Chat/pull/22364)) @@ -1124,10 +2346,10 @@ - Can't delete file from Room's file list ([#22191](https://github.com/RocketChat/Rocket.Chat/pull/22191)) - ### before - ![image](https://user-images.githubusercontent.com/27704687/120215931-bb239700-c20c-11eb-9494-d4bc017df390.png) - - ### after + ### before + ![image](https://user-images.githubusercontent.com/27704687/120215931-bb239700-c20c-11eb-9494-d4bc017df390.png) + + ### after ![image](https://user-images.githubusercontent.com/27704687/120216113-f8882480-c20c-11eb-9afb-b127e66a43da.png) - Cancel button and success toast at Leave Team modal ([#22373](https://github.com/RocketChat/Rocket.Chat/pull/22373)) @@ -1138,10 +2360,10 @@ - Convert and Move team permission ([#22350](https://github.com/RocketChat/Rocket.Chat/pull/22350)) - ### before - https://user-images.githubusercontent.com/45966964/114909360-5c04f100-9e1d-11eb-9363-f308e5d0be68.mp4 - - ### after + ### before + https://user-images.githubusercontent.com/45966964/114909360-5c04f100-9e1d-11eb-9363-f308e5d0be68.mp4 + + ### after https://user-images.githubusercontent.com/45966964/114909388-61fad200-9e1d-11eb-9bbe-114b55954a9f.mp4 - CORS error while interacting with any action button on Livechat ([#22150](https://github.com/RocketChat/Rocket.Chat/pull/22150)) @@ -1160,50 +2382,50 @@ - Members tab visual issues ([#22138](https://github.com/RocketChat/Rocket.Chat/pull/22138)) - ## Before - ![image](https://user-images.githubusercontent.com/27704687/119558283-95fbd800-bd77-11eb-91b4-91821f365bf3.png) - - ## After + ## Before + ![image](https://user-images.githubusercontent.com/27704687/119558283-95fbd800-bd77-11eb-91b4-91821f365bf3.png) + + ## After ![image](https://user-images.githubusercontent.com/27704687/119558120-6947c080-bd77-11eb-8ecb-7fedc07afa82.png) - Memory leak generated by Stream Cast usage ([#22329](https://github.com/RocketChat/Rocket.Chat/pull/22329)) - Stream Cast uses a different approach to broadcast data to the instances, it uses the DDP subscription method that requires a collection on the other side, if no collection exists with the given name `broadcast-stream` it caches in memory waiting for the collection to be set later. The cache is cleared only when a reconnection happens. - + Stream Cast uses a different approach to broadcast data to the instances, it uses the DDP subscription method that requires a collection on the other side, if no collection exists with the given name `broadcast-stream` it caches in memory waiting for the collection to be set later. The cache is cleared only when a reconnection happens. + This PR overrides the function that processes the data for that specific connection, preventing the cache and everything else to be processed since we already have our low-level listener to process the data. - Message box hiding on mobile view (Safari) ([#22212](https://github.com/RocketChat/Rocket.Chat/pull/22212)) - ### before - ![image](https://user-images.githubusercontent.com/27704687/120404256-5b1c1600-c31c-11eb-96e9-860e4132db5f.png) - - ### after + ### before + ![image](https://user-images.githubusercontent.com/27704687/120404256-5b1c1600-c31c-11eb-96e9-860e4132db5f.png) + + ### after ![image](https://user-images.githubusercontent.com/27704687/120404406-acc4a080-c31c-11eb-9efb-c2ad88664fda.png) - Missing burger menu on direct messages ([#22211](https://github.com/RocketChat/Rocket.Chat/pull/22211)) - ### before - ![image](https://user-images.githubusercontent.com/27704687/120403671-09bf5700-c31b-11eb-92a1-a2f589bd85fc.png) - - ### after + ### before + ![image](https://user-images.githubusercontent.com/27704687/120403671-09bf5700-c31b-11eb-92a1-a2f589bd85fc.png) + + ### after ![image](https://user-images.githubusercontent.com/27704687/120403693-1643af80-c31b-11eb-8027-dbdc4f560647.png) - Missing Throbber while thread list is loading ([#22316](https://github.com/RocketChat/Rocket.Chat/pull/22316)) - ### before - List was starting with no results even if there's results: - - ![image](https://user-images.githubusercontent.com/27704687/121606744-1e8ba100-ca25-11eb-9b31-706fb998d05f.png) - - ### after + ### before + List was starting with no results even if there's results: + + ![image](https://user-images.githubusercontent.com/27704687/121606744-1e8ba100-ca25-11eb-9b31-706fb998d05f.png) + + ### after ![image](https://user-images.githubusercontent.com/27704687/121606635-e97f4e80-ca24-11eb-81f7-af8b0cc41c89.png) - Not possible to edit some messages inside threads ([#22325](https://github.com/RocketChat/Rocket.Chat/pull/22325)) - ### Before - ![before](https://user-images.githubusercontent.com/27704687/121755733-4eeb4200-caee-11eb-9d77-1b498c38c478.gif) - - ### After + ### Before + ![before](https://user-images.githubusercontent.com/27704687/121755733-4eeb4200-caee-11eb-9d77-1b498c38c478.gif) + + ### After ![after](https://user-images.githubusercontent.com/27704687/121755736-514d9c00-caee-11eb-9897-78fcead172f2.gif) - Notifications not using user's name ([#22309](https://github.com/RocketChat/Rocket.Chat/pull/22309)) @@ -1230,10 +2452,10 @@ - Sidebar not closing when clicking on a channel ([#22271](https://github.com/RocketChat/Rocket.Chat/pull/22271)) - ### before - ![before](https://user-images.githubusercontent.com/27704687/121074843-c6e20100-c7aa-11eb-88db-76e39b57b064.gif) - - ### after + ### before + ![before](https://user-images.githubusercontent.com/27704687/121074843-c6e20100-c7aa-11eb-88db-76e39b57b064.gif) + + ### after ![after](https://user-images.githubusercontent.com/27704687/121074860-cb0e1e80-c7aa-11eb-9e96-06d75044b763.gif) - Sound notification is not emitted when the Omnichannel chat comes from another department ([#22291](https://github.com/RocketChat/Rocket.Chat/pull/22291)) @@ -1244,9 +2466,9 @@ - Undefined error when forwarding chats to offline department ([#22154](https://github.com/RocketChat/Rocket.Chat/pull/22154) by [@rafaelblink](https://github.com/rafaelblink)) - ![Screen Shot 2021-05-26 at 5 29 17 PM](https://user-images.githubusercontent.com/59577424/119727520-c495b380-be48-11eb-88a2-158017c7ad0a.png) - - Omnichannel agents are facing the error shown above when forwarding chats to offline departments. + ![Screen Shot 2021-05-26 at 5 29 17 PM](https://user-images.githubusercontent.com/59577424/119727520-c495b380-be48-11eb-88a2-158017c7ad0a.png) + + Omnichannel agents are facing the error shown above when forwarding chats to offline departments. The error usually takes place when the routing system algorithm is **Manual Selection**. - Unread bar in channel flash quickly and then disappear ([#22275](https://github.com/RocketChat/Rocket.Chat/pull/22275)) @@ -1277,15 +2499,15 @@ - Chore: Change modals for remove user from team && leave team ([#22141](https://github.com/RocketChat/Rocket.Chat/pull/22141)) - ![image](https://user-images.githubusercontent.com/40830821/119576154-93f14380-bd8e-11eb-8885-f889f2939bf4.png) + ![image](https://user-images.githubusercontent.com/40830821/119576154-93f14380-bd8e-11eb-8885-f889f2939bf4.png) ![image](https://user-images.githubusercontent.com/40830821/119576219-b5eac600-bd8e-11eb-832c-ea7a17a56bdd.png) - Chore: Check PR Title on every submission ([#22140](https://github.com/RocketChat/Rocket.Chat/pull/22140)) - Chore: Enable push gateway only if the server is registered ([#22346](https://github.com/RocketChat/Rocket.Chat/pull/22346) by [@lucassartor](https://github.com/lucassartor)) - Currently, when creating an unregistered server, the default value of the push gateway setting is set to true and is disabled (it can't be changed unless the server is registered). This is a wrong behavior as an unregistered server **can't** use the push gateway. - + Currently, when creating an unregistered server, the default value of the push gateway setting is set to true and is disabled (it can't be changed unless the server is registered). This is a wrong behavior as an unregistered server **can't** use the push gateway. + This PR creates a validation to check if the server is registered when enabling the push gateway. That way, even if the push gateway setting is turned on, but the server is unregistered, the push gateway **won't** work - it will behave like it is off. - Chore: Enforce TypeScript on Storybook ([#22317](https://github.com/RocketChat/Rocket.Chat/pull/22317)) @@ -1302,7 +2524,7 @@ - Chore: Update delete team modal to new design ([#22127](https://github.com/RocketChat/Rocket.Chat/pull/22127)) - Now the modal has only 2 steps (steps 1 and 2 were merged) + Now the modal has only 2 steps (steps 1 and 2 were merged) ![image](https://user-images.githubusercontent.com/40830821/119414580-2e398480-bcc6-11eb-9a47-515568257974.png) - Language update from LingoHub 🤖 on 2021-05-31Z ([#22196](https://github.com/RocketChat/Rocket.Chat/pull/22196)) @@ -1329,10 +2551,10 @@ - Regression: Missing flexDirection on select field ([#22300](https://github.com/RocketChat/Rocket.Chat/pull/22300)) - ### before - ![image](https://user-images.githubusercontent.com/27704687/121425905-532a2a80-c949-11eb-885f-e8ddaf5c8d5c.png) - - ### after + ### before + ![image](https://user-images.githubusercontent.com/27704687/121425905-532a2a80-c949-11eb-885f-e8ddaf5c8d5c.png) + + ### after ![image](https://user-images.githubusercontent.com/27704687/121425770-283fd680-c949-11eb-8d94-86886f174599.png) - Regression: RoomProvider using wrong types ([#22370](https://github.com/RocketChat/Rocket.Chat/pull/22370)) @@ -1473,50 +2695,50 @@ - **ENTERPRISE:** Introduce Load Rotation routing algorithm for Omnichannel ([#22090](https://github.com/RocketChat/Rocket.Chat/pull/22090) by [@rafaelblink](https://github.com/rafaelblink)) - This PR introduces a new Auto Chat Distribution (ACD) algorithm for Omnichannel: **Load Rotation**. - The algorithm distributes chats to agents one by one, which means that when a new chat arrives, the agent with the oldest routing assignment time will be selected to serve the chat, regardless of the number of chats in progress each agent has. - + This PR introduces a new Auto Chat Distribution (ACD) algorithm for Omnichannel: **Load Rotation**. + The algorithm distributes chats to agents one by one, which means that when a new chat arrives, the agent with the oldest routing assignment time will be selected to serve the chat, regardless of the number of chats in progress each agent has. + ![Screen Shot 2021-05-20 at 5 17 40 PM](https://user-images.githubusercontent.com/59577424/119043752-c61a3400-b98f-11eb-8543-f3176879af1d.png) - Back button for Omnichannel ([#21647](https://github.com/RocketChat/Rocket.Chat/pull/21647) by [@rafaelblink](https://github.com/rafaelblink)) - New Message Parser ([#21962](https://github.com/RocketChat/Rocket.Chat/pull/21962)) - The objective is to put an end to the confusion that we face having multiple parsers, and the problems that this brings, it is still experimental then users need to choose to use it. - - The benefits are multiple. no more unexpected cases or grammatical collisions (in addition to more flexible nested cases like bold within link labels). - Besides, we no longer render raw html, instead we use components, so the xss attacks are over (the easy ones at least). Without further discoveries and at the fronted, we only reder what is delivered thus improving our performance. + The objective is to put an end to the confusion that we face having multiple parsers, and the problems that this brings, it is still experimental then users need to choose to use it. + + The benefits are multiple. no more unexpected cases or grammatical collisions (in addition to more flexible nested cases like bold within link labels). + Besides, we no longer render raw html, instead we use components, so the xss attacks are over (the easy ones at least). Without further discoveries and at the fronted, we only reder what is delivered thus improving our performance. This can be used in multiple places, (message, alert, sidenav and in the entire mobile application.) - Option to notify failed login attempts to a channel ([#21968](https://github.com/RocketChat/Rocket.Chat/pull/21968)) - Option to prevent users from using Invisible status ([#20084](https://github.com/RocketChat/Rocket.Chat/pull/20084) by [@lucassartor](https://github.com/lucassartor)) - Add an `admin` option to allow/disallow the `Invisible` status option from all users. This option is available in the `Accounts` section. - - ![2021-01-06-11-55-22](https://user-images.githubusercontent.com/49413772/103782988-ebc52300-5016-11eb-8a29-dd540c21e11c.gif) - - If the option is turned off, the `users.setStatus` endpoint is also restricted from users trying to change their status to `Invisible`, throwing the following error: - ```json - { - "success": false, - "error": "Invisible status is disabled [error-not-allowed]", - "stack": "Error: Invisible status is disabled [error-not-allowed]\n at DDPCommon.MethodInvocation. (app/api/server/v1/users.js:425:13)\n at packages/dispatch_run-as-user.js:211:14\n at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1234:12)\n at Object.Meteor.runAsUser (packages/dispatch_run-as-user.js:210:33)\n at Object.post (app/api/server/v1/users.js:415:10)\n at app/api/server/api.js:394:82\n at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1234:12)\n at Object._internalRouteActionHandler [as action] (app/api/server/api.js:394:39)\n at Route.share.Route.Route._callEndpoint (packages/nimble_restivus/lib/route.coffee:150:32)\n at packages/nimble_restivus/lib/route.coffee:59:33\n at packages/simple_json-routes.js:98:9", - "errorType": "error-not-allowed", - "details": { - "method": "users.setStatus" - } - } + Add an `admin` option to allow/disallow the `Invisible` status option from all users. This option is available in the `Accounts` section. + + ![2021-01-06-11-55-22](https://user-images.githubusercontent.com/49413772/103782988-ebc52300-5016-11eb-8a29-dd540c21e11c.gif) + + If the option is turned off, the `users.setStatus` endpoint is also restricted from users trying to change their status to `Invisible`, throwing the following error: + ```json + { + "success": false, + "error": "Invisible status is disabled [error-not-allowed]", + "stack": "Error: Invisible status is disabled [error-not-allowed]\n at DDPCommon.MethodInvocation. (app/api/server/v1/users.js:425:13)\n at packages/dispatch_run-as-user.js:211:14\n at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1234:12)\n at Object.Meteor.runAsUser (packages/dispatch_run-as-user.js:210:33)\n at Object.post (app/api/server/v1/users.js:415:10)\n at app/api/server/api.js:394:82\n at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1234:12)\n at Object._internalRouteActionHandler [as action] (app/api/server/api.js:394:39)\n at Route.share.Route.Route._callEndpoint (packages/nimble_restivus/lib/route.coffee:150:32)\n at packages/nimble_restivus/lib/route.coffee:59:33\n at packages/simple_json-routes.js:98:9", + "errorType": "error-not-allowed", + "details": { + "method": "users.setStatus" + } + } ``` - Paginated and Filtered selects on new/edit unit ([#22052](https://github.com/RocketChat/Rocket.Chat/pull/22052) by [@rafaelblink](https://github.com/rafaelblink)) - REQUIRES https://github.com/RocketChat/Rocket.Chat.Fuselage/pull/447 - - Adds infinite scrolling selects to the units edit/create with the ability to be filtered by text as well - - ![Screen Shot 2021-05-17 at 9 24 19 AM](https://user-images.githubusercontent.com/20868078/118487999-abc32a80-b6f1-11eb-8d58-d031111ea0fb.png) - + REQUIRES https://github.com/RocketChat/Rocket.Chat.Fuselage/pull/447 + + Adds infinite scrolling selects to the units edit/create with the ability to be filtered by text as well + + ![Screen Shot 2021-05-17 at 9 24 19 AM](https://user-images.githubusercontent.com/20868078/118487999-abc32a80-b6f1-11eb-8d58-d031111ea0fb.png) + This Affects the monitors and departments inputs - Remove exif metadata from uploaded files ([#22044](https://github.com/RocketChat/Rocket.Chat/pull/22044)) @@ -1544,17 +2766,13 @@ - Inconsistent and misleading 2FA settings ([#22042](https://github.com/RocketChat/Rocket.Chat/pull/22042) by [@lucassartor](https://github.com/lucassartor)) - Currently, there are some inconsistencies and incorrect behaviors on the 2FA settings, such as: - - - - When disabling the TOTP 2FA, all 2FA are disabled; - - - There are no option to disable only the TOTP 2FA; - - - If 2FA are disabled, the other settings aren't blocked (the e-mail 2FA setting, for example); - - - It lacks some labels to warn the user of some specific 2FA options. - + Currently, there are some inconsistencies and incorrect behaviors on the 2FA settings, such as: + + - When disabling the TOTP 2FA, all 2FA are disabled; + - There are no option to disable only the TOTP 2FA; + - If 2FA are disabled, the other settings aren't blocked (the e-mail 2FA setting, for example); + - It lacks some labels to warn the user of some specific 2FA options. + This PR looks to fix those issues. - LDAP port setting input type to allow only numbers ([#21912](https://github.com/RocketChat/Rocket.Chat/pull/21912) by [@Deepak-learner](https://github.com/Deepak-learner)) @@ -1576,29 +2794,29 @@ - **APPS:** Scheduler duplicating recurrent tasks after server restart ([#21866](https://github.com/RocketChat/Rocket.Chat/pull/21866)) - Reintroduces the old method for creating recurring tasks in the apps' scheduler bridge to ensure tasks won't be duplicated. - - By introducing the [`skipImmediate` property option](https://github.com/RocketChat/Rocket.Chat/pull/21353) at the [`scheduleRecurring`](https://github.com/RocketChat/Rocket.Chat/blob/f8171f464ed8a7487795651767695fb33a1c709e/app/apps/server/bridges/scheduler.js#L119) method, the `every` method from _agenda.js_, which ensured no duplicates were created, was removed in favor of a more manual procedure. The new procedure was not taking into account the management of duplicates and as a result multiple copies of the same task could be created and they would get executed at the same time. - + Reintroduces the old method for creating recurring tasks in the apps' scheduler bridge to ensure tasks won't be duplicated. + + By introducing the [`skipImmediate` property option](https://github.com/RocketChat/Rocket.Chat/pull/21353) at the [`scheduleRecurring`](https://github.com/RocketChat/Rocket.Chat/blob/f8171f464ed8a7487795651767695fb33a1c709e/app/apps/server/bridges/scheduler.js#L119) method, the `every` method from _agenda.js_, which ensured no duplicates were created, was removed in favor of a more manual procedure. The new procedure was not taking into account the management of duplicates and as a result multiple copies of the same task could be created and they would get executed at the same time. + In the case of server restarts, every time this event happened and the app had the `startupSetting` configured to use _recurring tasks_, they would get recreated the same number of times. In the case of a server that restarts frequently (_n_ times), there would be the same (_n_) number of tasks duplicated (and running) in the system. - **ENTERPRISE:** Omnichannel Monitors can't forward chats to departments that they are not supervising ([#22128](https://github.com/RocketChat/Rocket.Chat/pull/22128)) - Currently, Omnichannel Monitors just can't forward chats to a department that is part of a `Business Unit` they're not supervising. This issue is causing critical problems on customer operations since this behaviour is not by design. - The reason this issue is taking place is that, by design, Monitors just have access to departments related to the `Business Units` they're monitoring, but this restriction is designed only for Omnichannel management areas, which means in case the monitor is, also, an agent, they're supposed to be able to forward a chat to any available departments regardless the `Business Units` it's associated with. + Currently, Omnichannel Monitors just can't forward chats to a department that is part of a `Business Unit` they're not supervising. This issue is causing critical problems on customer operations since this behaviour is not by design. + The reason this issue is taking place is that, by design, Monitors just have access to departments related to the `Business Units` they're monitoring, but this restriction is designed only for Omnichannel management areas, which means in case the monitor is, also, an agent, they're supposed to be able to forward a chat to any available departments regardless the `Business Units` it's associated with. So, initially, the restriction was implemented on the `Department Model` and, now, we're implementing the logic properly and introducing a new parameter to department endpoints, so the client will define which type of departments it needs. - **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. @@ -1624,18 +2842,18 @@ - Correcting a the wrong Archived label in edit room ([#21717](https://github.com/RocketChat/Rocket.Chat/pull/21717) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - ![image](https://user-images.githubusercontent.com/45966964/116584997-3cd78a80-a918-11eb-81fa-8a7eb5318ae9.png) - + ![image](https://user-images.githubusercontent.com/45966964/116584997-3cd78a80-a918-11eb-81fa-8a7eb5318ae9.png) + A label exists for Archived, and it has not been used. So I replaced it with the existing one. the label 'Archived' does not exist. - Custom OAuth not being completely deleted ([#21637](https://github.com/RocketChat/Rocket.Chat/pull/21637) by [@siva2204](https://github.com/siva2204)) - Directory Table's Sort Function ([#21921](https://github.com/RocketChat/Rocket.Chat/pull/21921)) - ### TableRow Margin Issue: - ![image](https://user-images.githubusercontent.com/27704687/116907348-d6a07f80-ac17-11eb-9411-edfe0906bfe1.png) - - ### Table Sort Action Issue: + ### TableRow Margin Issue: + ![image](https://user-images.githubusercontent.com/27704687/116907348-d6a07f80-ac17-11eb-9411-edfe0906bfe1.png) + + ### Table Sort Action Issue: ![directory](https://user-images.githubusercontent.com/27704687/116907441-f20b8a80-ac17-11eb-8790-bfce19e89a67.gif) - Discussion names showing a random value ([#22172](https://github.com/RocketChat/Rocket.Chat/pull/22172)) @@ -1646,54 +2864,54 @@ - Emails being sent with HTML entities getting escaped multiple times ([#21994](https://github.com/RocketChat/Rocket.Chat/pull/21994) by [@bhavayAnand9](https://github.com/bhavayAnand9)) - fixes an issue where if password contains special HTML character like &, in the email it would end up something like `&amp;` - - - password was going through multiple escapeHTML function calls - `secure&123 => secure&123 => secure&amp;123 + fixes an issue where if password contains special HTML character like &, in the email it would end up something like `&amp;` + + + password was going through multiple escapeHTML function calls + `secure&123 => secure&123 => secure&amp;123 ` - Error when you look at the members list of a room in which you are not a member ([#21952](https://github.com/RocketChat/Rocket.Chat/pull/21952) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - Before, when you look at the members of a room in which you are not a member the app crashed, i corrected this problem. - Indeed, there was a check on each currentSubscription. to see if it was not undefined except on currentSubscription.blocker - + Before, when you look at the members of a room in which you are not a member the app crashed, i corrected this problem. + Indeed, there was a check on each currentSubscription. to see if it was not undefined except on currentSubscription.blocker + https://user-images.githubusercontent.com/45966964/117087470-d3101400-ad4f-11eb-8f44-0ebca830a4d8.mp4 - errors when viewing a room that you're not subscribed to ([#21984](https://github.com/RocketChat/Rocket.Chat/pull/21984)) - Files list will not show deleted files. ([#21732](https://github.com/RocketChat/Rocket.Chat/pull/21732) by [@Darshilp326](https://github.com/Darshilp326)) - When you delete files from the header option, deleted files will not be shown. - + When you delete files from the header option, deleted files will not be shown. + https://user-images.githubusercontent.com/55157259/115730786-38552400-a3a4-11eb-9684-7f510920db66.mp4 - Fixed the fact that when a team was deleted, not all channels were unlinked from the team ([#21942](https://github.com/RocketChat/Rocket.Chat/pull/21942) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - Fixed the fact that when a team was deleted, not all channels were unlinked from the team. Only the first room of the rooms list was unlinked. - - After the fix, there is nos more errors: - - + Fixed the fact that when a team was deleted, not all channels were unlinked from the team. Only the first room of the rooms list was unlinked. + + After the fix, there is nos more errors: + + https://user-images.githubusercontent.com/45966964/117055182-2a47c180-ad1b-11eb-806f-07fb3fa7ec12.mp4 - Fixing Jitsi call ended Issue. ([#21808](https://github.com/RocketChat/Rocket.Chat/pull/21808)) - The new rewrite in react of contextual call component broke the Jitsi "click to join" messages. The issue being after 10 seconds of initiating the call, the message "click to join" always returned "Call Ended" even if the call was still going on. - This was due to the fact that after closing the contextual bar, the react component gets unmounted and we are not able to keep track of ongoing call and increase jitsi room timeout. - - This PR solves this issue by using the setInterval methods on component will unmount. When the call component unmounts, we keep on checking the state of jitsi call and based on conditions increase the jitsi room timeout. After the call is ended all setInterval calls are closed. - + The new rewrite in react of contextual call component broke the Jitsi "click to join" messages. The issue being after 10 seconds of initiating the call, the message "click to join" always returned "Call Ended" even if the call was still going on. + This was due to the fact that after closing the contextual bar, the react component gets unmounted and we are not able to keep track of ongoing call and increase jitsi room timeout. + + This PR solves this issue by using the setInterval methods on component will unmount. When the call component unmounts, we keep on checking the state of jitsi call and based on conditions increase the jitsi room timeout. After the call is ended all setInterval calls are closed. + This PR also removes the implementation of HEARTBEAT events of JitsiBridge. This is because this is no longer needed and all logic is being taken care of by the unmount function. - Handle NPS errors instead of throwing them ([#21945](https://github.com/RocketChat/Rocket.Chat/pull/21945)) - Header Tag Visual Issues ([#21991](https://github.com/RocketChat/Rocket.Chat/pull/21991)) - ### Normal - ![image](https://user-images.githubusercontent.com/27704687/117504793-69635600-af59-11eb-8b79-9d8f631490ee.png) - - ### Hover + ### Normal + ![image](https://user-images.githubusercontent.com/27704687/117504793-69635600-af59-11eb-8b79-9d8f631490ee.png) + + ### Hover ![image](https://user-images.githubusercontent.com/27704687/117504934-97489a80-af59-11eb-87c3-0a62731e9ce3.png) - Horizontal scrollbar not showing on tables ([#21852](https://github.com/RocketChat/Rocket.Chat/pull/21852)) @@ -1702,17 +2920,17 @@ - iFrame size on embedded videos ([#21992](https://github.com/RocketChat/Rocket.Chat/pull/21992)) - ### Before - ![image](https://user-images.githubusercontent.com/27704687/117508802-8bf86d80-af5f-11eb-9eb8-29e55b73eac5.png) - - ### After + ### Before + ![image](https://user-images.githubusercontent.com/27704687/117508802-8bf86d80-af5f-11eb-9eb8-29e55b73eac5.png) + + ### After ![image](https://user-images.githubusercontent.com/27704687/117508870-a4688800-af5f-11eb-9176-7f24de5fc424.png) - Incorrect error message when opening channel in anonymous read ([#22066](https://github.com/RocketChat/Rocket.Chat/pull/22066) by [@lucassartor](https://github.com/lucassartor)) - Every time you open a public channel with threads in it when using anonymous read an `Incorrect User` error will be thrown. - This is an incorrect behaviour as everything that is public should be valid for an anonymous user. - + Every time you open a public channel with threads in it when using anonymous read an `Incorrect User` error will be thrown. + This is an incorrect behaviour as everything that is public should be valid for an anonymous user. + Some files are adapted to that and have already removed this kind of incorrect error, but there are some that need some fix, this PR aims to do that. - Incorrect Team's Info spacing ([#22021](https://github.com/RocketChat/Rocket.Chat/pull/22021)) @@ -1725,21 +2943,19 @@ - Make the FR translation consistent with the 'room' translation + typos ([#21913](https://github.com/RocketChat/Rocket.Chat/pull/21913) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - In the FR translation files, there were two terms that were used to refer to **'room'**: - - - 'salon' (149 times used) - - ![image](https://user-images.githubusercontent.com/45966964/116829860-ac62a980-aba6-11eb-8212-e6f15ed0af82.png) - - - - 'salle' (46 times used) - - ![image](https://user-images.githubusercontent.com/45966964/116829871-be444c80-aba6-11eb-9b42-e213fee6586a.png) - - The problem is that both were used in the same context and sometimes even in the same option list. - However, since 'salon' is a better translation and was also in the majority, I used the translation 'salon' wherever 'salle' was marked. - - For example: + In the FR translation files, there were two terms that were used to refer to **'room'**: + - 'salon' (149 times used) + + ![image](https://user-images.githubusercontent.com/45966964/116829860-ac62a980-aba6-11eb-8212-e6f15ed0af82.png) + + - 'salle' (46 times used) + + ![image](https://user-images.githubusercontent.com/45966964/116829871-be444c80-aba6-11eb-9b42-e213fee6586a.png) + + The problem is that both were used in the same context and sometimes even in the same option list. + However, since 'salon' is a better translation and was also in the majority, I used the translation 'salon' wherever 'salle' was marked. + + For example: ![image](https://user-images.githubusercontent.com/45966964/116830523-1da45b80-abab-11eb-81f8-5225d51cecc6.png) - Maximum 25 channels can be loaded in the teams' channels list ([#21708](https://github.com/RocketChat/Rocket.Chat/pull/21708) by [@Jeanstaquet](https://github.com/Jeanstaquet)) @@ -1754,8 +2970,8 @@ - No warning message is sent when user is removed from a team's main channel ([#21949](https://github.com/RocketChat/Rocket.Chat/pull/21949)) - - Send a warning message to a team's main channel when a user is removed from the team; - - Trigger events while removing a user from a team's main channel; + - Send a warning message to a team's main channel when a user is removed from the team; + - Trigger events while removing a user from a team's main channel; - Fix `usersCount` field in the team's main room when a user is removed from the team (`usersCount` is now decreased by 1). - Not possible accept video call if "Hide right sidebar with click" is enabled ([#22175](https://github.com/RocketChat/Rocket.Chat/pull/22175)) @@ -1776,14 +2992,14 @@ - Prevent the userInfo tab to return 'User not found' each time if a certain member of a DM group has been deleted ([#21970](https://github.com/RocketChat/Rocket.Chat/pull/21970) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - Prevent the userInfo tab to return 'User not found' if a member of a DM group has been deleted. - This happens if the user that has been deleted is the one originally displayed on the userInfo tab in a DM group with >2 users. - + Prevent the userInfo tab to return 'User not found' if a member of a DM group has been deleted. + This happens if the user that has been deleted is the one originally displayed on the userInfo tab in a DM group with >2 users. + https://user-images.githubusercontent.com/45966964/117221081-db785580-ae08-11eb-9b33-2314a99eb037.mp4 - Prune messages not cleaning up unread threads ([#21326](https://github.com/RocketChat/Rocket.Chat/pull/21326) by [@renancleyson-dev](https://github.com/renancleyson-dev)) - Fixes permanent unread messages when admin prune at least two different thread messages in the room that were unread by some user. + Fixes permanent unread messages when admin prune at least two different thread messages in the room that were unread by some user. ![screencapture-localhost-3000-channel-general-thread-2021-03-26-13_17_16](https://user-images.githubusercontent.com/43624243/112678973-62b9cd00-8e4a-11eb-9af9-56f17cc66baf.png) - Redirect on remove user from channel by user profile tab ([#21951](https://github.com/RocketChat/Rocket.Chat/pull/21951)) @@ -1794,8 +3010,8 @@ - Removed fields from User Info for which the user doesn't have permissions. ([#20923](https://github.com/RocketChat/Rocket.Chat/pull/20923) by [@Darshilp326](https://github.com/Darshilp326)) - Removed LastLogin, CreatedAt and Roles for users who don't have permission. - + Removed LastLogin, CreatedAt and Roles for users who don't have permission. + https://user-images.githubusercontent.com/55157259/109381351-f2c62e80-78ff-11eb-9289-e11072bf62f8.mp4 - Replace `query` param by `name`, `username` and `status` on the `teams.members` endpoint ([#21539](https://github.com/RocketChat/Rocket.Chat/pull/21539)) @@ -1808,39 +3024,39 @@ - Unable to edit a 'direct' room setting in the admin due to the room name ([#21636](https://github.com/RocketChat/Rocket.Chat/pull/21636) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - When you are in the admin and want to change a room 'd' setting, it doesn't work because it takes into account the name that is set automatically and therefore tries to save that name. Since the name is not valid and should not be registered, we cannot change the setting for the 'd' room. - I made sure that when you want to change a setting in a 'd' room, that you don't take the name into account - - - https://user-images.githubusercontent.com/45966964/115150919-cd85af00-a06a-11eb-9667-ef3dcfc5adb6.mp4 - - + When you are in the admin and want to change a room 'd' setting, it doesn't work because it takes into account the name that is set automatically and therefore tries to save that name. Since the name is not valid and should not be registered, we cannot change the setting for the 'd' room. + I made sure that when you want to change a setting in a 'd' room, that you don't take the name into account + + + https://user-images.githubusercontent.com/45966964/115150919-cd85af00-a06a-11eb-9667-ef3dcfc5adb6.mp4 + + Behind the scene, the name is not saved - Unable to edit a user who does not have an email via the admin or via the user's profile ([#21626](https://github.com/RocketChat/Rocket.Chat/pull/21626) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - If a user does not have an email address, they cannot change it via their profile or via the admin. I fixed this issue. I have created several profiles and there was one that didn't have an email, I don't know how I did it, I am working on it. I had not modified the db to delete his email, hence the fix - - in admin - - https://user-images.githubusercontent.com/45966964/115112617-9b9b1c80-9f86-11eb-8e3a-950c3c1a1746.mp4 - - - - in the user profile - + If a user does not have an email address, they cannot change it via their profile or via the admin. I fixed this issue. I have created several profiles and there was one that didn't have an email, I don't know how I did it, I am working on it. I had not modified the db to delete his email, hence the fix + + in admin + + https://user-images.githubusercontent.com/45966964/115112617-9b9b1c80-9f86-11eb-8e3a-950c3c1a1746.mp4 + + + + in the user profile + https://user-images.githubusercontent.com/45966964/115112620-a0f86700-9f86-11eb-97b1-56eaba42216b.mp4 - Unable to get channels, sort by most recent message ([#21701](https://github.com/RocketChat/Rocket.Chat/pull/21701) by [@sumukhah](https://github.com/sumukhah)) - Unable to update app manually ([#21215](https://github.com/RocketChat/Rocket.Chat/pull/21215)) - It allows for update of apps using a zip file. - - When installing apps using the zip file, either by url or the file form, if the app was already installed, an error would be thrown stating the condition and forbidding the installation. Now, when sending a zip file of an app that is already installed, the user is presented with the following modal: - - ![2021-04-30-113936_627x235_scrot](https://user-images.githubusercontent.com/733282/116711383-2cbbbb80-a9a9-11eb-8c77-22d6802cb9f5.png) - + It allows for update of apps using a zip file. + + When installing apps using the zip file, either by url or the file form, if the app was already installed, an error would be thrown stating the condition and forbidding the installation. Now, when sending a zip file of an app that is already installed, the user is presented with the following modal: + + ![2021-04-30-113936_627x235_scrot](https://user-images.githubusercontent.com/733282/116711383-2cbbbb80-a9a9-11eb-8c77-22d6802cb9f5.png) + If the app also requires permissions to be reviewed, the modal that handles permission reviews will be shown after this one is accepted. - Unpin message reactivity ([#22029](https://github.com/RocketChat/Rocket.Chat/pull/22029)) @@ -1851,20 +3067,20 @@ - User Impersonation through sendMessage API ([#20391](https://github.com/RocketChat/Rocket.Chat/pull/20391) by [@lucassartor](https://github.com/lucassartor)) - Create a new permission: `message-impersonate`. For new installs only bot role will have the permission and for updating installs the permission will also be given to user role, so it won't break running deployments. - - If a message is being sent with `avatar` or `alias` properties, it validates if the sender has the `message-impersonate` permission, if not, an error is throwed: - ```json - { - "success": false, - "error": "Not enough permission", - "stack": "Error: Not enough permission\n ..." - } + Create a new permission: `message-impersonate`. For new installs only bot role will have the permission and for updating installs the permission will also be given to user role, so it won't break running deployments. + + If a message is being sent with `avatar` or `alias` properties, it validates if the sender has the `message-impersonate` permission, if not, an error is throwed: + ```json + { + "success": false, + "error": "Not enough permission", + "stack": "Error: Not enough permission\n ..." + } ``` -- 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. + 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. - When closing chats a comment is always required ([#21947](https://github.com/RocketChat/Rocket.Chat/pull/21947)) @@ -1879,8 +3095,8 @@ - Wrong icon on "Move to team" option in the channel info actions ([#21944](https://github.com/RocketChat/Rocket.Chat/pull/21944)) - ![image](https://user-images.githubusercontent.com/40830821/117061659-d9bf6c80-acf8-11eb-8e29-be47e702dedd.png) - + ![image](https://user-images.githubusercontent.com/40830821/117061659-d9bf6c80-acf8-11eb-8e29-be47e702dedd.png) + Depends on https://github.com/RocketChat/Rocket.Chat.Fuselage/pull/444
@@ -1897,10 +3113,8 @@ - Add two more test cases to the slash-command test suite ([#21317](https://github.com/RocketChat/Rocket.Chat/pull/21317) by [@EduardoPicolo](https://github.com/EduardoPicolo)) - Added two more test cases to the slash-command test suite: - - - 'should return an error when the command does not exist''; - + Added two more test cases to the slash-command test suite: + - 'should return an error when the command does not exist''; - 'should return an error when no command is provided'; - Bump actions/stale from v3.0.8 to v3.0.18 ([#21877](https://github.com/RocketChat/Rocket.Chat/pull/21877) by [@dependabot[bot]](https://github.com/dependabot[bot])) @@ -1935,9 +3149,9 @@ - i18n: Add missing translation string in account preference ([#21448](https://github.com/RocketChat/Rocket.Chat/pull/21448) by [@sumukhah](https://github.com/sumukhah)) - "Test Desktop Notifications" was missing in translation, Added to the file. - Screenshot 2021-04-05 at 3 58 01 PM - + "Test Desktop Notifications" was missing in translation, Added to the file. + Screenshot 2021-04-05 at 3 58 01 PM + Screenshot 2021-04-05 at 3 58 32 PM - i18n: Correct a typo in German ([#21711](https://github.com/RocketChat/Rocket.Chat/pull/21711) by [@Jeanstaquet](https://github.com/Jeanstaquet)) @@ -1964,10 +3178,10 @@ - Regression: discussions display on sidebar ([#22157](https://github.com/RocketChat/Rocket.Chat/pull/22157)) - ### group by type active - ![image](https://user-images.githubusercontent.com/27704687/119741996-37a92500-be5d-11eb-8b36-4067a7a229f1.png) - - ### group by type inactive + ### group by type active + ![image](https://user-images.githubusercontent.com/27704687/119741996-37a92500-be5d-11eb-8b36-4067a7a229f1.png) + + ### group by type inactive ![image](https://user-images.githubusercontent.com/27704687/119742054-56a7b700-be5d-11eb-8810-e31d4216f573.png) - regression: fix departments with empty ancestors not being returned ([#22068](https://github.com/RocketChat/Rocket.Chat/pull/22068)) @@ -1978,8 +3192,8 @@ - regression: Fix Users list in the Administration ([#22034](https://github.com/RocketChat/Rocket.Chat/pull/22034) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - The app crashed if no custom fields for user profiles have been created by the admin. I fixed this issue. This bug was introduced by a recent commit. - + The app crashed if no custom fields for user profiles have been created by the admin. I fixed this issue. This bug was introduced by a recent commit. + https://user-images.githubusercontent.com/45966964/118210838-5b3a9b80-b46b-11eb-9fe5-5b813848190c.mp4 - Regression: Improve migration 225 ([#22099](https://github.com/RocketChat/Rocket.Chat/pull/22099)) @@ -1996,7 +3210,7 @@ - Regression: not allowed to edit roles due to a new verification ([#22159](https://github.com/RocketChat/Rocket.Chat/pull/22159)) - introduced by https://github.com/RocketChat/Rocket.Chat/pull/21905 + introduced by https://github.com/RocketChat/Rocket.Chat/pull/21905 ![Peek 2021-05-26 22-21](https://user-images.githubusercontent.com/27704687/119750970-b9567e00-be70-11eb-9d52-04c8595950df.gif) - regression: Select Team Modal margin ([#22030](https://github.com/RocketChat/Rocket.Chat/pull/22030)) @@ -2007,10 +3221,10 @@ - Regression: Visual issue on sort list item ([#22158](https://github.com/RocketChat/Rocket.Chat/pull/22158)) - ### before - ![image](https://user-images.githubusercontent.com/27704687/119743703-d84d1400-be60-11eb-97cc-c8256b2c8b07.png) - - ### after + ### before + ![image](https://user-images.githubusercontent.com/27704687/119743703-d84d1400-be60-11eb-97cc-c8256b2c8b07.png) + + ### after ![image](https://user-images.githubusercontent.com/27704687/119743638-b18edd80-be60-11eb-828d-22cc5e1b2f5b.png) - Release 3.14.2 ([#22135](https://github.com/RocketChat/Rocket.Chat/pull/22135)) @@ -2047,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 🤓 @@ -2068,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 👩‍💻👨‍💻` @@ -2196,12 +3410,12 @@ - Paginated and Filtered selects on new/edit unit ([#22052](https://github.com/RocketChat/Rocket.Chat/pull/22052) by [@rafaelblink](https://github.com/rafaelblink)) - REQUIRES https://github.com/RocketChat/Rocket.Chat.Fuselage/pull/447 - - Adds infinite scrolling selects to the units edit/create with the ability to be filtered by text as well - - ![Screen Shot 2021-05-17 at 9 24 19 AM](https://user-images.githubusercontent.com/20868078/118487999-abc32a80-b6f1-11eb-8d58-d031111ea0fb.png) - + REQUIRES https://github.com/RocketChat/Rocket.Chat.Fuselage/pull/447 + + Adds infinite scrolling selects to the units edit/create with the ability to be filtered by text as well + + ![Screen Shot 2021-05-17 at 9 24 19 AM](https://user-images.githubusercontent.com/20868078/118487999-abc32a80-b6f1-11eb-8d58-d031111ea0fb.png) + This Affects the monitors and departments inputs ### 🚀 Improvements @@ -2277,24 +3491,18 @@ - New set of rules for client code ([#21318](https://github.com/RocketChat/Rocket.Chat/pull/21318)) - This _small_ PR does the following: - - - - Now **React** is the web client's first-class citizen, being **loaded before Blaze**. Thus, `BlazeLayout` calls render templates inside of a React component (`BlazeLayoutWrapper`); - - - Main client startup code, including polyfills, is written in **TypeScript**; - - - At the moment, routes are treated as regular startup code; it's expected that `FlowRouter` will be deprecated in favor of a new routing library; - - - **React** was updated to major version **17**, deprecating the usage of `React` as namespace (e.g. use `memo()` instead of `React.memo()`); - - - The `client/` and `ee/client/` directory are linted with a **custom ESLint configuration** that includes: - - **Prettier**; - - `react-hooks/*` rules for TypeScript files; - - `react/no-multi-comp`, enforcing the rule of **one single React component per module**; - - `react/display-name`, which enforces that **React components must have a name for debugging**; - - `import/named`, avoiding broken named imports. - + This _small_ PR does the following: + + - Now **React** is the web client's first-class citizen, being **loaded before Blaze**. Thus, `BlazeLayout` calls render templates inside of a React component (`BlazeLayoutWrapper`); + - Main client startup code, including polyfills, is written in **TypeScript**; + - At the moment, routes are treated as regular startup code; it's expected that `FlowRouter` will be deprecated in favor of a new routing library; + - **React** was updated to major version **17**, deprecating the usage of `React` as namespace (e.g. use `memo()` instead of `React.memo()`); + - The `client/` and `ee/client/` directory are linted with a **custom ESLint configuration** that includes: + - **Prettier**; + - `react-hooks/*` rules for TypeScript files; + - `react/no-multi-comp`, enforcing the rule of **one single React component per module**; + - `react/display-name`, which enforces that **React components must have a name for debugging**; + - `import/named`, avoiding broken named imports. - A bunch of components were refactored to match the new ESLint rules. - On Hold system messages ([#21360](https://github.com/RocketChat/Rocket.Chat/pull/21360) by [@rafaelblink](https://github.com/rafaelblink)) @@ -2303,15 +3511,12 @@ - Password history ([#21607](https://github.com/RocketChat/Rocket.Chat/pull/21607)) - - Store each user's previously used passwords in a `passwordHistory` field (in the `users` record); - - - Users' previously used passwords are stored in their `passwordHistory` even when the setting is disabled; - - - Add "Password History" setting -- when enabled, it blocks users from reusing their most recent passwords; - - - Convert `comparePassword` file to TypeScript. - - ![Password_Change](https://user-images.githubusercontent.com/36537004/115035168-ac726200-9ea2-11eb-93c6-fc8182ba5f3f.png) + - Store each user's previously used passwords in a `passwordHistory` field (in the `users` record); + - Users' previously used passwords are stored in their `passwordHistory` even when the setting is disabled; + - Add "Password History" setting -- when enabled, it blocks users from reusing their most recent passwords; + - Convert `comparePassword` file to TypeScript. + + ![Password_Change](https://user-images.githubusercontent.com/36537004/115035168-ac726200-9ea2-11eb-93c6-fc8182ba5f3f.png) ![Password_History](https://user-images.githubusercontent.com/36537004/115035175-ad0af880-9ea2-11eb-9f40-94c6327a9854.png) - REST endpoint `teams.update` ([#21134](https://github.com/RocketChat/Rocket.Chat/pull/21134) by [@g-thome](https://github.com/g-thome)) @@ -2329,18 +3534,14 @@ - Add error messages to the creation of channels or usernames containing reserved words ([#21016](https://github.com/RocketChat/Rocket.Chat/pull/21016)) - Display error messages when the user attempts to create or edit users' or channels' names with any of the following words (**case-insensitive**): - - - admin; - - - administrator; - - - system; - - - user. - ![create-channel](https://user-images.githubusercontent.com/36537004/110132223-b421ef80-7da9-11eb-82bc-f0d4e1df967f.png) - ![register-username](https://user-images.githubusercontent.com/36537004/110132234-b71ce000-7da9-11eb-904e-580233625951.png) - ![change-channel](https://user-images.githubusercontent.com/36537004/110143057-96f31e00-7db5-11eb-994a-39ae9e63392e.png) + Display error messages when the user attempts to create or edit users' or channels' names with any of the following words (**case-insensitive**): + - admin; + - administrator; + - system; + - user. + ![create-channel](https://user-images.githubusercontent.com/36537004/110132223-b421ef80-7da9-11eb-82bc-f0d4e1df967f.png) + ![register-username](https://user-images.githubusercontent.com/36537004/110132234-b71ce000-7da9-11eb-904e-580233625951.png) + ![change-channel](https://user-images.githubusercontent.com/36537004/110143057-96f31e00-7db5-11eb-994a-39ae9e63392e.png) ![change-username](https://user-images.githubusercontent.com/36537004/110143065-98244b00-7db5-11eb-9d13-afc5dc9866de.png) - add permission check when adding a channel to a team ([#21689](https://github.com/RocketChat/Rocket.Chat/pull/21689) by [@g-thome](https://github.com/g-thome)) @@ -2365,8 +3566,7 @@ - Resize custom emojis on upload instead of saving at max res ([#21593](https://github.com/RocketChat/Rocket.Chat/pull/21593)) - - Create new MediaService (ideally, should be in charge of all media-related operations) - + - Create new MediaService (ideally, should be in charge of all media-related operations) - Resize emojis to 128x128 ### 🐛 Bug fixes @@ -2384,27 +3584,27 @@ - Allow deletion of own account for passwordless accounts (e.g. OAUTH) ([#21119](https://github.com/RocketChat/Rocket.Chat/pull/21119) by [@wolbernd](https://github.com/wolbernd)) -- Allows more than 25 discussions/files to be loaded in the contextualbar ([#21511](https://github.com/RocketChat/Rocket.Chat/pull/21511) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - - In some places, you could not load more than 25 threads/discussions/files on the screen when searching the lists in the contextualbar. - Threads & list are numbered for a better view of the solution - +- Allows more than 25 discussions/files to be loaded in the contextualbar ([#21511](https://github.com/RocketChat/Rocket.Chat/pull/21511) by [@Jeanstaquet](https://github.com/Jeanstaquet)) + In some places, you could not load more than 25 threads/discussions/files on the screen when searching the lists in the contextualbar. + Threads & list are numbered for a better view of the solution + + https://user-images.githubusercontent.com/45966964/114222225-93335800-996e-11eb-833f-568e83129aae.mp4 - Allows more than 25 threads to be loaded, fixes #21507 ([#21508](https://github.com/RocketChat/Rocket.Chat/pull/21508) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - Allows to display more than 25 users maximum in the users list ([#21518](https://github.com/RocketChat/Rocket.Chat/pull/21518) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - Now when you scroll to the bottom of the users list, it shows more users. Before the fix, the limit for the query for loadMore was calculated so that no additional users could be loaded. - - Before - - https://user-images.githubusercontent.com/45966964/114249739-baece500-999b-11eb-9bb0-3a5bcee18ad8.mp4 - - After - - + Now when you scroll to the bottom of the users list, it shows more users. Before the fix, the limit for the query for loadMore was calculated so that no additional users could be loaded. + + Before + + https://user-images.githubusercontent.com/45966964/114249739-baece500-999b-11eb-9bb0-3a5bcee18ad8.mp4 + + After + + https://user-images.githubusercontent.com/45966964/114249895-364e9680-999c-11eb-985c-47aedc763488.mp4 - App installation from marketplace not correctly displaying the permissions ([#21470](https://github.com/RocketChat/Rocket.Chat/pull/21470)) @@ -2431,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. @@ -2471,19 +3671,19 @@ - Margins on contextual bar information ([#21457](https://github.com/RocketChat/Rocket.Chat/pull/21457)) - ### Room - **Before** - ![image](https://user-images.githubusercontent.com/27704687/115080812-ba8fa500-9ed9-11eb-9078-3625603bf92b.png) - - **After** - ![image](https://user-images.githubusercontent.com/27704687/115080966-e9a61680-9ed9-11eb-929f-6516c1563e99.png) - - ### Livechat + ### Room + **Before** + ![image](https://user-images.githubusercontent.com/27704687/115080812-ba8fa500-9ed9-11eb-9078-3625603bf92b.png) + + **After** + ![image](https://user-images.githubusercontent.com/27704687/115080966-e9a61680-9ed9-11eb-929f-6516c1563e99.png) + + ### Livechat ![image](https://user-images.githubusercontent.com/27704687/113640101-1859fc80-9651-11eb-88f8-09a899953988.png) - Message Block ordering ([#21464](https://github.com/RocketChat/Rocket.Chat/pull/21464)) - Reactions should come before reply button. + Reactions should come before reply button. ![image](https://user-images.githubusercontent.com/40830821/113748926-6f0e1780-96df-11eb-93a5-ddcfa891413e.png) - Message link null corrupts message rendering ([#21579](https://github.com/RocketChat/Rocket.Chat/pull/21579) by [@g-thome](https://github.com/g-thome)) @@ -2536,19 +3736,15 @@ - Typos/missing elements in the French translation ([#21525](https://github.com/RocketChat/Rocket.Chat/pull/21525) by [@Jeanstaquet](https://github.com/Jeanstaquet)) - - I have corrected some typos in the translation - - - I added a translation for missing words - - - I took the opportunity to correct a mistranslated word - - - Test_Desktop_Notifications was missing in the EN and FR file + - I have corrected some typos in the translation + - I added a translation for missing words + - I took the opportunity to correct a mistranslated word + - Test_Desktop_Notifications was missing in the EN and FR file ![image](https://user-images.githubusercontent.com/45966964/114290186-e7792d80-9a7d-11eb-8164-3b5e72e93703.png) - Updating a message causing URLs to be parsed even within markdown code ([#21489](https://github.com/RocketChat/Rocket.Chat/pull/21489)) - - Fix `updateMessage` to avoid parsing URLs inside markdown - + - Fix `updateMessage` to avoid parsing URLs inside markdown - Honor `parseUrls` property when updating messages - Use async await in TeamChannels delete channel action ([#21534](https://github.com/RocketChat/Rocket.Chat/pull/21534)) @@ -2561,8 +3757,8 @@ - Wrong user in user info ([#21451](https://github.com/RocketChat/Rocket.Chat/pull/21451)) - Fixed some race conditions in admin. - + Fixed some race conditions in admin. + Self DMs used to be created with the userId duplicated. Sometimes rooms can have 2 equal uids, but it's a self DM. Fixed a getter so this isn't a problem anymore.
@@ -2571,30 +3767,22 @@ - Doc: Corrected links to documentation of rocket.chat README.md ([#20478](https://github.com/RocketChat/Rocket.Chat/pull/20478) by [@joshi008](https://github.com/joshi008)) - The link for documentation in the readme was previously https://rocket.chat/docs/ while that was not working and according to the website it was https://docs.rocket.chat/ - The link for deployment methods in readme was corrected from https://rocket.chat/docs/installation/paas-deployments/ to https://docs.rocket.chat/installation/paas-deployments + The link for documentation in the readme was previously https://rocket.chat/docs/ while that was not working and according to the website it was https://docs.rocket.chat/ + The link for deployment methods in readme was corrected from https://rocket.chat/docs/installation/paas-deployments/ to https://docs.rocket.chat/installation/paas-deployments Some more links to the documentations were giving 404 error which hence updated. - [Improve] Remove useless tabbar options from Omnichannel rooms ([#21561](https://github.com/RocketChat/Rocket.Chat/pull/21561) by [@rafaelblink](https://github.com/rafaelblink)) - A React-based replacement for BlazeLayout ([#21527](https://github.com/RocketChat/Rocket.Chat/pull/21527)) - - The Meteor package **`kadira:blaze-layout` was removed**; - - - A **global subscription** for the current application layout (**`appLayout`**) replaces `BlazeLayout` entirely; - - - The **`#react-root` element** is rendered on server-side instead of dynamically injected into the DOM tree; - - - The **"page loading" throbber** is now rendered on the React tree; - - - The **`renderRouteComponent` helper was removed**; - - - Some code run without any criteria on **`main` template** module was moved into **client startup modules**; - - - React portals used to embed Blaze templates have their own subscription (**`blazePortals`**); - - - Some **route components were refactored** to remove a URL path trap originally disabled by `renderRouteComponent`; - + - The Meteor package **`kadira:blaze-layout` was removed**; + - A **global subscription** for the current application layout (**`appLayout`**) replaces `BlazeLayout` entirely; + - The **`#react-root` element** is rendered on server-side instead of dynamically injected into the DOM tree; + - The **"page loading" throbber** is now rendered on the React tree; + - The **`renderRouteComponent` helper was removed**; + - Some code run without any criteria on **`main` template** module was moved into **client startup modules**; + - React portals used to embed Blaze templates have their own subscription (**`blazePortals`**); + - Some **route components were refactored** to remove a URL path trap originally disabled by `renderRouteComponent`; - A new component to embed the DOM nodes generated by **`RoomManager`** was created. - Add ')' after Date and Time in DB migration ([#21519](https://github.com/RocketChat/Rocket.Chat/pull/21519) by [@im-adithya](https://github.com/im-adithya)) @@ -2617,8 +3805,8 @@ - Chore: Meteor update to 2.1.1 ([#21494](https://github.com/RocketChat/Rocket.Chat/pull/21494)) - Basically Node update to version 12.22.1 - + Basically Node update to version 12.22.1 + Meteor change log https://github.com/meteor/meteor/blob/devel/History.md#v211-2021-04-06 - Chore: Remove control character from room model operation ([#21493](https://github.com/RocketChat/Rocket.Chat/pull/21493)) @@ -2627,8 +3815,7 @@ - Fix: Missing module `eventemitter3` for micro services ([#21611](https://github.com/RocketChat/Rocket.Chat/pull/21611)) - - Fix error when running micro services after version 3.12 - + - Fix error when running micro services after version 3.12 - Fix build of docker image version latest for micro services - Language update from LingoHub 🤖 on 2021-04-05Z ([#21446](https://github.com/RocketChat/Rocket.Chat/pull/21446)) @@ -2641,12 +3828,9 @@ - QoL improvements to add channel to team flow ([#21778](https://github.com/RocketChat/Rocket.Chat/pull/21778)) - - Fixed canAccessRoom validation - - - Added e2e tests - - - Removed channels that user cannot add to the team from autocomplete suggestions - + - Fixed canAccessRoom validation + - Added e2e tests + - Removed channels that user cannot add to the team from autocomplete suggestions - Improved error messages - Regression: Bold, italic and strike render (Original markdown) ([#21747](https://github.com/RocketChat/Rocket.Chat/pull/21747)) @@ -2669,10 +3853,10 @@ - Regression: Legacy Banner Position ([#21598](https://github.com/RocketChat/Rocket.Chat/pull/21598)) - ### Before: - ![image](https://user-images.githubusercontent.com/27704687/114961773-dc3c4e00-9e3f-11eb-9a32-e882db3fbfbc.png) - - ### After + ### Before: + ![image](https://user-images.githubusercontent.com/27704687/114961773-dc3c4e00-9e3f-11eb-9a32-e882db3fbfbc.png) + + ### After ![image](https://user-images.githubusercontent.com/27704687/114961673-a6976500-9e3f-11eb-9238-a12870d7db8f.png) - regression: Markdown broken on safari ([#21780](https://github.com/RocketChat/Rocket.Chat/pull/21780)) @@ -2711,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 🤓 @@ -2732,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 👩‍💻👨‍💻` @@ -2882,62 +4066,56 @@ - **APPS:** New event interfaces for pre/post user leaving a room ([#20917](https://github.com/RocketChat/Rocket.Chat/pull/20917) by [@lucassartor](https://github.com/lucassartor)) - Added events and errors that trigger when a user leaves a room. + Added events and errors that trigger when a user leaves a room. That way it can communicate with the Apps-Engine by the `IPreRoomUserLeave` and `IPostRoomUserLeave` event interfaces. - **Enterprise:** Omnichannel On-Hold Queue ([#20945](https://github.com/RocketChat/Rocket.Chat/pull/20945)) - ### About this feature - This feature has been introduced to deal with Inactive chats. A chat is considered Inactive if an Omnichannel End User (aka Visitor) has not replied back to an agent in some time. These types of inactive chats become very important when an organisation has a limit set for `Max Simultaneous Chats per agent` which is defined by the following setting :point_down: , as more number of Inactive chats would directly affect an agent's productivity. - ![image](https://user-images.githubusercontent.com/34130764/111533003-4d7ad980-878c-11eb-8c1c-2796678a07db.png) - - Before this feature, we only had one option to deal with such Inactive/Abandoned chats - which was to auto close abandoned chats via this setting :point_down: - ![image](https://user-images.githubusercontent.com/34130764/111534353-e65e2480-878d-11eb-82a5-71368064ef45.png) - - however closing a chat isn't a best option for some cases. Let me take an example to explain a scenario - - > An agent is assisting a customer for installing a very huge software which is likely to take more than 20-30 minutes to download. In such scenarios closing a chat isn't the best approach since even after the lengthy download the customer might still need some assist from the agent. - > So basically this chat is going to block the agent's queue until the customer is able to finish his time-consuming download task in which he/she doesn't require any agent's assistance. Due to the `Max Simultaneous Chats per agent` limit, the agent is also not able to use this extra time to help other customer thus affecting his overall productivity. - - **So how does the On-Hold feature solve this problem?** - With the On-Hold feature, an agent is now able to place a chat on-hold. On-Hold chats **don’t count towards the maximum number of concurrent chats** an agent can have. So in our above example, the agent can simply now place the customer on-hold for 20-30 minutes until the customer downloads the software and within this time, the agent can serve other customers - hence increasing the productivity of an agent. - - ---------------------------------------- - ### Working of the new On-Hold feature - - #### How can you place a chat on Hold ? - - A chat can be placed on-hold via 2 means - - 1. Automatically place Abandoned chats On-hold - ![image](https://user-images.githubusercontent.com/34130764/111537074-06431780-8791-11eb-8d23-99f5d9f8ec45.png) - Via this :top: option you can define a timer which will get started when a customer sends a message. If we don't receive any message from the customer within this timer, the timer will get expired and the chat will be considered as Abandoned. - ![image](https://user-images.githubusercontent.com/34130764/111537346-53bf8480-8791-11eb-8dc7-260633b4e98f.png) - The via this :top: setting you can choose to automatically place this abandoned chat On Hold - - 2. Manually place a chat On Hold - As an admin, you can allow an agent to manually place a chat on-hold. To do so, you'll need to turn on this :point_down: setting - ![image](https://user-images.githubusercontent.com/34130764/111537545-97b28980-8791-11eb-86fd-db45b87e9cc1.png) - Now an agent will be able to see a new `On Hold` button within their `Visitor Info Panel` like this :point_down: , provided the agent has sent the last message - ![image](https://user-images.githubusercontent.com/34130764/111537853-f24be580-8791-11eb-9561-d77ba430c625.png) - - #### How can you resume a On Hold chat ? - An On Hold chat can be resumed via 2 means - - - 1. If the Customer sends a message - If the Customer / Omnichannel End User sends a message to the On Hold chat, the On Hold chat will get automatically resumed. - - 2. Manually by agent - An Agent can manually resume the On Hold chat via clicking the `Resume` button in the bottom of a chat room. - ![image](https://user-images.githubusercontent.com/34130764/111538666-f88e9180-8792-11eb-8d14-01453b8e3db0.png) - - #### What would happen if the agent already reached maximum chats, and a On-Hold chat gets resumed ? - Based on how the chat was resumed, there are multiple cases are each case is dealt differently - - - - If an agent manually tries to resume the On Hold chat, he/she will get an error saying `Maximum Simultaneous chat limit reached` - + ### About this feature + This feature has been introduced to deal with Inactive chats. A chat is considered Inactive if an Omnichannel End User (aka Visitor) has not replied back to an agent in some time. These types of inactive chats become very important when an organisation has a limit set for `Max Simultaneous Chats per agent` which is defined by the following setting :point_down: , as more number of Inactive chats would directly affect an agent's productivity. + ![image](https://user-images.githubusercontent.com/34130764/111533003-4d7ad980-878c-11eb-8c1c-2796678a07db.png) + + Before this feature, we only had one option to deal with such Inactive/Abandoned chats - which was to auto close abandoned chats via this setting :point_down: + ![image](https://user-images.githubusercontent.com/34130764/111534353-e65e2480-878d-11eb-82a5-71368064ef45.png) + + however closing a chat isn't a best option for some cases. Let me take an example to explain a scenario + + > An agent is assisting a customer for installing a very huge software which is likely to take more than 20-30 minutes to download. In such scenarios closing a chat isn't the best approach since even after the lengthy download the customer might still need some assist from the agent. + > So basically this chat is going to block the agent's queue until the customer is able to finish his time-consuming download task in which he/she doesn't require any agent's assistance. Due to the `Max Simultaneous Chats per agent` limit, the agent is also not able to use this extra time to help other customer thus affecting his overall productivity. + + **So how does the On-Hold feature solve this problem?** + With the On-Hold feature, an agent is now able to place a chat on-hold. On-Hold chats **don’t count towards the maximum number of concurrent chats** an agent can have. So in our above example, the agent can simply now place the customer on-hold for 20-30 minutes until the customer downloads the software and within this time, the agent can serve other customers - hence increasing the productivity of an agent. + + ---------------------------------------- + ### Working of the new On-Hold feature + + #### How can you place a chat on Hold ? + + A chat can be placed on-hold via 2 means + 1. Automatically place Abandoned chats On-hold + ![image](https://user-images.githubusercontent.com/34130764/111537074-06431780-8791-11eb-8d23-99f5d9f8ec45.png) + Via this :top: option you can define a timer which will get started when a customer sends a message. If we don't receive any message from the customer within this timer, the timer will get expired and the chat will be considered as Abandoned. + ![image](https://user-images.githubusercontent.com/34130764/111537346-53bf8480-8791-11eb-8dc7-260633b4e98f.png) + The via this :top: setting you can choose to automatically place this abandoned chat On Hold + 2. Manually place a chat On Hold + As an admin, you can allow an agent to manually place a chat on-hold. To do so, you'll need to turn on this :point_down: setting + ![image](https://user-images.githubusercontent.com/34130764/111537545-97b28980-8791-11eb-86fd-db45b87e9cc1.png) + Now an agent will be able to see a new `On Hold` button within their `Visitor Info Panel` like this :point_down: , provided the agent has sent the last message + ![image](https://user-images.githubusercontent.com/34130764/111537853-f24be580-8791-11eb-9561-d77ba430c625.png) + + #### How can you resume a On Hold chat ? + An On Hold chat can be resumed via 2 means + + 1. If the Customer sends a message + If the Customer / Omnichannel End User sends a message to the On Hold chat, the On Hold chat will get automatically resumed. + 2. Manually by agent + An Agent can manually resume the On Hold chat via clicking the `Resume` button in the bottom of a chat room. + ![image](https://user-images.githubusercontent.com/34130764/111538666-f88e9180-8792-11eb-8d14-01453b8e3db0.png) + + #### What would happen if the agent already reached maximum chats, and a On-Hold chat gets resumed ? + Based on how the chat was resumed, there are multiple cases are each case is dealt differently + + - If an agent manually tries to resume the On Hold chat, he/she will get an error saying `Maximum Simultaneous chat limit reached` - If a customer replies back on an On Hold chat and the last serving agent has reached maximum capacity, then this customer will be placed on the queue again from where based on the Routing Algorithm selected, the chat will get transferred to any available agent - Ability to hide 'Room topic changed' system messages ([#21062](https://github.com/RocketChat/Rocket.Chat/pull/21062) by [@Tirieru](https://github.com/Tirieru)) @@ -2948,39 +4126,33 @@ - Teams ([#20966](https://github.com/RocketChat/Rocket.Chat/pull/20966) by [@g-thome](https://github.com/g-thome)) - ## Teams - - - - You can easily group your users as Teams on Rocket.Chat. The feature takes the hassle out of managing multiple users one by one and allows you to handle them at the same time efficiently. - - - - - Teams can be public or private and each team can have its own channels, which also can be public or private. - - - It's possible to add existing channels to a Team or create new ones inside a Team. - - - It's possible to invite people outside a Team to join Team's channels. - - - It's possible to convert channels to Teams - - - It's possible to add all team members to a channel at once - - - Team members have roles - - - ![image](https://user-images.githubusercontent.com/70927132/113421955-4f56b680-93a2-11eb-80dc-9b70a3f09b3e.png) - - - - **Quickly onboard new users with Autojoin channels** - - Teams can have Auto-join channels – channels to which the team members are automatically added, so you don’t need to go through the manual process of adding users repetitively - - ![image](https://user-images.githubusercontent.com/70927132/113419284-81194e80-939d-11eb-9fff-aeb05cbc8089.png) - - **Instantly mention multiple members at once** (available in EE) - + ## Teams + + + + You can easily group your users as Teams on Rocket.Chat. The feature takes the hassle out of managing multiple users one by one and allows you to handle them at the same time efficiently. + + + - Teams can be public or private and each team can have its own channels, which also can be public or private. + - It's possible to add existing channels to a Team or create new ones inside a Team. + - It's possible to invite people outside a Team to join Team's channels. + - It's possible to convert channels to Teams + - It's possible to add all team members to a channel at once + - Team members have roles + + + ![image](https://user-images.githubusercontent.com/70927132/113421955-4f56b680-93a2-11eb-80dc-9b70a3f09b3e.png) + + + + **Quickly onboard new users with Autojoin channels** + + Teams can have Auto-join channels – channels to which the team members are automatically added, so you don’t need to go through the manual process of adding users repetitively + + ![image](https://user-images.githubusercontent.com/70927132/113419284-81194e80-939d-11eb-9fff-aeb05cbc8089.png) + + **Instantly mention multiple members at once** (available in EE) + With Teams, you don’t need to remember everyone’s name to communicate with a team quickly. Just mention a Team — @engineers, for instance — and all members will be instantly notified. ### 🚀 Improvements @@ -2990,22 +4162,22 @@ - Added modal-box for preview after recording audio. ([#20370](https://github.com/RocketChat/Rocket.Chat/pull/20370) by [@Darshilp326](https://github.com/Darshilp326)) - A modal box will be displayed so that users can change the filename and add description. - - **Before** - - https://user-images.githubusercontent.com/55157259/105687301-4e2a8880-5f1e-11eb-873d-dc8a880a2fc8.mp4 - - **After** - + A modal box will be displayed so that users can change the filename and add description. + + **Before** + + https://user-images.githubusercontent.com/55157259/105687301-4e2a8880-5f1e-11eb-873d-dc8a880a2fc8.mp4 + + **After** + https://user-images.githubusercontent.com/55157259/105687342-597db400-5f1e-11eb-8b61-8f9d9ebad0c4.mp4 - Adds toast after follow/unfollow messages and following icon for followed messages without threads. ([#20025](https://github.com/RocketChat/Rocket.Chat/pull/20025) by [@RonLek](https://github.com/RonLek)) - There was no alert on following/unfollowing a message previously. Also, it was impossible to make out a followed message with no threads from an unfollowed one. - - This PR would show an alert on following/unfollowing a message and also display a small bell icon (similar to the ones for starred and pinned messages) when a message with no thread is followed. - + There was no alert on following/unfollowing a message previously. Also, it was impossible to make out a followed message with no threads from an unfollowed one. + + This PR would show an alert on following/unfollowing a message and also display a small bell icon (similar to the ones for starred and pinned messages) when a message with no thread is followed. + https://user-images.githubusercontent.com/28918901/103813540-43e73e00-5086-11eb-8592-2877eb650f3e.mp4 - Back to threads list button on threads contextual bar ([#20882](https://github.com/RocketChat/Rocket.Chat/pull/20882)) @@ -3018,12 +4190,12 @@ - Improve Apps permission modal ([#21193](https://github.com/RocketChat/Rocket.Chat/pull/21193) by [@lucassartor](https://github.com/lucassartor)) - Improve the UI of the Apps permission modal when installing an App that requires permissions. - - **New UI:** - ![after](https://user-images.githubusercontent.com/49413772/111685622-e817fe80-8806-11eb-998d-b56623560e74.PNG) - - **Old UI:** + Improve the UI of the Apps permission modal when installing an App that requires permissions. + + **New UI:** + ![after](https://user-images.githubusercontent.com/49413772/111685622-e817fe80-8806-11eb-998d-b56623560e74.PNG) + + **Old UI:** ![before](https://user-images.githubusercontent.com/49413772/111685897-375e2f00-8807-11eb-814e-cb8060dc1830.PNG) - Make debug logs of Apps configurable via Log_Level setting in the Admin panel ([#21000](https://github.com/RocketChat/Rocket.Chat/pull/21000) by [@cuonghuunguyen](https://github.com/cuonghuunguyen)) @@ -3034,15 +4206,15 @@ - Sort Users List In Case Insensitive Manner ([#20790](https://github.com/RocketChat/Rocket.Chat/pull/20790) by [@aditya-mitra](https://github.com/aditya-mitra)) - The users listed in the admin panel were sorted in a case-sensitive manner , where the capitals came first and then the small letters (like - *A B C a b c*). This Change fixes this by sorting the names in a caseinsensitive manner (now - *A a B b C c*). - - ### Before - - ![before](https://user-images.githubusercontent.com/55396651/108189880-3fa74980-7137-11eb-99da-6498707b4bf8.png) - - - ### With This Change - + The users listed in the admin panel were sorted in a case-sensitive manner , where the capitals came first and then the small letters (like - *A B C a b c*). This Change fixes this by sorting the names in a caseinsensitive manner (now - *A a B b C c*). + + ### Before + + ![before](https://user-images.githubusercontent.com/55396651/108189880-3fa74980-7137-11eb-99da-6498707b4bf8.png) + + + ### With This Change + ![after](https://user-images.githubusercontent.com/55396651/108190177-9dd42c80-7137-11eb-8b4e-b7cef4ba512f.png) ### 🐛 Bug fixes @@ -3056,17 +4228,17 @@ - **APPS:** Warn message while installing app in air-gapped environment ([#20992](https://github.com/RocketChat/Rocket.Chat/pull/20992) by [@lucassartor](https://github.com/lucassartor)) - Change **error** message to a **warn** message when uploading a `.zip` file app into a air-gapped environment. - - The **error** message was giving the impression for the user that the app wasn't properly being installed , which it wasn't the case: - ![error](https://user-images.githubusercontent.com/49413772/109855273-d3e4d680-7c36-11eb-824b-ad455d24710c.PNG) - - A more detailed **warn** message can fix that impression for the user: + Change **error** message to a **warn** message when uploading a `.zip` file app into a air-gapped environment. + + The **error** message was giving the impression for the user that the app wasn't properly being installed , which it wasn't the case: + ![error](https://user-images.githubusercontent.com/49413772/109855273-d3e4d680-7c36-11eb-824b-ad455d24710c.PNG) + + A more detailed **warn** message can fix that impression for the user: ![warn](https://user-images.githubusercontent.com/49413772/109855383-f2e36880-7c36-11eb-8d61-c442980bd8fd.PNG) - 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. @@ -3076,10 +4248,10 @@ - Correct direction for admin mapview text ([#20897](https://github.com/RocketChat/Rocket.Chat/pull/20897) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) - ![Screenshot from 2021-02-25 02-49-21](https://user-images.githubusercontent.com/38764067/109068512-f8602080-7715-11eb-8e22-d610f9d046d8.png) - ![Screenshot from 2021-02-25 02-49-46](https://user-images.githubusercontent.com/38764067/109068516-fa29e400-7715-11eb-9119-1c79abce278f.png) - ![Screenshot from 2021-02-25 02-49-57](https://user-images.githubusercontent.com/38764067/109068519-fbf3a780-7715-11eb-8b3d-0dc32f898725.png) - + ![Screenshot from 2021-02-25 02-49-21](https://user-images.githubusercontent.com/38764067/109068512-f8602080-7715-11eb-8e22-d610f9d046d8.png) + ![Screenshot from 2021-02-25 02-49-46](https://user-images.githubusercontent.com/38764067/109068516-fa29e400-7715-11eb-9119-1c79abce278f.png) + ![Screenshot from 2021-02-25 02-49-57](https://user-images.githubusercontent.com/38764067/109068519-fbf3a780-7715-11eb-8b3d-0dc32f898725.png) + The text says the share button will be on the left of the messagebox once enabled. However, it actually is on the right. - Correct ignored message CSS ([#20928](https://github.com/RocketChat/Rocket.Chat/pull/20928) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) @@ -3096,13 +4268,13 @@ - Custom emojis to override default ([#20359](https://github.com/RocketChat/Rocket.Chat/pull/20359) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) - Due to the sequence of the imports and how the emojiRenderer prioritizes lists, the custom emojis could not override the emojione emojis. Making two small changes fixed the issue. - - With the custom emoji for `:facepalm:` added, you can check out the result below: - ### Before - ![Screenshot from 2021-01-25 02-20-04](https://user-images.githubusercontent.com/38764067/105643088-dfb0e080-5eb3-11eb-8a00-582c53fbe9a4.png) - - ### After + Due to the sequence of the imports and how the emojiRenderer prioritizes lists, the custom emojis could not override the emojione emojis. Making two small changes fixed the issue. + + With the custom emoji for `:facepalm:` added, you can check out the result below: + ### Before + ![Screenshot from 2021-01-25 02-20-04](https://user-images.githubusercontent.com/38764067/105643088-dfb0e080-5eb3-11eb-8a00-582c53fbe9a4.png) + + ### After ![Screenshot from 2021-01-25 02-18-58](https://user-images.githubusercontent.com/38764067/105643076-cdcf3d80-5eb3-11eb-84b8-5dbc4f1135df.png) - Empty URL in user avatar doesn't show error and enables save ([#20440](https://github.com/RocketChat/Rocket.Chat/pull/20440) by [@im-adithya](https://github.com/im-adithya)) @@ -3115,12 +4287,12 @@ - Fix the search list showing the last channel ([#21160](https://github.com/RocketChat/Rocket.Chat/pull/21160) by [@shrinish123](https://github.com/shrinish123)) - The search list now also properly shows the last channel - Before : - - ![searchlist](https://user-images.githubusercontent.com/56491104/111471487-f3a7ee80-874e-11eb-9c6e-19bbf0731d60.png) - - After : + The search list now also properly shows the last channel + Before : + + ![searchlist](https://user-images.githubusercontent.com/56491104/111471487-f3a7ee80-874e-11eb-9c6e-19bbf0731d60.png) + + After : ![search_final](https://user-images.githubusercontent.com/56491104/111471521-fe628380-874e-11eb-8fa3-d1edb57587e1.png) - Follow thread action on threads list ([#20881](https://github.com/RocketChat/Rocket.Chat/pull/20881)) @@ -3145,13 +4317,13 @@ - Multi Select isn't working in Export Messages ([#21236](https://github.com/RocketChat/Rocket.Chat/pull/21236) by [@PriyaBihani](https://github.com/PriyaBihani)) - While exporting messages, we were not able to select multiple Users like this: - - https://user-images.githubusercontent.com/69837339/111953057-169a2000-8b0c-11eb-94a4-0e1657683f96.mp4 - - Now we can select multiple users: - - + While exporting messages, we were not able to select multiple Users like this: + + https://user-images.githubusercontent.com/69837339/111953057-169a2000-8b0c-11eb-94a4-0e1657683f96.mp4 + + Now we can select multiple users: + + https://user-images.githubusercontent.com/69837339/111953097-274a9600-8b0c-11eb-9177-bec388b042bd.mp4 - New Channel popover not closing ([#21080](https://github.com/RocketChat/Rocket.Chat/pull/21080)) @@ -3160,43 +4332,43 @@ - OEmbedURLWidget - Show Full Embedded Text Description ([#20569](https://github.com/RocketChat/Rocket.Chat/pull/20569) by [@aditya-mitra](https://github.com/aditya-mitra)) - Embeds were cutoff when either _urls had a long description_. - This was handled by removing `overflow:hidden;text-overflow:ellipsis;` from the inline styles in [`oembedUrlWidget.html`](https://github.com/RocketChat/Rocket.Chat/blob/develop/app/oembed/client/oembedUrlWidget.html#L28). - - ### Earlier - - ![earlier](https://user-images.githubusercontent.com/55396651/107110825-00dcde00-6871-11eb-866e-13cabc5b0d05.png) - - ### Now - + Embeds were cutoff when either _urls had a long description_. + This was handled by removing `overflow:hidden;text-overflow:ellipsis;` from the inline styles in [`oembedUrlWidget.html`](https://github.com/RocketChat/Rocket.Chat/blob/develop/app/oembed/client/oembedUrlWidget.html#L28). + + ### Earlier + + ![earlier](https://user-images.githubusercontent.com/55396651/107110825-00dcde00-6871-11eb-866e-13cabc5b0d05.png) + + ### Now + ![now](https://user-images.githubusercontent.com/55396651/107110794-ca06c800-6870-11eb-9b3b-168679936612.png) - Reactions list showing users in reactions option of message action. ([#20753](https://github.com/RocketChat/Rocket.Chat/pull/20753) by [@Darshilp326](https://github.com/Darshilp326)) - Reactions list shows emojis with respected users who have reacted with that emoji. - + Reactions list shows emojis with respected users who have reacted with that emoji. + https://user-images.githubusercontent.com/55157259/107857609-5870e000-6e55-11eb-8137-494a9f71b171.mp4 - Removing truncation from profile ([#20352](https://github.com/RocketChat/Rocket.Chat/pull/20352) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) - Truncating text in profile view was making some information completely inaccessible. Removed it from the user status and the custom fields where if the information is longer, the user would actually want to see all of it. - - ### Before - ![Screenshot from 2021-01-24 20-54-44](https://user-images.githubusercontent.com/38764067/105634935-7e264d00-5e86-11eb-8a6c-9f2a363e0f6c.png) - - ### After + Truncating text in profile view was making some information completely inaccessible. Removed it from the user status and the custom fields where if the information is longer, the user would actually want to see all of it. + + ### Before + ![Screenshot from 2021-01-24 20-54-44](https://user-images.githubusercontent.com/38764067/105634935-7e264d00-5e86-11eb-8a6c-9f2a363e0f6c.png) + + ### After ![Screenshot from 2021-01-24 20-54-06](https://user-images.githubusercontent.com/38764067/105634940-82eb0100-5e86-11eb-8b90-e97a43c5e938.png) - 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. - Set establishing to false if OTR timeouts ([#21183](https://github.com/RocketChat/Rocket.Chat/pull/21183) by [@Darshilp326](https://github.com/Darshilp326)) - Set establishing false if OTR timeouts. - + Set establishing false if OTR timeouts. + https://user-images.githubusercontent.com/55157259/111617086-b30cab80-8808-11eb-8740-3b4ffacfc322.mp4 - Sidebar scroll missing full height ([#21071](https://github.com/RocketChat/Rocket.Chat/pull/21071)) @@ -3235,33 +4407,20 @@ - Chore: Add tests for Meteor methods ([#20901](https://github.com/RocketChat/Rocket.Chat/pull/20901)) - Add end-to-end tests for the following meteor methods - - - - [x] public-settings:get - - - [x] rooms:get - - - [x] subscriptions:get - - - [x] permissions:get - - - [x] loadMissedMessages - - - [x] loadHistory - - - [x] listCustomUserStatus - - - [x] getUserRoles - - - [x] getRoomRoles (called by the API, already covered) - - - [x] getMessages - - - [x] getUsersOfRoom - - - [x] loadNextMessages - + Add end-to-end tests for the following meteor methods + + - [x] public-settings:get + - [x] rooms:get + - [x] subscriptions:get + - [x] permissions:get + - [x] loadMissedMessages + - [x] loadHistory + - [x] listCustomUserStatus + - [x] getUserRoles + - [x] getRoomRoles (called by the API, already covered) + - [x] getMessages + - [x] getUsersOfRoom + - [x] loadNextMessages - [x] getThreadMessages - Chore: Meteor update 2.1 ([#21061](https://github.com/RocketChat/Rocket.Chat/pull/21061)) @@ -3274,10 +4433,8 @@ - Improve: Increase testing coverage ([#21015](https://github.com/RocketChat/Rocket.Chat/pull/21015)) - Add test for - - - settings/raw - + Add test for + - settings/raw - minimongo/comparisons - Improve: NPS survey fetch ([#21263](https://github.com/RocketChat/Rocket.Chat/pull/21263)) @@ -3296,19 +4453,17 @@ - Regression: Add scope to permission checks in Team's endpoints ([#21369](https://github.com/RocketChat/Rocket.Chat/pull/21369)) - - Include scope (team's main room ID) in the permission checks; + - Include scope (team's main room ID) in the permission checks; - Remove the `teamName` parameter from the `members`, `addMembers`, `updateMember` and `removeMembers` methods (since `teamId` will always be defined). - Regression: Add support to filter on `teams.listRooms` endpoint ([#21327](https://github.com/RocketChat/Rocket.Chat/pull/21327)) - - Add support for queries (within the `query` parameter); - + - Add support for queries (within the `query` parameter); - Add support to pagination (`offset` and `count`) when an user doesn't have the permission to get all rooms. - Regression: Add teams support to directory ([#21351](https://github.com/RocketChat/Rocket.Chat/pull/21351)) - - Change `directory.js` to reduce function complexity - + - Change `directory.js` to reduce function complexity - Add `teams` type of item. Directory will return all public teams & private teams the user is part of. - Regression: add view room action on Teams Channels ([#21295](https://github.com/RocketChat/Rocket.Chat/pull/21295)) @@ -3361,19 +4516,18 @@ - Regression: Quick action button missing for Omnichannel On-Hold queue ([#21285](https://github.com/RocketChat/Rocket.Chat/pull/21285)) - - Move the Manual On Hold button to the new Omnichannel Header - ![image](https://user-images.githubusercontent.com/34130764/112291749-6ae10380-8cb6-11eb-94cd-e05efc14b1bf.png) - ![image](https://user-images.githubusercontent.com/34130764/112304146-27d95d00-8cc3-11eb-85db-dde04a110dd1.png) - - + - Move the Manual On Hold button to the new Omnichannel Header + ![image](https://user-images.githubusercontent.com/34130764/112291749-6ae10380-8cb6-11eb-94cd-e05efc14b1bf.png) + ![image](https://user-images.githubusercontent.com/34130764/112304146-27d95d00-8cc3-11eb-85db-dde04a110dd1.png) + - Minor fixes - regression: Remove Breadcrumbs and update Tag component ([#21399](https://github.com/RocketChat/Rocket.Chat/pull/21399)) - Regression: Remove channel action on add channel's modal don't work ([#21356](https://github.com/RocketChat/Rocket.Chat/pull/21356)) - ![removechannel-on-add-existing-modal](https://user-images.githubusercontent.com/27704687/112911017-eda8fa80-90ca-11eb-9c24-47a70be0c314.gif) - + ![removechannel-on-add-existing-modal](https://user-images.githubusercontent.com/27704687/112911017-eda8fa80-90ca-11eb-9c24-47a70be0c314.gif) + ![image](https://user-images.githubusercontent.com/27704687/112911052-02858e00-90cb-11eb-85a2-0ef1f5f9ffd9.png) - Regression: Remove primary color from button in TeamChannels component ([#21293](https://github.com/RocketChat/Rocket.Chat/pull/21293)) @@ -3402,10 +4556,10 @@ - Regression: Unify Contact information displayed on the Room header and Room Info ([#21312](https://github.com/RocketChat/Rocket.Chat/pull/21312) by [@rafaelblink](https://github.com/rafaelblink)) - ![image](https://user-images.githubusercontent.com/34130764/112586659-35592900-8e22-11eb-94be-32bdff7ca883.png) - - ![image](https://user-images.githubusercontent.com/2493803/112913130-788bf400-90cf-11eb-84c6-782b203e100a.png) - + ![image](https://user-images.githubusercontent.com/34130764/112586659-35592900-8e22-11eb-94be-32bdff7ca883.png) + + ![image](https://user-images.githubusercontent.com/2493803/112913130-788bf400-90cf-11eb-84c6-782b203e100a.png) + ![image](https://user-images.githubusercontent.com/2493803/112913146-817cc580-90cf-11eb-87ad-ef79766be2b3.png) - Regression: Unify team actions to add a room to a team ([#21386](https://github.com/RocketChat/Rocket.Chat/pull/21386)) @@ -3414,10 +4568,8 @@ - Regression: Update .invite endpoints to support multiple users at once ([#21328](https://github.com/RocketChat/Rocket.Chat/pull/21328)) - - channels.invite now supports passing an array as a param (either with usernames or userIds) via `usernames` or `userIds` properties. - - - You can still use the endpoint to invite only one user via the old params `userId`, `username` or `user`. - + - channels.invite now supports passing an array as a param (either with usernames or userIds) via `usernames` or `userIds` properties. + - You can still use the endpoint to invite only one user via the old params `userId`, `username` or `user`. - Same changes apply to groups.invite - Regression: user actions in admin ([#21307](https://github.com/RocketChat/Rocket.Chat/pull/21307)) @@ -3426,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)) @@ -3454,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 🤓 @@ -3475,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 👩‍💻👨‍💻` @@ -3550,9 +4702,9 @@ ### 🚀 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. + 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. ### 🐛 Bug fixes @@ -3560,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. + 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 👩‍💻👨‍💻` @@ -3590,10 +4739,10 @@ - Cloud Workspace bridge ([#20838](https://github.com/RocketChat/Rocket.Chat/pull/20838)) - Adds the new CloudWorkspace functionality. - - It allows apps to request the access token for the workspace it's installed on, so it can perform actions with other Rocket.Chat services, such as the Omni Gateway. - + Adds the new CloudWorkspace functionality. + + It allows apps to request the access token for the workspace it's installed on, so it can perform actions with other Rocket.Chat services, such as the Omni Gateway. + https://github.com/RocketChat/Rocket.Chat.Apps-engine/pull/382 - Header with Breadcrumbs ([#20609](https://github.com/RocketChat/Rocket.Chat/pull/20609)) @@ -3611,51 +4760,51 @@ - Add symbol to indicate apps' required settings in the UI ([#20447](https://github.com/RocketChat/Rocket.Chat/pull/20447)) - - Apps are able to define **required** settings. These settings should not be left blank by the user and an error will be thrown and shown in the interface if an user attempts to save changes in the app details page leaving any required fields blank; - ![prt_screen_required_app_settings_warning](https://user-images.githubusercontent.com/36537004/106032964-e73cd900-60af-11eb-8eab-c11fd651b593.png) - - - A sign (*) is added to the label of app settings' fields that are required so as to highlight the fields which must not be left blank. + - Apps are able to define **required** settings. These settings should not be left blank by the user and an error will be thrown and shown in the interface if an user attempts to save changes in the app details page leaving any required fields blank; + ![prt_screen_required_app_settings_warning](https://user-images.githubusercontent.com/36537004/106032964-e73cd900-60af-11eb-8eab-c11fd651b593.png) + + - A sign (*) is added to the label of app settings' fields that are required so as to highlight the fields which must not be left blank. ![prt_screen_required_app_settings](https://user-images.githubusercontent.com/36537004/106014879-ae473900-609c-11eb-9b9e-95de7bbf20a5.png) - Add visual validation on users admin forms ([#20308](https://github.com/RocketChat/Rocket.Chat/pull/20308)) - 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. - Adds tooltip for sidebar header icons ([#19934](https://github.com/RocketChat/Rocket.Chat/pull/19934) by [@RonLek](https://github.com/RonLek)) - Previously the header icons in the sidebar didn't show a tooltip when hovered over. This PR fixes that. - + Previously the header icons in the sidebar didn't show a tooltip when hovered over. This PR fixes that. + ![Screenshot from 2020-12-22 15-17-41](https://user-images.githubusercontent.com/28918901/102874804-f2756700-4468-11eb-8324-b7f3194e62fe.png) - Better Presentation of Blockquotes ([#20750](https://github.com/RocketChat/Rocket.Chat/pull/20750) by [@aditya-mitra](https://github.com/aditya-mitra)) - Changed the values of `margin-top` and `margin-bottom` for *first* and *last* childs in blockquotes to increase readability. - - ### Before - - ![before](https://user-images.githubusercontent.com/55396651/107858662-3e3a0080-6e5b-11eb-8274-9bd956807235.png) - - ### Now - + Changed the values of `margin-top` and `margin-bottom` for *first* and *last* childs in blockquotes to increase readability. + + ### Before + + ![before](https://user-images.githubusercontent.com/55396651/107858662-3e3a0080-6e5b-11eb-8274-9bd956807235.png) + + ### Now + ![now](https://user-images.githubusercontent.com/55396651/107858471-480f3400-6e5a-11eb-9ccb-3f1be2fed0a4.png) - Change header based on room type ([#20612](https://github.com/RocketChat/Rocket.Chat/pull/20612)) 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. @@ -3669,18 +4818,13 @@ - Replace react-window for react-virtuoso package ([#20392](https://github.com/RocketChat/Rocket.Chat/pull/20392)) - Remove: - - - react-window - - - react-window-infinite-loader - - - simplebar-react - - Include: - - - react-virtuoso - + Remove: + - react-window + - react-window-infinite-loader + - simplebar-react + + Include: + - react-virtuoso - rc-scrollbars - Rewrite Call as React component ([#19778](https://github.com/RocketChat/Rocket.Chat/pull/19778)) @@ -3696,71 +4840,71 @@ - Add debouncing to add users search field. ([#20297](https://github.com/RocketChat/Rocket.Chat/pull/20297) by [@Darshilp326](https://github.com/Darshilp326)) - BEFORE - - https://user-images.githubusercontent.com/55157259/105350722-98a3c080-5c11-11eb-82f3-d9a62a4fa50b.mp4 - - - AFTER - + BEFORE + + https://user-images.githubusercontent.com/55157259/105350722-98a3c080-5c11-11eb-82f3-d9a62a4fa50b.mp4 + + + AFTER + https://user-images.githubusercontent.com/55157259/105350757-a2c5bf00-5c11-11eb-91db-25c0b9e01a28.mp4 - Add tooltips to Thread header buttons ([#20456](https://github.com/RocketChat/Rocket.Chat/pull/20456) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) 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. + 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)) - - 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. +- 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. + So unsetting data if data isn't available to save. Will also fix bio and other fields. :) - 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. - Blank Personal Access Token Bug ([#20193](https://github.com/RocketChat/Rocket.Chat/pull/20193) by [@RonLek](https://github.com/RonLek)) - Adds error when personal access token is blank thereby disallowing the creation of one. - + Adds error when personal access token is blank thereby disallowing the creation of one. + https://user-images.githubusercontent.com/28918901/104483631-5adde100-55ee-11eb-9938-64146bce127e.mp4 - CAS login failing due to TOTP requirement ([#20840](https://github.com/RocketChat/Rocket.Chat/pull/20840)) - Changed password input field for password access in edit room info. ([#20356](https://github.com/RocketChat/Rocket.Chat/pull/20356) by [@Darshilp326](https://github.com/Darshilp326)) - Password field would be secured with asterisks in edit room info - - https://user-images.githubusercontent.com/55157259/105641758-cad04f00-5eab-11eb-90de-0c91263edd55.mp4 - + Password field would be secured with asterisks in edit room info + + https://user-images.githubusercontent.com/55157259/105641758-cad04f00-5eab-11eb-90de-0c91263edd55.mp4 + . - Channel mentions showing user subscribed channels twice ([#20484](https://github.com/RocketChat/Rocket.Chat/pull/20484) by [@Darshilp326](https://github.com/Darshilp326)) - Channel mention shows user subscribed channels twice. - + Channel mention shows user subscribed channels twice. + https://user-images.githubusercontent.com/55157259/106183033-b353d780-61c5-11eb-8aab-1dbb62b02ff8.mp4 - CORS config not accepting multiple origins ([#20696](https://github.com/RocketChat/Rocket.Chat/pull/20696) by [@g-thome](https://github.com/g-thome)) @@ -3771,26 +4915,26 @@ - Default Attachments - Remove Extra Margin in Field Attachments ([#20618](https://github.com/RocketChat/Rocket.Chat/pull/20618) by [@aditya-mitra](https://github.com/aditya-mitra)) - A large amount of unnecessary margin which existed in the **Field Attachments inside the `DefaultAttachments`** has been fixed. - - ### Earlier - - ![earlier](https://user-images.githubusercontent.com/55396651/107056792-ba4b9d00-67f8-11eb-9153-05281416cddb.png) - - ### Now - + A large amount of unnecessary margin which existed in the **Field Attachments inside the `DefaultAttachments`** has been fixed. + + ### Earlier + + ![earlier](https://user-images.githubusercontent.com/55396651/107056792-ba4b9d00-67f8-11eb-9153-05281416cddb.png) + + ### Now + ![now](https://user-images.githubusercontent.com/55396651/107057196-3219c780-67f9-11eb-84db-e4a0addfc168.png) - Default Attachments - Show Full Attachment.Text with Markdown ([#20606](https://github.com/RocketChat/Rocket.Chat/pull/20606) by [@aditya-mitra](https://github.com/aditya-mitra)) - Removed truncating of text in `Attachment.Text`. - Added `Attachment.Text` to be parsed to markdown by default. - - ### Earlier - ![earlier](https://user-images.githubusercontent.com/55396651/106910781-92d8cf80-6727-11eb-82ec-818df7544ff0.png) - - ### Now - + Removed truncating of text in `Attachment.Text`. + Added `Attachment.Text` to be parsed to markdown by default. + + ### Earlier + ![earlier](https://user-images.githubusercontent.com/55396651/106910781-92d8cf80-6727-11eb-82ec-818df7544ff0.png) + + ### Now + ![now](https://user-images.githubusercontent.com/55396651/106910840-a126eb80-6727-11eb-8bd6-d86383dd9181.png) - Don't ask again not rendering ([#20745](https://github.com/RocketChat/Rocket.Chat/pull/20745)) @@ -3811,24 +4955,24 @@ - Feedback on bulk invite ([#20339](https://github.com/RocketChat/Rocket.Chat/pull/20339) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) - Resolved structure where no response was being received. Changed from callback to async/await. - Added error in case of empty submission, or if no valid emails were found. - + Resolved structure where no response was being received. Changed from callback to async/await. + Added error in case of empty submission, or if no valid emails were found. + https://user-images.githubusercontent.com/38764067/105613964-dfe5a900-5deb-11eb-80f2-21fc8dee57c0.mp4 - Filters are not being applied correctly in Omnichannel Current Chats list ([#20320](https://github.com/RocketChat/Rocket.Chat/pull/20320) by [@rafaelblink](https://github.com/rafaelblink)) - ### Before - ![image](https://user-images.githubusercontent.com/2493803/105537672-082cb500-5cd1-11eb-8f1b-1726ba60420a.png) - - ### After - ![image](https://user-images.githubusercontent.com/2493803/105537773-2d212800-5cd1-11eb-8746-048deb9502d9.png) - - ![image](https://user-images.githubusercontent.com/2493803/106494728-88090b00-6499-11eb-922e-5386107e2389.png) - + ### Before + ![image](https://user-images.githubusercontent.com/2493803/105537672-082cb500-5cd1-11eb-8f1b-1726ba60420a.png) + + ### After + ![image](https://user-images.githubusercontent.com/2493803/105537773-2d212800-5cd1-11eb-8746-048deb9502d9.png) + + ![image](https://user-images.githubusercontent.com/2493803/106494728-88090b00-6499-11eb-922e-5386107e2389.png) + ![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 @@ -3854,11 +4998,11 @@ - List of Omnichannel triggers is not listing data ([#20624](https://github.com/RocketChat/Rocket.Chat/pull/20624) by [@rafaelblink](https://github.com/rafaelblink)) - ### Before - ![image](https://user-images.githubusercontent.com/2493803/107095379-7308e080-67e7-11eb-8251-7e7ff891087a.png) - - - ### After + ### Before + ![image](https://user-images.githubusercontent.com/2493803/107095379-7308e080-67e7-11eb-8251-7e7ff891087a.png) + + + ### After ![image](https://user-images.githubusercontent.com/2493803/107095261-3b019d80-67e7-11eb-8425-8612b03ac50a.png) - Livechat bridge permission checkers ([#20653](https://github.com/RocketChat/Rocket.Chat/pull/20653) by [@lolimay](https://github.com/lolimay)) @@ -3881,11 +5025,10 @@ - Missing setting to control when to send the ReplyTo field in email notifications ([#20744](https://github.com/RocketChat/Rocket.Chat/pull/20744)) - - 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; - + - 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)) @@ -3915,15 +5058,15 @@ - Remove duplicate getCommonRoomEvents() event binding for starredMessages ([#20185](https://github.com/RocketChat/Rocket.Chat/pull/20185) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) - The getCommonRoomEvents() returned functions were bound to the starredMessages template twice. This was causing some bugs, as detailed in the Issue mentioned below. + The getCommonRoomEvents() returned functions were bound to the starredMessages template twice. This was causing some bugs, as detailed in the Issue mentioned below. I removed the top events call that only bound the getCommonRoomEvents(). Therefore, only one call for the same is left, which is at the end of the file. Having the events bound just once removes the bugs mentioned. - Remove warning problems from console ([#20800](https://github.com/RocketChat/Rocket.Chat/pull/20800)) - Removed tooltip in kebab menu options. ([#20498](https://github.com/RocketChat/Rocket.Chat/pull/20498) by [@Darshilp326](https://github.com/Darshilp326)) - Removed tooltip as it was not needed. - + Removed tooltip as it was not needed. + https://user-images.githubusercontent.com/55157259/106246146-a53ca000-6233-11eb-9874-cbd1b4331bc0.mp4 - Retry icon comes out of the div ([#20390](https://github.com/RocketChat/Rocket.Chat/pull/20390) by [@im-adithya](https://github.com/im-adithya)) @@ -3938,8 +5081,8 @@ - Room's last message's update date format on IE ([#20680](https://github.com/RocketChat/Rocket.Chat/pull/20680)) - The proposed change fixes a bug when updates the cached records on Internet Explorer and it breaks the sidebar as shown on the screenshot below: - + The proposed change fixes a bug when updates the cached records on Internet Explorer and it breaks the sidebar as shown on the screenshot below: + ![image](https://user-images.githubusercontent.com/27704687/107578007-f2285b00-6bd1-11eb-9250-1e76ae67f9c9.png) - Save user password and email from My Account ([#20737](https://github.com/RocketChat/Rocket.Chat/pull/20737)) @@ -3948,8 +5091,8 @@ - Selected hide system messages would now be viewed in vertical bar. ([#20358](https://github.com/RocketChat/Rocket.Chat/pull/20358) by [@Darshilp326](https://github.com/Darshilp326)) - All selected hide system messages are now in vertical Bar. - + All selected hide system messages are now in vertical Bar. + https://user-images.githubusercontent.com/55157259/105642624-d5411780-5eb0-11eb-8848-93e4b02629cb.mp4 - Selected messages don't get unselected ([#20408](https://github.com/RocketChat/Rocket.Chat/pull/20408) by [@im-adithya](https://github.com/im-adithya)) @@ -3964,22 +5107,14 @@ - Several Slack Importer issues ([#20216](https://github.com/RocketChat/Rocket.Chat/pull/20216)) - - Fix: Slack Importer crashes when importing a large users.json file - - - Fix: Slack importer crashes when messages have invalid mentions - - - Skip listing all users on the preparation screen when the user count is too large. - - - Split avatar download into a separate process. - - - Update room's last message when the import is complete. - - - Prevent invalid or duplicated channel names - - - Improve message error handling. - - - Reduce max allowed BSON size to avoid possible issues in some servers. - + - Fix: Slack Importer crashes when importing a large users.json file + - Fix: Slack importer crashes when messages have invalid mentions + - Skip listing all users on the preparation screen when the user count is too large. + - Split avatar download into a separate process. + - Update room's last message when the import is complete. + - Prevent invalid or duplicated channel names + - Improve message error handling. + - Reduce max allowed BSON size to avoid possible issues in some servers. - Improve handling of very large channel files. - star icon was visible after unstarring a message ([#19645](https://github.com/RocketChat/Rocket.Chat/pull/19645) by [@bhavayAnand9](https://github.com/bhavayAnand9)) @@ -3998,15 +5133,15 @@ - User statuses in admin user info panel ([#20341](https://github.com/RocketChat/Rocket.Chat/pull/20341) by [@RonLek](https://github.com/RonLek)) - Modifies user statuses in admin info panel based on their actual status instead of their `statusConnection`. This enables correct and consistent change in user statuses. - Also, bot users having status as online were classified as offline, with this change they are now correctly classified based on their corresponding statuses. - + Modifies user statuses in admin info panel based on their actual status instead of their `statusConnection`. This enables correct and consistent change in user statuses. + Also, bot users having status as online were classified as offline, with this change they are now correctly classified based on their corresponding statuses. + https://user-images.githubusercontent.com/28918901/105624438-b8bcc500-5e47-11eb-8d1e-3a4180da1304.mp4 - Users autocomplete showing duplicated results ([#20481](https://github.com/RocketChat/Rocket.Chat/pull/20481) by [@Darshilp326](https://github.com/Darshilp326)) - Added new query for outside room users so that room members are not shown twice. - + Added new query for outside room users so that room members are not shown twice. + https://user-images.githubusercontent.com/55157259/106174582-33c10b00-61bb-11eb-9716-377ef7bba34e.mp4
@@ -4027,7 +5162,7 @@ - Chore: Disable Sessions Aggregates tests locally ([#20607](https://github.com/RocketChat/Rocket.Chat/pull/20607)) - Disable Session aggregates tests in local environments + Disable Session aggregates tests in local environments For context, refer to: #20161 - Chore: Improve performance of messages’ watcher ([#20519](https://github.com/RocketChat/Rocket.Chat/pull/20519)) @@ -4107,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 🤓 @@ -4126,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 👩‍💻👨‍💻` @@ -4188,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. @@ -4207,7 +5342,6 @@ ### 👩‍💻👨‍💻 Contributors 😍 - [@lolimay](https://github.com/lolimay) -- [@yash-rajpal](https://github.com/yash-rajpal) ### 👩‍💻👨‍💻 Core Team 🤓 @@ -4215,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 👩‍💻👨‍💻` @@ -4236,20 +5371,18 @@ - **ENTERPRISE:** Omnichannel Contact Manager as preferred agent for routing ([#20244](https://github.com/RocketChat/Rocket.Chat/pull/20244)) - If the `Contact-Manager` is assigned to a Visitor, the chat will automatically get transferred to the respective Contact-Manager, provided the Contact-Manager is online. In-case the Contact-Manager is offline, the chat will be transferred to any other online agent. - We have provided a setting to control this auto-assignment feature - ![image](https://user-images.githubusercontent.com/34130764/104880961-8104d780-5986-11eb-9d87-82b99814b028.png) - - Behavior based-on Routing method - - - 1. Auto-selection, Load-Balancing, or External Service (`autoAssignAgent = true`) - This is straightforward, - - if the Contact-manager is online, the chat will be transferred to the Contact-Manger only - - if the Contact-manager is offline, the chat will be transferred to any other online-agent based on the Routing system - - 2. Manual-selection (`autoAssignAgent = false`) - - If the Contact-Manager is online, the chat will appear in the Queue of Contact-Manager **ONLY** + If the `Contact-Manager` is assigned to a Visitor, the chat will automatically get transferred to the respective Contact-Manager, provided the Contact-Manager is online. In-case the Contact-Manager is offline, the chat will be transferred to any other online agent. + We have provided a setting to control this auto-assignment feature + ![image](https://user-images.githubusercontent.com/34130764/104880961-8104d780-5986-11eb-9d87-82b99814b028.png) + + Behavior based-on Routing method + + 1. Auto-selection, Load-Balancing, or External Service (`autoAssignAgent = true`) + This is straightforward, + - if the Contact-manager is online, the chat will be transferred to the Contact-Manger only + - if the Contact-manager is offline, the chat will be transferred to any other online-agent based on the Routing system + 2. Manual-selection (`autoAssignAgent = false`) + - If the Contact-Manager is online, the chat will appear in the Queue of Contact-Manager **ONLY** - If the Contact-Manager is offline, the chat will appear in the Queue of all related Agents/Manager ( like it's done right now ) - Banner system and NPS ([#20221](https://github.com/RocketChat/Rocket.Chat/pull/20221)) @@ -4258,34 +5391,34 @@ - Email Inboxes for Omnichannel ([#20101](https://github.com/RocketChat/Rocket.Chat/pull/20101) by [@rafaelblink](https://github.com/rafaelblink)) - With this new feature, email accounts will receive email messages(threads) which will be transformed into Omnichannel chats. It'll be possible to set up multiple email accounts, test the connection with email server(email provider) and define the behaviour of each account. - - https://user-images.githubusercontent.com/2493803/105430398-242d4980-5c32-11eb-835a-450c94837d23.mp4 - - ### New item on admin menu - - ![image](https://user-images.githubusercontent.com/2493803/105428723-bc293400-5c2e-11eb-8c02-e8d36ea82726.png) - - - ### Send test email tooltip - - ![image](https://user-images.githubusercontent.com/2493803/104366986-eaa16380-54f8-11eb-9ba7-831cfde2319c.png) - - - ### Inbox Info - - ![image](https://user-images.githubusercontent.com/2493803/104366796-ab731280-54f8-11eb-9941-a3cc8eb610e1.png) - - ### SMTP Info - - ![image](https://user-images.githubusercontent.com/2493803/104366868-c47bc380-54f8-11eb-969e-ccc29070957c.png) - - ### IMAP Info - - ![image](https://user-images.githubusercontent.com/2493803/104366897-cd6c9500-54f8-11eb-80c4-97d5b0c002d5.png) - - ### Messages - + With this new feature, email accounts will receive email messages(threads) which will be transformed into Omnichannel chats. It'll be possible to set up multiple email accounts, test the connection with email server(email provider) and define the behaviour of each account. + + https://user-images.githubusercontent.com/2493803/105430398-242d4980-5c32-11eb-835a-450c94837d23.mp4 + + ### New item on admin menu + + ![image](https://user-images.githubusercontent.com/2493803/105428723-bc293400-5c2e-11eb-8c02-e8d36ea82726.png) + + + ### Send test email tooltip + + ![image](https://user-images.githubusercontent.com/2493803/104366986-eaa16380-54f8-11eb-9ba7-831cfde2319c.png) + + + ### Inbox Info + + ![image](https://user-images.githubusercontent.com/2493803/104366796-ab731280-54f8-11eb-9941-a3cc8eb610e1.png) + + ### SMTP Info + + ![image](https://user-images.githubusercontent.com/2493803/104366868-c47bc380-54f8-11eb-969e-ccc29070957c.png) + + ### IMAP Info + + ![image](https://user-images.githubusercontent.com/2493803/104366897-cd6c9500-54f8-11eb-80c4-97d5b0c002d5.png) + + ### Messages + ![image](https://user-images.githubusercontent.com/2493803/105428971-45d90180-5c2f-11eb-992a-022a3df94471.png) - Encrypted Discussions and new Encryption Permissions ([#20201](https://github.com/RocketChat/Rocket.Chat/pull/20201)) @@ -4297,7 +5430,7 @@ - Add extra SAML settings to update room subs and add private room subs. ([#19489](https://github.com/RocketChat/Rocket.Chat/pull/19489) by [@tlskinneriv](https://github.com/tlskinneriv)) - Added a SAML setting to support updating room subscriptions each time a user logs in via SAML. + Added a SAML setting to support updating room subscriptions each time a user logs in via SAML. Added a SAML setting to support including private rooms in SAML updated subscriptions (whether initial or on each logon). - Autofocus on directory ([#20509](https://github.com/RocketChat/Rocket.Chat/pull/20509)) @@ -4322,9 +5455,9 @@ 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. + 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) ### 🐛 Bug fixes @@ -4344,19 +5477,19 @@ 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. + 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 - Added success message on saving notification preference. ([#20220](https://github.com/RocketChat/Rocket.Chat/pull/20220) by [@Darshilp326](https://github.com/Darshilp326)) - Added success message after saving notification preferences. - + Added success message after saving notification preferences. + https://user-images.githubusercontent.com/55157259/104774617-03ca3e80-579d-11eb-8fa4-990b108dd8d9.mp4 - Admin User Info email verified status ([#20110](https://github.com/RocketChat/Rocket.Chat/pull/20110) by [@bdelwood](https://github.com/bdelwood)) @@ -4365,10 +5498,10 @@ - Change header's favorite icon to filled star ([#20174](https://github.com/RocketChat/Rocket.Chat/pull/20174)) - ### Before: - ![image](https://user-images.githubusercontent.com/27704687/104351819-a60bcd00-54e4-11eb-8b43-7d281a6e5dcb.png) - - ### After: + ### Before: + ![image](https://user-images.githubusercontent.com/27704687/104351819-a60bcd00-54e4-11eb-8b43-7d281a6e5dcb.png) + + ### After: ![image](https://user-images.githubusercontent.com/27704687/104351632-67761280-54e4-11eb-87ba-25b940494bb5.png) - Changed success message for adding custom sound. ([#20272](https://github.com/RocketChat/Rocket.Chat/pull/20272) by [@Darshilp326](https://github.com/Darshilp326)) @@ -4377,24 +5510,24 @@ - Changed success message for ignoring member. ([#19996](https://github.com/RocketChat/Rocket.Chat/pull/19996) by [@Darshilp326](https://github.com/Darshilp326)) - Different messages for ignoring/unignoring will be displayed. - + Different messages for ignoring/unignoring will be displayed. + https://user-images.githubusercontent.com/55157259/103310307-4241c880-4a3d-11eb-8c6c-4c9b99d023db.mp4 - Creation of Omnichannel rooms not working correctly through the Apps when the agent parameter is set ([#19997](https://github.com/RocketChat/Rocket.Chat/pull/19997)) - Engagement dashboard graphs labels superposing each other ([#20267](https://github.com/RocketChat/Rocket.Chat/pull/20267)) - Now after a certain breakpoint, the graphs should stack vertically, and overlapping text rotated. - + Now after a certain breakpoint, the graphs should stack vertically, and overlapping text rotated. + ![image](https://user-images.githubusercontent.com/40830821/105098926-93b40500-5a89-11eb-9a56-2fc3b1552914.png) - Fields overflowing page ([#20287](https://github.com/RocketChat/Rocket.Chat/pull/20287)) - ### Before - ![image](https://user-images.githubusercontent.com/40830821/105246952-c1b14c00-5b52-11eb-8671-cff88edf242d.png) - - ### After + ### Before + ![image](https://user-images.githubusercontent.com/40830821/105246952-c1b14c00-5b52-11eb-8671-cff88edf242d.png) + + ### After ![image](https://user-images.githubusercontent.com/40830821/105247125-0a690500-5b53-11eb-9f3c-d6a68108e336.png) - Fix error that occurs on changing archive status of room ([#20098](https://github.com/RocketChat/Rocket.Chat/pull/20098) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) @@ -4411,10 +5544,10 @@ - Livechat.RegisterGuest method removing unset fields ([#20124](https://github.com/RocketChat/Rocket.Chat/pull/20124) by [@rafaelblink](https://github.com/rafaelblink)) - 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. + 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 @@ -4432,18 +5565,18 @@ - Omnichannel - Contact Center form is not validating custom fields properly ([#20196](https://github.com/RocketChat/Rocket.Chat/pull/20196) by [@rafaelblink](https://github.com/rafaelblink)) - The contact form is accepting undefined values in required custom fields when creating or editing contacts, and, the errror message isn't following Rocket.chat design system. - - ### Before - ![image](https://user-images.githubusercontent.com/2493803/104522668-31688980-55dd-11eb-92c5-83f96073edc4.png) - - ### After - - #### New - ![image](https://user-images.githubusercontent.com/2493803/104770494-68f74300-574f-11eb-94a3-c8fd73365308.png) - - - #### Edit + The contact form is accepting undefined values in required custom fields when creating or editing contacts, and, the errror message isn't following Rocket.chat design system. + + ### Before + ![image](https://user-images.githubusercontent.com/2493803/104522668-31688980-55dd-11eb-92c5-83f96073edc4.png) + + ### After + + #### New + ![image](https://user-images.githubusercontent.com/2493803/104770494-68f74300-574f-11eb-94a3-c8fd73365308.png) + + + #### Edit ![image](https://user-images.githubusercontent.com/2493803/104770538-7b717c80-574f-11eb-829f-1ae304103369.png) - Omnichannel Agents unable to take new chats in the queue ([#20022](https://github.com/RocketChat/Rocket.Chat/pull/20022) by [@rafaelblink](https://github.com/rafaelblink)) @@ -4464,15 +5597,15 @@ - Room special name in prompts ([#20277](https://github.com/RocketChat/Rocket.Chat/pull/20277) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) - The "Hide room" and "Leave Room" confirmation prompts use the "name" key from the room info. When the setting " - Allow Special Characters in Room Names" is enabled, the prompts show the normalized names instead of those that contain the special characters. - - Changed the value being used from name to fname, which always has the user-set name. - - Previous: - ![Screenshot from 2021-01-20 15-52-29](https://user-images.githubusercontent.com/38764067/105161642-9b31e780-5b37-11eb-8b0c-ec4b1414c948.png) - - Updated: + The "Hide room" and "Leave Room" confirmation prompts use the "name" key from the room info. When the setting " + Allow Special Characters in Room Names" is enabled, the prompts show the normalized names instead of those that contain the special characters. + + Changed the value being used from name to fname, which always has the user-set name. + + Previous: + ![Screenshot from 2021-01-20 15-52-29](https://user-images.githubusercontent.com/38764067/105161642-9b31e780-5b37-11eb-8b0c-ec4b1414c948.png) + + Updated: ![Screenshot from 2021-01-20 15-50-19](https://user-images.githubusercontent.com/38764067/105161627-966d3380-5b37-11eb-9812-3dd9352b4f95.png) - Room's list showing all rooms with same name ([#20176](https://github.com/RocketChat/Rocket.Chat/pull/20176)) @@ -4483,9 +5616,9 @@ - Saving with blank email in edit user ([#20259](https://github.com/RocketChat/Rocket.Chat/pull/20259) by [@RonLek](https://github.com/RonLek)) - Disallows showing a success popup when email field is made blank in Edit User and instead shows the relevant error popup. - - + Disallows showing a success popup when email field is made blank in Edit User and instead shows the relevant error popup. + + https://user-images.githubusercontent.com/28918901/104960749-dbd81680-59fa-11eb-9c7b-2b257936f894.mp4 - Search list filter ([#19937](https://github.com/RocketChat/Rocket.Chat/pull/19937)) @@ -4494,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. @@ -4532,7 +5665,7 @@ - Add translation of Edit Status in all languages ([#19916](https://github.com/RocketChat/Rocket.Chat/pull/19916) by [@sushant52](https://github.com/sushant52)) - Closes [#19915](https://github.com/RocketChat/Rocket.Chat/issues/19915) + Closes [#19915](https://github.com/RocketChat/Rocket.Chat/issues/19915) The profile options menu is well translated in many languages. However, Edit Status is the only button which is not well translated. With this change, the whole profile options will be properly translated in a lot of languages. - Bump axios from 0.18.0 to 0.18.1 ([#20055](https://github.com/RocketChat/Rocket.Chat/pull/20055) by [@dependabot[bot]](https://github.com/dependabot[bot])) @@ -4567,10 +5700,10 @@ - Regression: Announcement bar not showing properly Markdown content ([#20290](https://github.com/RocketChat/Rocket.Chat/pull/20290)) - **Before**: - ![image](https://user-images.githubusercontent.com/27704687/105273746-a4907380-5b7a-11eb-8121-aff665251c44.png) - - **After**: + **Before**: + ![image](https://user-images.githubusercontent.com/27704687/105273746-a4907380-5b7a-11eb-8121-aff665251c44.png) + + **After**: ![image](https://user-images.githubusercontent.com/27704687/105274050-2e404100-5b7b-11eb-93b2-b6282a7bed95.png) - regression: Announcement link open in new tab ([#20435](https://github.com/RocketChat/Rocket.Chat/pull/20435)) @@ -4585,23 +5718,23 @@ - Regression: Change sort icon ([#20177](https://github.com/RocketChat/Rocket.Chat/pull/20177)) - ### Before - ![image](https://user-images.githubusercontent.com/40830821/104366414-1bcd6400-54f8-11eb-9fc7-c6f13f07a61e.png) - - ### After + ### Before + ![image](https://user-images.githubusercontent.com/40830821/104366414-1bcd6400-54f8-11eb-9fc7-c6f13f07a61e.png) + + ### After ![image](https://user-images.githubusercontent.com/40830821/104366542-4cad9900-54f8-11eb-83ca-acb99899515a.png) - Regression: Custom field labels are not displayed properly on Omnichannel Contact Profile form ([#20393](https://github.com/RocketChat/Rocket.Chat/pull/20393) by [@rafaelblink](https://github.com/rafaelblink)) - ### Before - ![image](https://user-images.githubusercontent.com/2493803/105780399-20116c80-5f4f-11eb-9620-0901472e453b.png) - - ![image](https://user-images.githubusercontent.com/2493803/105780420-2e5f8880-5f4f-11eb-8e93-8115ebc685be.png) - - ### After - - ![image](https://user-images.githubusercontent.com/2493803/105780832-1ccab080-5f50-11eb-8042-188dd0c41904.png) - + ### Before + ![image](https://user-images.githubusercontent.com/2493803/105780399-20116c80-5f4f-11eb-9620-0901472e453b.png) + + ![image](https://user-images.githubusercontent.com/2493803/105780420-2e5f8880-5f4f-11eb-8e93-8115ebc685be.png) + + ### After + + ![image](https://user-images.githubusercontent.com/2493803/105780832-1ccab080-5f50-11eb-8042-188dd0c41904.png) + ![image](https://user-images.githubusercontent.com/2493803/105780911-500d3f80-5f50-11eb-96e0-7df3f179dbd5.png) - Regression: ESLint Warning - explicit-function-return-type ([#20434](https://github.com/RocketChat/Rocket.Chat/pull/20434) by [@aditya-mitra](https://github.com/aditya-mitra)) @@ -4618,8 +5751,8 @@ - Regression: Fixed update room avatar issue. ([#20433](https://github.com/RocketChat/Rocket.Chat/pull/20433) by [@Darshilp326](https://github.com/Darshilp326)) - Users can now update their room avatar without any error. - + Users can now update their room avatar without any error. + https://user-images.githubusercontent.com/55157259/105951602-560d3880-6096-11eb-97a5-b5eb9a28b58d.mp4 - Regression: Info Page Icon style and usage graph breaking ([#20180](https://github.com/RocketChat/Rocket.Chat/pull/20180)) @@ -4636,11 +5769,11 @@ - Regression: Unread superposing announcement. ([#20306](https://github.com/RocketChat/Rocket.Chat/pull/20306)) - ### Before - ![image](https://user-images.githubusercontent.com/40830821/105412619-c2f67d80-5c13-11eb-8204-5932ea880c8a.png) - - - ### After + ### Before + ![image](https://user-images.githubusercontent.com/40830821/105412619-c2f67d80-5c13-11eb-8204-5932ea880c8a.png) + + + ### After ![image](https://user-images.githubusercontent.com/40830821/105411176-d1439a00-5c11-11eb-8d1b-ea27c8485214.png) - Regression: User Dropdown margin ([#20222](https://github.com/RocketChat/Rocket.Chat/pull/20222)) @@ -4698,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 🤓 @@ -4716,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 👩‍💻👨‍💻` @@ -4928,8 +6061,8 @@ - Hightlights validation on Account Preferences page ([#19902](https://github.com/RocketChat/Rocket.Chat/pull/19902) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) - This PR fixes two issues in the account settings "preferences" panel. - Once set, the "Highlighted Words" setting cannot be reset to an empty string. This was fixed by changing the string validation from checking the length to checking the type of variable. + This PR fixes two issues in the account settings "preferences" panel. + Once set, the "Highlighted Words" setting cannot be reset to an empty string. This was fixed by changing the string validation from checking the length to checking the type of variable. Secondly, it tracks the changes to correctly identify if changes after the last "save changes" action have been made, using an "updates" state variable, instead of just comparing against the initialValue that does not change on clicking "save changes". - Image preview for image URLs on messages ([#19734](https://github.com/RocketChat/Rocket.Chat/pull/19734) by [@g-thome](https://github.com/g-thome)) @@ -4988,14 +6121,10 @@ - Chore: Update Pull Request template ([#19768](https://github.com/RocketChat/Rocket.Chat/pull/19768)) - Improve the template of Pull Requests in order to make it clear reducing duplicated information and removing the visible checklists that were generating noise and misunderstanding with the PR progress. - - - Moved the checklists to inside comments - - - Merge the changelog and proposed changes sections to have a single source of description that goes to the changelog - - - Remove the screenshot section, they can be added inside the description - + Improve the template of Pull Requests in order to make it clear reducing duplicated information and removing the visible checklists that were generating noise and misunderstanding with the PR progress. + - Moved the checklists to inside comments + - Merge the changelog and proposed changes sections to have a single source of description that goes to the changelog + - Remove the screenshot section, they can be added inside the description - Changed the proposed changes title to incentivizing the usage of images and videos - Frontend folder structure ([#19631](https://github.com/RocketChat/Rocket.Chat/pull/19631)) @@ -5030,11 +6159,11 @@ - Regression: Double Scrollbars on tables ([#19980](https://github.com/RocketChat/Rocket.Chat/pull/19980)) - Before: - ![image](https://user-images.githubusercontent.com/40830821/103242719-0ec84680-4936-11eb-87a7-68b6eea8de7b.png) - - - After: + Before: + ![image](https://user-images.githubusercontent.com/40830821/103242719-0ec84680-4936-11eb-87a7-68b6eea8de7b.png) + + + After: ![image](https://user-images.githubusercontent.com/40830821/103242680-ee988780-4935-11eb-99e2-a95de99f78f1.png) - Regression: Failed autolinker and markdown rendering ([#19831](https://github.com/RocketChat/Rocket.Chat/pull/19831)) @@ -5053,7 +6182,7 @@ - Regression: Omnichannel Custom Fields Form no longer working after refactoring ([#19948](https://github.com/RocketChat/Rocket.Chat/pull/19948)) - The Omnichannel `Custom Fields` form is not working anymore after some refactorings on client-side. + The Omnichannel `Custom Fields` form is not working anymore after some refactorings on client-side. When the user clicks on `Custom Field` in the Omnichannel menu, a blank page appears. - Regression: polishing licenses endpoints ([#19981](https://github.com/RocketChat/Rocket.Chat/pull/19981) by [@g-thome](https://github.com/g-thome)) @@ -5252,8 +6381,8 @@ - Bundle Size Client ([#19533](https://github.com/RocketChat/Rocket.Chat/pull/19533)) - temporarily removes some codeblock languages - Moved some libraries to dynamic imports + temporarily removes some codeblock languages + Moved some libraries to dynamic imports Removed some shared code not used on the client side - Forward Omnichannel room to agent in another department ([#19576](https://github.com/RocketChat/Rocket.Chat/pull/19576) by [@mrfigueiredo](https://github.com/mrfigueiredo)) @@ -6334,10 +7463,8 @@ - **2FA:** Password enforcement setting and 2FA protection when saving settings or resetting E2E encryption ([#18640](https://github.com/RocketChat/Rocket.Chat/pull/18640)) - - Increase the 2FA remembering time from 5min to 30min - - - Add new setting to enforce 2FA password fallback (enabled only for new installations) - + - Increase the 2FA remembering time from 5min to 30min + - Add new setting to enforce 2FA password fallback (enabled only for new installations) - Require 2FA to save settings and reset E2E Encryption keys - **Omnichannel:** Allow set other agent status via method `livechat:changeLivechatStatus ` ([#18571](https://github.com/RocketChat/Rocket.Chat/pull/18571)) @@ -6355,7 +7482,7 @@ - 2FA by Email setting showing for the user even when disabled by the admin ([#18473](https://github.com/RocketChat/Rocket.Chat/pull/18473)) - The option to disable/enable the **Two-factor authentication via Email** at `Account > Security > Two Factor Authentication + The option to disable/enable the **Two-factor authentication via Email** at `Account > Security > Two Factor Authentication ` was visible even when the setting **Enable Two Factor Authentication via Email** at `Admin > Accounts > Two Factor Authentication` was disabled leading to misbehavior since the functionality was disabled. - Agents enabledDepartment attribute not set on collection ([#18614](https://github.com/RocketChat/Rocket.Chat/pull/18614) by [@paulobernardoaf](https://github.com/paulobernardoaf)) @@ -6705,16 +7832,13 @@ - Mention autocomplete UI and performance improvements ([#18309](https://github.com/RocketChat/Rocket.Chat/pull/18309)) - * New setting to configure the number of suggestions `Admin > Layout > User Interface > Number of users' autocomplete suggestions` (default 5) - - * The UI shows whenever the user is not a member of the room - - * The UI shows when the suggestion came from the last messages for quick selection/reply - - * The suggestions follow this order: - * The user with the exact username and member of the room - * The user with the exact username but not a member of the room (if allowed to list non-members) - * The users containing the text in username, name or nickname and member of the room + * New setting to configure the number of suggestions `Admin > Layout > User Interface > Number of users' autocomplete suggestions` (default 5) + * The UI shows whenever the user is not a member of the room + * The UI shows when the suggestion came from the last messages for quick selection/reply + * The suggestions follow this order: + * The user with the exact username and member of the room + * The user with the exact username but not a member of the room (if allowed to list non-members) + * The users containing the text in username, name or nickname and member of the room * The users containing the text in username, name or nickname and not a member of the room (if allowed to list non-members) - Message action styles ([#18190](https://github.com/RocketChat/Rocket.Chat/pull/18190)) @@ -7056,10 +8180,10 @@ - Split NOTIFICATIONS_SCHEDULE_DELAY into three separate variables ([#17669](https://github.com/RocketChat/Rocket.Chat/pull/17669) by [@jazztickets](https://github.com/jazztickets)) - Email notification delay can now be customized with the following environment variables: - NOTIFICATIONS_SCHEDULE_DELAY_ONLINE - NOTIFICATIONS_SCHEDULE_DELAY_AWAY - NOTIFICATIONS_SCHEDULE_DELAY_OFFLINE + Email notification delay can now be customized with the following environment variables: + NOTIFICATIONS_SCHEDULE_DELAY_ONLINE + NOTIFICATIONS_SCHEDULE_DELAY_AWAY + NOTIFICATIONS_SCHEDULE_DELAY_OFFLINE Setting the value to -1 disable notifications for that type. - Threads ([#17416](https://github.com/RocketChat/Rocket.Chat/pull/17416)) @@ -7459,11 +8583,11 @@ - **ENTERPRISE:** Omnichannel Last-Chatted Agent Preferred option ([#17666](https://github.com/RocketChat/Rocket.Chat/pull/17666)) - If activated, this feature will store the last agent that assisted each Omnichannel visitor when a conversation is taken. So, when a visitor returns(it works with any entry point, Livechat, Facebook, REST API, and so on) and starts a new chat, the routing system checks: - - 1 - The visitor object for any stored agent that the visitor has previously talked to; - 2 - If a previous agent is not found, the system will try to find a previous conversation of the same visitor. If a room is found, the system will get the previous agent from the room; - + If activated, this feature will store the last agent that assisted each Omnichannel visitor when a conversation is taken. So, when a visitor returns(it works with any entry point, Livechat, Facebook, REST API, and so on) and starts a new chat, the routing system checks: + + 1 - The visitor object for any stored agent that the visitor has previously talked to; + 2 - If a previous agent is not found, the system will try to find a previous conversation of the same visitor. If a room is found, the system will get the previous agent from the room; + After this process, if an agent has been found, the system will check the agent's availability to assist the new chat. If it's not available, then the routing system will get the next available agent in the queue. - **ENTERPRISE:** Support for custom Livechat registration form fields ([#17581](https://github.com/RocketChat/Rocket.Chat/pull/17581)) @@ -7568,12 +8692,9 @@ - Notification sounds ([#17616](https://github.com/RocketChat/Rocket.Chat/pull/17616)) - * Global CDN config was ignored when loading the sound files - - * Upload of custom sounds wasn't getting the file extension correctly - - * Some translations were missing - + * Global CDN config was ignored when loading the sound files + * Upload of custom sounds wasn't getting the file extension correctly + * Some translations were missing * Edit and delete of custom sounds were not working correctly - Omnichannel departments are not saved when the offline channel name is not defined ([#17553](https://github.com/RocketChat/Rocket.Chat/pull/17553)) @@ -7861,19 +8982,14 @@ - Better Push and Email Notification logic ([#17357](https://github.com/RocketChat/Rocket.Chat/pull/17357)) - We are still using the same logic to define which notifications every new message will generate, it takes some servers' settings, users's preferences and subscriptions' settings in consideration to determine who will receive each notification type (desktop, audio, email and mobile push), but now it doesn't check the user's status (online, away, offline) for email and mobile push notifications but send those notifications to a new queue with the following rules: - - - - When the user is online the notification is scheduled to be sent in 120 seconds - - - When the user is away the notification is scheduled to be sent in 120 seconds minus the amount of time he is away - - - When the user is offline the notification is scheduled to be sent right away - - - When the user reads a channel all the notifications for that user are removed (clear queue) - - - When a notification is processed to be sent to a user and there are other scheduled notifications: - - All the scheduled notifications for that user are rescheduled to now + We are still using the same logic to define which notifications every new message will generate, it takes some servers' settings, users's preferences and subscriptions' settings in consideration to determine who will receive each notification type (desktop, audio, email and mobile push), but now it doesn't check the user's status (online, away, offline) for email and mobile push notifications but send those notifications to a new queue with the following rules: + + - When the user is online the notification is scheduled to be sent in 120 seconds + - When the user is away the notification is scheduled to be sent in 120 seconds minus the amount of time he is away + - When the user is offline the notification is scheduled to be sent right away + - When the user reads a channel all the notifications for that user are removed (clear queue) + - When a notification is processed to be sent to a user and there are other scheduled notifications: + - All the scheduled notifications for that user are rescheduled to now - The current notification goes back to the queue to be processed ordered by creation date - Buttons to check/uncheck all users and channels on import ([#17207](https://github.com/RocketChat/Rocket.Chat/pull/17207)) @@ -8236,7 +9352,7 @@ - Translation via MS translate ([#16363](https://github.com/RocketChat/Rocket.Chat/pull/16363) by [@mrsimpson](https://github.com/mrsimpson)) - Adds Microsoft's translation service (https://translator.microsoft.com/) as a provider for translation of messages. + Adds Microsoft's translation service (https://translator.microsoft.com/) as a provider for translation of messages. In addition to implementing the interface (similar to google and DeepL), a small change has been done in order to display the translation provider on the UI. - Two Factor authentication via email ([#15949](https://github.com/RocketChat/Rocket.Chat/pull/15949)) @@ -8415,7 +9531,7 @@ - Slash command preview: Wrong item being selected, Horizontal scroll ([#16750](https://github.com/RocketChat/Rocket.Chat/pull/16750)) -- Text formatted to remain within button even on screen resize ([#14136](https://github.com/RocketChat/Rocket.Chat/pull/14136) by [@Rodriq](https://github.com/Rodriq)) +- Text formatted to remain within button even on screen resize ([#14136](https://github.com/RocketChat/Rocket.Chat/pull/14136)) - There is no option to pin a thread message by admin ([#16457](https://github.com/RocketChat/Rocket.Chat/pull/16457) by [@ashwaniYDV](https://github.com/ashwaniYDV)) @@ -8621,7 +9737,6 @@ - [@GOVINDDIXIT](https://github.com/GOVINDDIXIT) - [@MarcosSpessatto](https://github.com/MarcosSpessatto) - [@Nikhil713](https://github.com/Nikhil713) -- [@Rodriq](https://github.com/Rodriq) - [@aKn1ghtOut](https://github.com/aKn1ghtOut) - [@antkaz](https://github.com/antkaz) - [@aryamanpuri](https://github.com/aryamanpuri) @@ -8649,6 +9764,7 @@ ### 👩‍💻👨‍💻 Core Team 🤓 - [@PrajvalRaval](https://github.com/PrajvalRaval) +- [@Rodriq](https://github.com/Rodriq) - [@Sing-Li](https://github.com/Sing-Li) - [@d-gubert](https://github.com/d-gubert) - [@engelgabriel](https://github.com/engelgabriel) @@ -14409,7 +15525,7 @@ 🔍 Minor changes -- Add reetp to the issues' bot whitelist ([#12227](https://github.com/RocketChat/Rocket.Chat/pull/12227)) +- Add reetp to the issues' bot whitelist ([#12227](https://github.com/RocketChat/Rocket.Chat/pull/12227) by [@theorenck](https://github.com/theorenck)) - Fix: Remove semver satisfies from Apps details that is already done my marketplace ([#12268](https://github.com/RocketChat/Rocket.Chat/pull/12268)) @@ -14417,7 +15533,7 @@ - Regression: fix modal submit ([#12233](https://github.com/RocketChat/Rocket.Chat/pull/12233)) -- Release 0.70.1 ([#12270](https://github.com/RocketChat/Rocket.Chat/pull/12270) by [@Hudell](https://github.com/Hudell) & [@edzluhan](https://github.com/edzluhan)) +- Release 0.70.1 ([#12270](https://github.com/RocketChat/Rocket.Chat/pull/12270) by [@Hudell](https://github.com/Hudell) & [@edzluhan](https://github.com/edzluhan) & [@theorenck](https://github.com/theorenck))
@@ -14427,6 +15543,7 @@ - [@cardoso](https://github.com/cardoso) - [@edzluhan](https://github.com/edzluhan) - [@kaiiiiiiiii](https://github.com/kaiiiiiiiii) +- [@theorenck](https://github.com/theorenck) - [@timkinnane](https://github.com/timkinnane) ### 👩‍💻👨‍💻 Core Team 🤓 @@ -14436,7 +15553,6 @@ - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) - [@tassoevan](https://github.com/tassoevan) -- [@theorenck](https://github.com/theorenck) # 0.70.0 `2018-09-28 · 2 ️️️⚠️ · 18 🎉 · 3 🚀 · 35 🐛 · 19 🔍 · 32 👩‍💻👨‍💻` @@ -19775,4 +20891,4 @@ - [@graywolf336](https://github.com/graywolf336) - [@marceloschmidt](https://github.com/marceloschmidt) - [@rodrigok](https://github.com/rodrigok) -- [@sampaiodiego](https://github.com/sampaiodiego) +- [@sampaiodiego](https://github.com/sampaiodiego) \ No newline at end of file diff --git a/README.md b/README.md index 6c47c5f0a1a3b..8c4efc78514fd 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ We are a MERN based application enabling real-time conversations between colleag Every day, tens of millions of users in over 150 countries and in organizations such as Deutsche Bahn, The US Navy, and Credit Suisse trust [Rocket.Chat](https://rocket.chat) to keep their communications completely private and secure. - * [Review product documentation](https://docs.rocket.chat/) - * [Review developer documentation](https://developer.rocket.chat/) + * [Review product documentation](https://docs.rocket.chat) + * [Review developer documentation](https://developer.rocket.chat) Using our self-managed offerings you can deploy Rocket.Chat on your own server, or you can use SaaS Rocket.Chat. We offer support for both community as well as commercial plans. @@ -21,7 +21,7 @@ https://cloud.rocket.chat/trial ## Installation Please see the [requirements documentation](https://docs.rocket.chat/installing-and-updating/minimum-requirements-for-using-rocket.chat) for system requirements and more information about supported operating systems. -Please refer to [Install Rocket.Chat](https://rocket.chat/install/) to install your Rocket.Chat instance. +Please refer to [Install Rocket.Chat](https://rocket.chat/install) to install your Rocket.Chat instance. ## Feature Request @@ -30,9 +30,10 @@ Please refer to [Install Rocket.Chat](https://rocket.chat/install/) to install y ## Community -Join thousands of members worldwide in our [community server](https://open.rocket.chat/). +Join thousands of members worldwide in our [community server](https://open.rocket.chat). Join [#Support](https://open.rocket.chat/channel/support) for help from our community with general Rocket.Chat questions. Join [#Dev](https://open.rocket.chat/channel/dev) for needing help from the community to develop new features. +Talk with Rocket.Chat's leadership at the [Community Open Call](https://www.youtube.com/watch?v=RdbqOdUb3Wk), held monthly. Join us for [the next Community Open Call](https://app.livestorm.co/rocket-chat/community-open-call?type=detailed). ## Contributions @@ -40,32 +41,32 @@ Rocket.Chat is an open source project and we are very happy to accept community ## Credits -* Emoji provided graciously by [JoyPixels](https://www.joypixels.com/) -* Testing with [BrowserStack](https://www.browserstack.com/) -* Translations done with [LingoHub](https://lingohub.com/) +* Emoji provided graciously by [JoyPixels](https://www.joypixels.com). +* Testing with [BrowserStack](https://www.browserstack.com). +* Translations done with [LingoHub](https://lingohub.com). ## Mobile Apps In addition to the web interface, you can also download Rocket.Chat clients for: -[![Rocket.Chat on Apple App Store](https://user-images.githubusercontent.com/551004/29770691-a2082ff4-8bc6-11e7-89a6-964cd405ea8e.png)](https://itunes.apple.com/us/app/rocket-chat/id1148741252?mt=8) [![Rocket.Chat on Google Play](https://user-images.githubusercontent.com/551004/29770692-a20975c6-8bc6-11e7-8ab0-1cde275496e0.png)](https://play.google.com/store/apps/details?id=chat.rocket.android) [![](https://user-images.githubusercontent.com/551004/48210349-50649480-e35e-11e8-97d9-74a4331faf3a.png)](https://f-droid.org/en/packages/chat.rocket.android/) +[![Rocket.Chat on Apple App Store](https://user-images.githubusercontent.com/551004/29770691-a2082ff4-8bc6-11e7-89a6-964cd405ea8e.png)](https://itunes.apple.com/us/app/rocket-chat/id1148741252?mt=8) [![Rocket.Chat on Google Play](https://user-images.githubusercontent.com/551004/29770692-a20975c6-8bc6-11e7-8ab0-1cde275496e0.png)](https://play.google.com/store/apps/details?id=chat.rocket.android) [![](https://user-images.githubusercontent.com/551004/48210349-50649480-e35e-11e8-97d9-74a4331faf3a.png)](https://f-droid.org/en/packages/chat.rocket.android) ## Learn More -* [API](https://developer.rocket.chat/) -* [See who's using Rocket.Chat](https://rocket.chat/customer-stories/) +* [API](https://developer.rocket.chat) +* [See who's using Rocket.Chat](https://rocket.chat/customer-stories) ## Become a Rocketeer -We're hiring developers, support people, and product managers all the time. Please check our [jobs page](https://rocket.chat/jobs/). +We're hiring developers, support people, and product managers all the time. Please check our [jobs page](https://rocket.chat/jobs). ## Get the Latest News * [Twitter](https://twitter.com/RocketChat) -* [Blog](https://rocket.chat/blog/) -* [Facebook](https://www.facebook.com/RocketChatApp/) -* [LinkedIn](https://www.linkedin.com/company/rocket-chat/) +* [Blog](https://rocket.chat/blog) +* [Facebook](https://www.facebook.com/RocketChatApp) +* [LinkedIn](https://www.linkedin.com/company/rocket-chat) * [Youtube](https://www.youtube.com/channel/UCin9nv7mUjoqrRiwrzS5UVQ) -* [Email Newsletter](https://rocket.chat/newsletter/) +* [Email Newsletter](https://rocket.chat/newsletter) Any other questions, reach out to us at [contact@rocket.chat](contact@rocket.chat). We’d happy to help! diff --git a/app/2fa/README.md b/app/2fa/README.md deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/app/2fa/client/TOTPCrowd.js b/app/2fa/client/TOTPCrowd.js index 44a08fe8b69f4..fa5d635c57653 100644 --- a/app/2fa/client/TOTPCrowd.js +++ b/app/2fa/client/TOTPCrowd.js @@ -1,8 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; -import { Utils2fa } from './lib/2fa'; import '../../crowd/client/index'; +import { reportError } from '../../../client/lib/2fa/utils'; +import { overrideLoginMethod } from '../../../client/lib/2fa/overrideLoginMethod'; Meteor.loginWithCrowdAndTOTP = function(username, password, code, callback) { const loginRequest = { @@ -20,7 +21,7 @@ Meteor.loginWithCrowdAndTOTP = function(username, password, code, callback) { }], userCallback(error) { if (error) { - Utils2fa.reportError(error, callback); + reportError(error, callback); } else { callback && callback(); } @@ -31,5 +32,5 @@ Meteor.loginWithCrowdAndTOTP = function(username, password, code, callback) { const { loginWithCrowd } = Meteor; Meteor.loginWithCrowd = function(username, password, callback) { - Utils2fa.overrideLoginMethod(loginWithCrowd, [username, password], callback, Meteor.loginWithCrowdAndTOTP); + overrideLoginMethod(loginWithCrowd, [username, password], callback, Meteor.loginWithCrowdAndTOTP); }; diff --git a/app/2fa/client/TOTPGoogle.js b/app/2fa/client/TOTPGoogle.js index 24c7c66b6908c..a28e8932f5f84 100644 --- a/app/2fa/client/TOTPGoogle.js +++ b/app/2fa/client/TOTPGoogle.js @@ -3,7 +3,7 @@ import { Accounts } from 'meteor/accounts-base'; import { Google } from 'meteor/google-oauth'; import _ from 'underscore'; -import { Utils2fa } from './lib/2fa'; +import { overrideLoginMethod } from '../../../client/lib/2fa/overrideLoginMethod'; const loginWithGoogleAndTOTP = function(options, code, callback) { // support a callback without options @@ -37,5 +37,5 @@ const loginWithGoogleAndTOTP = function(options, code, callback) { const { loginWithGoogle } = Meteor; Meteor.loginWithGoogle = function(options, cb) { - Utils2fa.overrideLoginMethod(loginWithGoogle, [options], cb, loginWithGoogleAndTOTP); + overrideLoginMethod(loginWithGoogle, [options], cb, loginWithGoogleAndTOTP); }; diff --git a/app/2fa/client/TOTPLDAP.js b/app/2fa/client/TOTPLDAP.js index 0d74719f7cf87..0bc2c85c9595f 100644 --- a/app/2fa/client/TOTPLDAP.js +++ b/app/2fa/client/TOTPLDAP.js @@ -1,8 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; -import { Utils2fa } from './lib/2fa'; -import '../../ldap/client/loginHelper'; +import '../../../client/startup/ldap'; +import { reportError } from '../../../client/lib/2fa/utils'; +import { overrideLoginMethod } from '../../../client/lib/2fa/overrideLoginMethod'; Meteor.loginWithLDAPAndTOTP = function(...args) { // Pull username and password @@ -34,7 +35,7 @@ Meteor.loginWithLDAPAndTOTP = function(...args) { }], userCallback(error) { if (error) { - Utils2fa.reportError(error, callback); + reportError(error, callback); } else { callback && callback(); } @@ -47,5 +48,5 @@ const { loginWithLDAP } = Meteor; Meteor.loginWithLDAP = function(...args) { const callback = typeof args[args.length - 1] === 'function' ? args.pop() : null; - Utils2fa.overrideLoginMethod(loginWithLDAP, args, callback, Meteor.loginWithLDAPAndTOTP, args[0]); + overrideLoginMethod(loginWithLDAP, args, callback, Meteor.loginWithLDAPAndTOTP, args[0]); }; diff --git a/app/2fa/client/TOTPOAuth.js b/app/2fa/client/TOTPOAuth.js index 5e2859c043f0a..e3a43b3af8686 100644 --- a/app/2fa/client/TOTPOAuth.js +++ b/app/2fa/client/TOTPOAuth.js @@ -8,9 +8,10 @@ import { MeteorDeveloperAccounts } from 'meteor/meteor-developer-oauth'; import { Linkedin } from 'meteor/pauli:linkedin-oauth'; import { OAuth } from 'meteor/oauth'; -import { Utils2fa } from './lib/2fa'; -import { process2faReturn } from './callWithTwoFactorRequired'; +import { process2faReturn } from '../../../client/lib/2fa/process2faReturn'; import { CustomOAuth } from '../../custom-oauth'; +import { convertError } from '../../../client/lib/2fa/utils'; +import { overrideLoginMethod } from '../../../client/lib/2fa/overrideLoginMethod'; let lastCredentialToken = null; let lastCredentialSecret = null; @@ -36,7 +37,7 @@ Accounts.oauth.tryLoginAfterPopupClosed = function(credentialToken, callback, to Accounts.callLoginMethod({ methodArguments: [methodArgument], userCallback: callback && function(err) { - callback(Utils2fa.convertError(err)); + callback(convertError(err)); } }); }; @@ -74,31 +75,31 @@ const loginWithOAuthTokenAndTOTP = createOAuthTotpLoginMethod(); const loginWithFacebookAndTOTP = createOAuthTotpLoginMethod(() => Facebook); const { loginWithFacebook } = Meteor; Meteor.loginWithFacebook = function(options, cb) { - Utils2fa.overrideLoginMethod(loginWithFacebook, [options], cb, loginWithFacebookAndTOTP); + overrideLoginMethod(loginWithFacebook, [options], cb, loginWithFacebookAndTOTP); }; const loginWithGithubAndTOTP = createOAuthTotpLoginMethod(() => Github); const { loginWithGithub } = Meteor; Meteor.loginWithGithub = function(options, cb) { - Utils2fa.overrideLoginMethod(loginWithGithub, [options], cb, loginWithGithubAndTOTP); + overrideLoginMethod(loginWithGithub, [options], cb, loginWithGithubAndTOTP); }; const loginWithMeteorDeveloperAccountAndTOTP = createOAuthTotpLoginMethod(() => MeteorDeveloperAccounts); const { loginWithMeteorDeveloperAccount } = Meteor; Meteor.loginWithMeteorDeveloperAccount = function(options, cb) { - Utils2fa.overrideLoginMethod(loginWithMeteorDeveloperAccount, [options], cb, loginWithMeteorDeveloperAccountAndTOTP); + overrideLoginMethod(loginWithMeteorDeveloperAccount, [options], cb, loginWithMeteorDeveloperAccountAndTOTP); }; const loginWithTwitterAndTOTP = createOAuthTotpLoginMethod(() => Twitter); const { loginWithTwitter } = Meteor; Meteor.loginWithTwitter = function(options, cb) { - Utils2fa.overrideLoginMethod(loginWithTwitter, [options], cb, loginWithTwitterAndTOTP); + overrideLoginMethod(loginWithTwitter, [options], cb, loginWithTwitterAndTOTP); }; const loginWithLinkedinAndTOTP = createOAuthTotpLoginMethod(() => Linkedin); const { loginWithLinkedin } = Meteor; Meteor.loginWithLinkedin = function(options, cb) { - Utils2fa.overrideLoginMethod(loginWithLinkedin, [options], cb, loginWithLinkedinAndTOTP); + overrideLoginMethod(loginWithLinkedin, [options], cb, loginWithLinkedinAndTOTP); }; Accounts.onPageLoadLogin((loginAttempt) => { @@ -133,6 +134,6 @@ CustomOAuth.prototype.configureLogin = function(...args) { const oldMethod = Meteor[loginWithService]; Meteor[loginWithService] = function(options, cb) { - Utils2fa.overrideLoginMethod(oldMethod, [options], cb, loginWithOAuthTokenAndTOTP); + overrideLoginMethod(oldMethod, [options], cb, loginWithOAuthTokenAndTOTP); }; }; diff --git a/app/2fa/client/TOTPPassword.js b/app/2fa/client/TOTPPassword.js index 430049560bac1..9886938fef663 100644 --- a/app/2fa/client/TOTPPassword.js +++ b/app/2fa/client/TOTPPassword.js @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; -import toastr from 'toastr'; -import { Utils2fa } from './lib/2fa'; import { t } from '../../utils'; -import { process2faReturn } from './callWithTwoFactorRequired'; +import { process2faReturn } from '../../../client/lib/2fa/process2faReturn'; +import { isTotpInvalidError, reportError } from '../../../client/lib/2fa/utils'; +import { dispatchToastMessage } from '../../../client/lib/toast'; Meteor.loginWithPasswordAndTOTP = function(selector, password, code, callback) { if (typeof selector === 'string') { @@ -27,7 +27,7 @@ Meteor.loginWithPasswordAndTOTP = function(selector, password, code, callback) { }], userCallback(error) { if (error) { - Utils2fa.reportError(error, callback); + reportError(error, callback); } else { callback && callback(); } @@ -45,12 +45,16 @@ Meteor.loginWithPassword = function(email, password, cb) { emailOrUsername: email, onCode: (code) => { Meteor.loginWithPasswordAndTOTP(email, password, code, (error) => { - if (error && error.error === 'totp-invalid') { - toastr.error(t('Invalid_two_factor_code')); + if (isTotpInvalidError(error)) { + dispatchToastMessage({ + type: 'error', + message: t('Invalid_two_factor_code'), + }); cb(); - } else { - cb(error); + return; } + + cb(error); }); }, }); diff --git a/app/2fa/client/TOTPSaml.js b/app/2fa/client/TOTPSaml.js index 94c2673b8c5f0..8a889f56176ee 100644 --- a/app/2fa/client/TOTPSaml.js +++ b/app/2fa/client/TOTPSaml.js @@ -1,8 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; -import { Utils2fa } from './lib/2fa'; import '../../meteor-accounts-saml/client/saml_client'; +import { reportError } from '../../../client/lib/2fa/utils'; +import { overrideLoginMethod } from '../../../client/lib/2fa/overrideLoginMethod'; Meteor.loginWithSamlTokenAndTOTP = function(credentialToken, code, callback) { Accounts.callLoginMethod({ @@ -17,7 +18,7 @@ Meteor.loginWithSamlTokenAndTOTP = function(credentialToken, code, callback) { }], userCallback(error) { if (error) { - Utils2fa.reportError(error, callback); + reportError(error, callback); } else { callback && callback(); } @@ -28,5 +29,5 @@ Meteor.loginWithSamlTokenAndTOTP = function(credentialToken, code, callback) { const { loginWithSamlToken } = Meteor; Meteor.loginWithSamlToken = function(options, callback) { - Utils2fa.overrideLoginMethod(loginWithSamlToken, [options], callback, Meteor.loginWithSamlTokenAndTOTP); + overrideLoginMethod(loginWithSamlToken, [options], callback, Meteor.loginWithSamlTokenAndTOTP); }; diff --git a/app/2fa/client/callWithTwoFactorRequired.js b/app/2fa/client/callWithTwoFactorRequired.js deleted file mode 100644 index 61a793e76f8c1..0000000000000 --- a/app/2fa/client/callWithTwoFactorRequired.js +++ /dev/null @@ -1,64 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { SHA256 } from 'meteor/sha'; -import toastr from 'toastr'; - -import { t } from '../../utils/client'; -import { imperativeModal } from '../../../client/lib/imperativeModal'; -import TwoFactorModal from '../../../client/components/TwoFactorModal'; - - -export function process2faReturn({ error, result, originalCallback, onCode, emailOrUsername }) { - if (!error || (error.error !== 'totp-required' && error.errorType !== 'totp-required')) { - return originalCallback(error, result); - } - - const method = error.details && error.details.method; - - if (!emailOrUsername && Meteor.user()) { - emailOrUsername = Meteor.user().username; - } - - imperativeModal.open({ - component: TwoFactorModal, - props: { - method, - onConfirm: (code, method) => { - onCode(method === 'password' ? SHA256(code) : code, method); - imperativeModal.close(); - }, - onClose: () => { - imperativeModal.close(); - originalCallback(new Meteor.Error('totp-canceled')); - }, - emailOrUsername, - }, - }); -} - -const { call } = Meteor; -Meteor.call = function(ddpMethod, ...args) { - let callback = args.pop(); - if (typeof callback !== 'function') { - args.push(callback); - callback = () => {}; - } - - return call(ddpMethod, ...args, function(error, result) { - process2faReturn({ - error, - result, - originalCallback: callback, - onCode: (code, method) => { - call(ddpMethod, ...args, { twoFactorCode: code, twoFactorMethod: method }, (error, result) => { - if (error && error.error === 'totp-invalid') { - error.toastrShowed = true; - toastr.error(t('Invalid_two_factor_code')); - return callback(error); - } - - callback(error, result); - }); - }, - }); - }); -}; diff --git a/app/2fa/client/index.js b/app/2fa/client/index.js deleted file mode 100644 index 24fd7cc729461..0000000000000 --- a/app/2fa/client/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import './callWithTwoFactorRequired'; -import './TOTPPassword'; -import './TOTPOAuth'; -import './TOTPGoogle'; -import './TOTPSaml'; -import './TOTPLDAP'; -import './TOTPCrowd'; diff --git a/app/2fa/client/index.ts b/app/2fa/client/index.ts new file mode 100644 index 0000000000000..1e8f20eb784cb --- /dev/null +++ b/app/2fa/client/index.ts @@ -0,0 +1,7 @@ +import './TOTPPassword'; +import './TOTPOAuth'; +import './TOTPGoogle'; +import './TOTPSaml'; +import './TOTPLDAP'; +import './TOTPCrowd'; +import './overrideMeteorCall'; diff --git a/app/2fa/client/lib/2fa.js b/app/2fa/client/lib/2fa.js deleted file mode 100644 index bef17e6979ec4..0000000000000 --- a/app/2fa/client/lib/2fa.js +++ /dev/null @@ -1,51 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import toastr from 'toastr'; -import { Accounts } from 'meteor/accounts-base'; - -import { t } from '../../../utils/client'; -import { process2faReturn } from '../callWithTwoFactorRequired'; - -export class Utils2fa { - static reportError(error, callback) { - if (callback) { - callback(error); - } else { - throw error; - } - } - - static convertError(err) { - if (err && err instanceof Meteor.Error && err.error === Accounts.LoginCancelledError.numericError) { - return new Accounts.LoginCancelledError(err.reason); - } - - return err; - } - - static overrideLoginMethod(loginMethod, loginArgs, cb, loginMethodTOTP, emailOrUsername) { - loginMethod.apply(this, loginArgs.concat([(error) => { - if (!error || error.error !== 'totp-required') { - return cb(error); - } - - process2faReturn({ - error, - emailOrUsername, - originalCallback: cb, - onCode: (code) => { - loginMethodTOTP && loginMethodTOTP.apply(this, loginArgs.concat([code, (error) => { - if (error) { - console.log(error); - } - if (error && error.error === 'totp-invalid') { - toastr.error(t('Invalid_two_factor_code')); - cb(); - } else { - cb(error); - } - }])); - }, - }); - }])); - } -} diff --git a/app/2fa/client/overrideMeteorCall.ts b/app/2fa/client/overrideMeteorCall.ts new file mode 100644 index 0000000000000..52777095b87df --- /dev/null +++ b/app/2fa/client/overrideMeteorCall.ts @@ -0,0 +1,49 @@ +import { Meteor } from 'meteor/meteor'; + +import { t } from '../../utils/client'; +import { process2faReturn } from '../../../client/lib/2fa/process2faReturn'; +import { isTotpInvalidError } from '../../../client/lib/2fa/utils'; +import { dispatchToastMessage } from '../../../client/lib/toast'; + +const { call } = Meteor; + +type Callback = { + (error: unknown): void; + (error: unknown, result: unknown): void; +}; + +const callWithTotp = (methodName: string, args: unknown[], callback: Callback) => + (twoFactorCode: string, twoFactorMethod: string): unknown => + call(methodName, ...args, { twoFactorCode, twoFactorMethod }, (error: unknown, result: unknown): void => { + if (isTotpInvalidError(error)) { + (error as { toastrShowed?: true }).toastrShowed = true; + dispatchToastMessage({ + type: 'error', + message: t('Invalid_two_factor_code'), + }); + callback(error); + return; + } + + callback(error, result); + }); + +const callWithoutTotp = (methodName: string, args: unknown[], callback: Callback) => + (): unknown => + call(methodName, ...args, (error: unknown, result: unknown): void => { + process2faReturn({ + error, + result, + onCode: callWithTotp(methodName, args, callback), + originalCallback: callback, + emailOrUsername: undefined, + }); + }); + +Meteor.call = function(methodName: string, ...args: unknown[]): unknown { + const callback = args.length > 0 && typeof args[args.length - 1] === 'function' + ? args.pop() as Callback + : (): void => undefined; + + return callWithoutTotp(methodName, args, callback)(); +}; diff --git a/app/2fa/server/functions/resetTOTP.ts b/app/2fa/server/functions/resetTOTP.ts index c41e33102fce8..d7ac072918451 100644 --- a/app/2fa/server/functions/resetTOTP.ts +++ b/app/2fa/server/functions/resetTOTP.ts @@ -60,7 +60,7 @@ export async function resetTOTP(userId: string, notifyUser = false): Promise = { + 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'; + }; +} + +type NotFoundResult = { + statusCode: 403; + body: { + success: false; + error: T | 'Resource not found'; + }; +} + +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>; + readonly request: Request; + 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; + + notFound(msg?: T): NotFoundResult; + + 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 34d34aadf09b1..43241eda28219 100644 --- a/app/api/server/api.js +++ b/app/api/server/api.js @@ -4,24 +4,18 @@ 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 '../../logger'; -import { settings } from '../../settings'; -import { metrics } from '../../metrics'; -import { hasPermission, hasAllPermission } from '../../authorization'; +import { Logger } from '../../../server/lib/logger/Logger'; +import { getRestPayload } from '../../../server/lib/logger/logPayloads'; +import { settings } from '../../settings/server'; +import { metrics } from '../../metrics/server'; +import { hasPermission, hasAllPermission } from '../../authorization/server'; import { getDefaultUserFields } from '../../utils/server/functions/getDefaultUserFields'; import { checkCodeForUser } from '../../2fa/server/code'; -const logger = new Logger('API', {}); - -const { - LOG_REST_PAYLOAD = 'false', - LOG_REST_METHOD_PAYLOADS = 'false', -} = process.env; - -const addPayloadToLog = LOG_REST_PAYLOAD !== 'false' || LOG_REST_METHOD_PAYLOADS !== 'false'; +const logger = new Logger('API'); const rateLimiterDictionary = {}; export const defaultRateLimiterOptions = { @@ -134,8 +128,6 @@ export class APIClass extends Restivus { body: result, }; - logger.debug('Success', result); - return result; } @@ -167,8 +159,6 @@ export class APIClass extends Restivus { body: result, }; - logger.debug('Failure', result); - return result; } @@ -283,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; } @@ -359,10 +352,26 @@ export class APIClass extends Restivus { }); this.requestIp = getRequestIP(this.request); + + const startTime = Date.now(); + + const log = logger.logger.child({ + method: this.request.method, + url: this.request.url, + userId: this.request.headers['x-user-id'], + userAgent: this.request.headers['user-agent'], + length: this.request.headers['content-length'], + host: this.request.headers.host, + referer: this.request.headers.referer, + remoteIP: this.requestIp, + ...getRestPayload(this.request.body), + }); + const objectForRateLimitMatch = { IPAddr: this.requestIp, route: `${ this.request.route }${ this.request.method.toLowerCase() }`, }; + let result; const connection = { @@ -393,29 +402,31 @@ 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)); - } catch (e) { - logger.debug(`${ method } ${ route } threw an error:`, e.stack); + result = DDP._CurrentInvocation.withValue(invocation, () => Promise.await(originalAction.apply(this))) || API.v1.success(); + log.http({ + status: result.statusCode, + responseTime: Date.now() - startTime, + }); + } catch (e) { const apiMethod = { 'error-too-many-requests': 'tooManyRequests', 'error-unauthorized': 'unauthorized', }[e.error] || 'failure'; result = API.v1[apiMethod](typeof e === 'string' ? e : e.message, e.error, process.env.TEST_MODE ? e.stack : undefined, e); + + log.http({ + err: e, + status: result.statusCode, + responseTime: Date.now() - startTime, + }); } finally { delete Accounts._accountData[connection.id]; } - const dateTime = new Date().toISOString(); - logger.info(() => `${ this.requestIp } - ${ this.userId } [${ dateTime }] "${ this.request.method } ${ this.request.url }" ${ result.statusCode } - "${ this.request.headers.referer }" "${ this.request.headers['user-agent'] }" | ${ addPayloadToLog ? JSON.stringify(this.request.body) : '' }`); - - result = result || API.v1.success(); - rocketchatRestApiEnd({ status: result.statusCode, }); @@ -437,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 @@ -734,11 +753,11 @@ const createApis = function _createApis() { createApis(); // register the API to be re-created once the CORS-setting changes. -settings.get(/^(API_Enable_CORS|API_CORS_Origin)$/, () => { +settings.watchMultiple(['API_Enable_CORS', 'API_CORS_Origin'], () => { createApis(); }); -settings.get('Accounts_CustomFields', (key, value) => { +settings.watch('Accounts_CustomFields', (value) => { if (!value) { return API.v1.setLimitedCustomFields([]); } @@ -751,16 +770,17 @@ settings.get('Accounts_CustomFields', (key, value) => { } }); -settings.get('API_Enable_Rate_Limiter_Limit_Time_Default', (key, value) => { +settings.watch('API_Enable_Rate_Limiter_Limit_Time_Default', (value) => { defaultRateLimiterOptions.intervalTimeInMS = value; API.v1.reloadRoutesToRefreshRateLimiter(); }); -settings.get('API_Enable_Rate_Limiter_Limit_Calls_Default', (key, value) => { +settings.watch('API_Enable_Rate_Limiter_Limit_Calls_Default', (value) => { defaultRateLimiterOptions.numRequestsAllowed = value; API.v1.reloadRoutesToRefreshRateLimiter(); }); -settings.get('Prometheus_API_User_Agent', (key, value) => { + +settings.watch('Prometheus_API_User_Agent', (value) => { prometheusAPIUserAgent = value; }); diff --git a/app/api/server/default/info.js b/app/api/server/default/info.js index 861d9dfb36aca..62ef49023f008 100644 --- a/app/api/server/default/info.js +++ b/app/api/server/default/info.js @@ -1,20 +1,11 @@ -import { hasRole } from '../../../authorization'; -import { Info } from '../../../utils'; import { API } from '../api'; +import { getServerInfo } from '../lib/getServerInfo'; API.default.addRoute('info', { authRequired: false }, { get() { const user = this.getLoggedInUser(); - if (user && hasRole(user._id, 'admin')) { - return API.v1.success({ - info: Info, - }); - } - - return API.v1.success({ - version: Info.version, - }); + return API.v1.success(Promise.await(getServerInfo(user?._id))); }, }); diff --git a/app/api/server/helpers/deprecationWarning.js b/app/api/server/helpers/deprecationWarning.js deleted file mode 100644 index fdcc98f4b1d2c..0000000000000 --- a/app/api/server/helpers/deprecationWarning.js +++ /dev/null @@ -1,14 +0,0 @@ -import { API } from '../api'; - -API.helperMethods.set('deprecationWarning', function _deprecationWarning({ endpoint, versionWillBeRemoved, response }) { - const warningMessage = `The endpoint "${ endpoint }" is deprecated and will be removed after version ${ versionWillBeRemoved }`; - console.warn(warningMessage); - if (process.env.NODE_ENV === 'development') { - return { - warning: warningMessage, - ...response, - }; - } - - return response; -}); diff --git a/app/api/server/helpers/deprecationWarning.ts b/app/api/server/helpers/deprecationWarning.ts new file mode 100644 index 0000000000000..bfee0827733d4 --- /dev/null +++ b/app/api/server/helpers/deprecationWarning.ts @@ -0,0 +1,17 @@ +import { API } from '../api'; +import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; + +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') { + return { + warning: warningMessage, + ...response, + }; + } + + 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 93a19b2cbf9fc..259f79a1191a3 100644 --- a/app/api/server/helpers/getPaginationItems.js +++ b/app/api/server/helpers/getPaginationItems.js @@ -1,7 +1,7 @@ // If the count query param is higher than the "API_Upper_Count_Limit" setting, then we limit that // If the count query param isn't defined, then we set it to the "API_Default_Count" setting // If the count is zero, then that means unlimited and is only allowed if the setting "API_Allow_Infinite_Count" is true -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { API } from '../api'; API.helperMethods.set('getPaginationItems', function _getPaginationItems() { @@ -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/helpers/getUserInfo.js b/app/api/server/helpers/getUserInfo.js index 2d2daee1af344..5353a78d04204 100644 --- a/app/api/server/helpers/getUserInfo.js +++ b/app/api/server/helpers/getUserInfo.js @@ -11,10 +11,10 @@ API.helperMethods.set('getUserInfo', function _getUserInfo(me) { }; const getUserPreferences = () => { const defaultUserSettingPrefix = 'Accounts_Default_User_Preferences_'; - const allDefaultUserSettings = settings.get(new RegExp(`^${ defaultUserSettingPrefix }.*$`)); + const allDefaultUserSettings = settings.getByRegexp(new RegExp(`^${ defaultUserSettingPrefix }.*$`)); - return allDefaultUserSettings.reduce((accumulator, setting) => { - const settingWithoutPrefix = setting.key.replace(defaultUserSettingPrefix, ' ').trim(); + return allDefaultUserSettings.reduce((accumulator, [key]) => { + const settingWithoutPrefix = key.replace(defaultUserSettingPrefix, ' ').trim(); accumulator[settingWithoutPrefix] = getUserPreference(me, settingWithoutPrefix); return accumulator; }, {}); diff --git a/app/api/server/helpers/isWidget.ts b/app/api/server/helpers/isWidget.ts new file mode 100644 index 0000000000000..203ab081925d0 --- /dev/null +++ b/app/api/server/helpers/isWidget.ts @@ -0,0 +1,13 @@ +import { parse } from 'cookie'; + +import { API } from '../api'; + +(API as any).helperMethods.set('isWidget', function _isWidget() { + // @ts-expect-error + const { headers } = this.request; + + const { rc_room_type: roomType, rc_is_widget: isWidget } = parse(headers.cookie || ''); + + const isLivechatRoom = roomType && roomType === 'l'; + return !!(isLivechatRoom && isWidget === 't'); +}); diff --git a/app/api/server/index.js b/app/api/server/index.js index b5a5c0d6f1dc4..48d0caa9d6fcd 100644 --- a/app/api/server/index.js +++ b/app/api/server/index.js @@ -9,6 +9,7 @@ import './helpers/insertUserObject'; import './helpers/isUserFromParams'; import './helpers/parseJsonQuery'; import './helpers/requestParams'; +import './helpers/isWidget'; import './default/info'; import './v1/assets'; import './v1/channels'; @@ -23,6 +24,7 @@ import './v1/im'; import './v1/integrations'; import './v1/invites'; import './v1/import'; +import './v1/ldap'; import './v1/misc'; import './v1/permissions'; import './v1/push'; diff --git a/app/api/server/lib/getServerInfo.ts b/app/api/server/lib/getServerInfo.ts new file mode 100644 index 0000000000000..f40be3cde7b8c --- /dev/null +++ b/app/api/server/lib/getServerInfo.ts @@ -0,0 +1,22 @@ + +import { Info } from '../../../utils/server'; +import { hasAnyRoleAsync } from '../../../authorization/server/functions/hasRole'; + +type ServerInfo = { + info: Info; +} | { + version: string | undefined; +}; + +const removePatchInfo = (version: string): string => version.replace(/(\d+\.\d+).*/, '$1'); + +export async function getServerInfo(userId?: string): Promise { + if (userId && await hasAnyRoleAsync(userId, ['admin'])) { + return { + info: Info, + }; + } + return { + version: removePatchInfo(Info.version), + }; +} diff --git a/app/api/server/lib/integrations.js b/app/api/server/lib/integrations.js deleted file mode 100644 index 55db33a636a5e..0000000000000 --- a/app/api/server/lib/integrations.js +++ /dev/null @@ -1,27 +0,0 @@ -import { Integrations } from '../../../models/server/raw'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; - -const hasIntegrationsPermission = async (userId, integration) => { - const type = integration.type === 'webhook-incoming' ? 'incoming' : 'outgoing'; - - if (await hasPermissionAsync(userId, `manage-${ type }-integrations`)) { - return true; - } - - if (userId === integration._createdBy._id) { - return hasPermissionAsync(userId, `manage-own-${ type }-integrations`); - } - - return false; -}; - -export const findOneIntegration = async ({ userId, integrationId, createdBy }) => { - const integration = await Integrations.findOneByIdAndCreatedByIfExists({ _id: integrationId, createdBy }); - if (!integration) { - throw new Error('The integration does not exists.'); - } - if (!await hasIntegrationsPermission(userId, integration)) { - throw new Error('not-authorized'); - } - return integration; -}; diff --git a/app/api/server/lib/integrations.ts b/app/api/server/lib/integrations.ts new file mode 100644 index 0000000000000..ef5cab57ed942 --- /dev/null +++ b/app/api/server/lib/integrations.ts @@ -0,0 +1,37 @@ +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: string, integration: IIntegration): Promise => { + const type = integration.type === 'webhook-incoming' ? 'incoming' : 'outgoing'; + + if (await hasPermissionAsync(userId, `manage-${ type }-integrations`)) { + return true; + } + + if (userId === integration._createdBy._id) { + return hasPermissionAsync(userId, `manage-own-${ type }-integrations`); + } + + return false; +}; + +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.'); + } + if (!await hasIntegrationsPermission(userId, integration)) { + throw new Error('not-authorized'); + } + return integration; +}; diff --git a/app/api/server/lib/rooms.js b/app/api/server/lib/rooms.js index 0b908620f18a9..a841973e02dbc 100644 --- a/app/api/server/lib/rooms.js +++ b/app/api/server/lib/rooms.js @@ -1,6 +1,6 @@ -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'; +import { Subscriptions } from '../../../models/server'; export async function findAdminRooms({ uid, filter, types = [], pagination: { offset, count, sort } }) { if (!await hasPermissionAsync(uid, 'view-room-administration')) { @@ -119,6 +119,60 @@ 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() + .map((item) => item.rid); + + const options = { + fields: { + _id: 1, + fname: 1, + name: 1, + t: 1, + avatarETag: 1, + }, + sort: sort || { name: 1 }, + skip: offset, + limit: count, + }; + + const cursor = await Rooms.findRoomsWithoutDiscussionsByRoomIds(selector.name, userRoomsIds, options); + + const total = await cursor.count(); + const rooms = await cursor.toArray(); + + return { + items: rooms, + total, + }; +} + export async function findRoomsAvailableForTeams({ uid, name }) { const options = { fields: { diff --git a/app/api/server/lib/webdav.js b/app/api/server/lib/webdav.js deleted file mode 100644 index cf5a3c8ea8f1d..0000000000000 --- 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 0000000000000..fe2f17185bb6d --- /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/settings.js b/app/api/server/settings.js deleted file mode 100644 index 7a33ff4dce9d3..0000000000000 --- a/app/api/server/settings.js +++ /dev/null @@ -1,20 +0,0 @@ -import { settings } from '../../settings'; - -settings.addGroup('General', function() { - this.section('REST API', function() { - this.add('API_Upper_Count_Limit', 100, { type: 'int', public: false }); - this.add('API_Default_Count', 50, { type: 'int', public: false }); - this.add('API_Allow_Infinite_Count', true, { type: 'boolean', public: false }); - this.add('API_Enable_Direct_Message_History_EndPoint', false, { type: 'boolean', public: false }); - this.add('API_Enable_Shields', true, { type: 'boolean', public: false }); - this.add('API_Shield_Types', '*', { type: 'string', public: false, enableQuery: { _id: 'API_Enable_Shields', value: true } }); - this.add('API_Shield_user_require_auth', false, { type: 'boolean', public: false, enableQuery: { _id: 'API_Enable_Shields', value: true } }); - this.add('API_Enable_CORS', false, { type: 'boolean', public: false }); - this.add('API_CORS_Origin', '*', { type: 'string', public: false, enableQuery: { _id: 'API_Enable_CORS', value: true } }); - - this.add('API_Use_REST_For_DDP_Calls', true, { - type: 'boolean', - public: true, - }); - }); -}); diff --git a/app/api/server/settings.ts b/app/api/server/settings.ts new file mode 100644 index 0000000000000..e042edee99a00 --- /dev/null +++ b/app/api/server/settings.ts @@ -0,0 +1,20 @@ +import { settingsRegistry } from '../../settings/server'; + +settingsRegistry.addGroup('General', function() { + this.section('REST API', function() { + this.add('API_Upper_Count_Limit', 100, { type: 'int', public: false }); + this.add('API_Default_Count', 50, { type: 'int', public: false }); + this.add('API_Allow_Infinite_Count', true, { type: 'boolean', public: false }); + this.add('API_Enable_Direct_Message_History_EndPoint', false, { type: 'boolean', public: false }); + this.add('API_Enable_Shields', true, { type: 'boolean', public: false }); + this.add('API_Shield_Types', '*', { type: 'string', public: false, enableQuery: { _id: 'API_Enable_Shields', value: true } }); + this.add('API_Shield_user_require_auth', false, { type: 'boolean', public: false, enableQuery: { _id: 'API_Enable_Shields', value: true } }); + this.add('API_Enable_CORS', false, { type: 'boolean', public: false }); + this.add('API_CORS_Origin', '*', { type: 'string', public: false, enableQuery: { _id: 'API_Enable_CORS', value: true } }); + + this.add('API_Use_REST_For_DDP_Calls', true, { + type: 'boolean', + public: true, + }); + }); +}); diff --git a/app/api/server/v1/assets.js b/app/api/server/v1/assets.js index 108f9649ffe64..09739ad66074e 100644 --- a/app/api/server/v1/assets.js +++ b/app/api/server/v1/assets.js @@ -1,42 +1,33 @@ import { Meteor } from 'meteor/meteor'; -import Busboy from 'busboy'; import { RocketChatAssets } from '../../../assets/server'; import { API } from '../api'; +import { getUploadFormData } from '../lib/getUploadFormData'; API.v1.addRoute('assets.setAsset', { authRequired: true }, { post() { - const busboy = new Busboy({ headers: this.request.headers }); - const fields = {}; - let asset = {}; - - Meteor.wrapAsync((callback) => { - busboy.on('field', (fieldname, value) => { fields[fieldname] = value; }); - busboy.on('file', Meteor.bindEnvironment((fieldname, file, filename, encoding, mimetype) => { - const isValidAsset = Object.keys(RocketChatAssets.assets).includes(fieldname); - if (!isValidAsset) { - callback(new Meteor.Error('error-invalid-asset', 'Invalid asset')); - } - const assetData = []; - file.on('data', Meteor.bindEnvironment((data) => { - assetData.push(data); - })); - - file.on('end', Meteor.bindEnvironment(() => { - asset = { - buffer: Buffer.concat(assetData), - name: fieldname, - mimetype, - }; - })); - })); - busboy.on('finish', () => callback()); - this.request.pipe(busboy); - })(); - Meteor.runAsUser(this.userId, () => Meteor.call('setAsset', asset.buffer, asset.mimetype, asset.name)); - if (fields.refreshAllClients) { - Meteor.runAsUser(this.userId, () => Meteor.call('refreshClients')); + const { refreshAllClients, ...files } = Promise.await(getUploadFormData({ + request: this.request, + })); + + const assetsKeys = Object.keys(RocketChatAssets.assets); + + const [assetName] = Object.keys(files); + + const isValidAsset = assetsKeys.includes(assetName); + if (!isValidAsset) { + throw new Meteor.Error('error-invalid-asset', 'Invalid asset'); } + + Meteor.runAsUser(this.userId, () => { + const { [assetName]: asset } = files; + + Meteor.call('setAsset', asset.fileBuffer, asset.mimetype, assetName); + if (refreshAllClients) { + Meteor.call('refreshClients'); + } + }); + return API.v1.success(); }, }); @@ -48,10 +39,12 @@ API.v1.addRoute('assets.unsetAsset', { authRequired: true }, { if (!isValidAsset) { throw new Meteor.Error('error-invalid-asset', 'Invalid asset'); } - Meteor.runAsUser(this.userId, () => Meteor.call('unsetAsset', assetName)); - if (refreshAllClients) { - Meteor.runAsUser(this.userId, () => Meteor.call('refreshClients')); - } + Meteor.runAsUser(this.userId, () => { + Meteor.call('unsetAsset', assetName); + if (refreshAllClients) { + Meteor.call('refreshClients'); + } + }); return API.v1.success(); }, }); diff --git a/app/api/server/v1/autotranslate.js b/app/api/server/v1/autotranslate.js index 6a65c21adc732..7bb9509cdada7 100644 --- a/app/api/server/v1/autotranslate.js +++ b/app/api/server/v1/autotranslate.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { API } from '../api'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { Messages } from '../../../models/server'; API.v1.addRoute('autotranslate.getSupportedLanguages', { authRequired: true }, { diff --git a/app/api/server/v1/banners.ts b/app/api/server/v1/banners.ts index 07a8077d72463..4e740cdf2c901 100644 --- a/app/api/server/v1/banners.ts +++ b/app/api/server/v1/banners.ts @@ -1,50 +1,227 @@ -import { Promise } from 'meteor/promise'; -import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { API } from '../api'; import { Banner } from '../../../../server/sdk'; import { BannerPlatform } from '../../../../definition/IBanner'; -API.v1.addRoute('banners.getNew', { authRequired: true }, { - get() { +/** + * @deprecated + * @openapi + * /api/v1/banners.getNew: + * get: + * description: Gets the banners to be shown to the authenticated user + * deprecated: true + * security: + * $ref: '#/security/authenticated' + * parameters: + * - name: platform + * in: query + * description: The platform rendering the banner + * required: true + * schema: + * type: string + * enum: [web, mobile] + * example: web + * - name: bid + * in: query + * description: The id of a single banner + * required: false + * schema: + * type: string + * example: ByehQjC44FwMeiLbX + * responses: + * 200: + * description: The banners matching the criteria + * content: + * application/json: + * schema: + * allOf: + * - $ref: '#/components/schemas/ApiSuccessV1' + * - type: object + * properties: + * banners: + * type: array + * items: + * $ref: '#/components/schemas/IBanner' + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ +API.v1.addRoute('banners.getNew', { authRequired: true }, { // deprecated + 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 = await Banner.getBannersForUser(this.userId, platform, bannerId ?? undefined); - const banners = Promise.await(Banner.getNewBannersForUser(this.userId, platform, bannerId)); + return API.v1.success({ banners }); + }, +}); + +/** + * @openapi + * /api/v1/banners/{id}: + * get: + * description: Gets the banner to be shown to the authenticated user + * security: + * $ref: '#/security/authenticated' + * parameters: + * - name: platform + * in: query + * description: The platform rendering the banner + * required: true + * schema: + * type: string + * enum: [web, mobile] + * example: web + * - name: id + * in: path + * description: The id of the banner + * required: true + * schema: + * type: string + * example: ByehQjC44FwMeiLbX + * responses: + * 200: + * description: | + * A collection with a single banner matching the criteria; an empty + * collection otherwise + * content: + * application/json: + * schema: + * allOf: + * - $ref: '#/components/schemas/ApiSuccessV1' + * - type: object + * properties: + * banners: + * type: array + * items: + * $ref: '#/components/schemas/IBanner' + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ +API.v1.addRoute('banners/:id', { authRequired: true }, { // TODO: move to users/:id/banners + async get() { + check(this.urlParams, Match.ObjectIncluding({ + 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; + const { id } = this.urlParams; + + const banners = await Banner.getBannersForUser(this.userId, platform, id); return API.v1.success({ banners }); }, }); +/** + * @openapi + * /api/v1/banners: + * get: + * description: Gets the banners to be shown to the authenticated user + * security: + * $ref: '#/security/authenticated' + * parameters: + * - name: platform + * in: query + * description: The platform rendering the banner + * required: true + * schema: + * type: string + * enum: [web, mobile] + * example: web + * responses: + * 200: + * description: The banners matching the criteria + * content: + * application/json: + * schema: + * allOf: + * - $ref: '#/components/schemas/ApiSuccessV1' + * - type: object + * properties: + * banners: + * type: array + * items: + * $ref: '#/components/schemas/IBanner' + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ +API.v1.addRoute('banners', { authRequired: true }, { + async get() { + check(this.queryParams, Match.ObjectIncluding({ + platform: Match.OneOf(...Object.values(BannerPlatform)), + })); + + const { platform } = this.queryParams; + + const banners = await Banner.getBannersForUser(this.userId, platform); + + return API.v1.success({ banners }); + }, +}); + +/** + * @openapi + * /api/v1/banners.dismiss: + * post: + * description: Dismisses a banner + * security: + * $ref: '#/security/authenticated' + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * bannerId: + * type: string + * example: | + * { + * "bannerId": "ByehQjC44FwMeiLbX" + * } + * responses: + * 200: + * description: The banners matching the criteria + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiSuccessV1' + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $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.'); - } - - try { - Promise.await(Banner.dismiss(this.userId, bannerId)); - return API.v1.success(); - } catch (e) { - return API.v1.failure(); - } + 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 8d360a5de5056..3e0139ec80b05 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'; @@ -285,28 +286,28 @@ API.v1.addRoute('channels.files', { authRequired: true }, { return file; }; - Meteor.runAsUser(this.userId, () => { - Meteor.call('canAccessRoom', findResult._id, this.userId); - }); + if (!canAccessRoom(findResult, { _id: this.userId })) { + return API.v1.unauthorized(); + } const { offset, count } = this.getPaginationItems(); const { sort, fields, query } = this.parseJsonQuery(); 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 11680765d1f87..fa3e917665fc1 100644 --- a/app/api/server/v1/chat.js +++ b/app/api/server/v1/chat.js @@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { Messages } from '../../../models'; -import { canAccessRoom, hasPermission } from '../../../authorization'; +import { canAccessRoom, hasPermission } from '../../../authorization/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { processWebhookMessage } from '../../../lib/server'; import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; @@ -12,7 +12,7 @@ import { API } from '../api'; import Rooms from '../../../models/server/models/Rooms'; import Users from '../../../models/server/models/Users'; import Subscriptions from '../../../models/server/models/Subscriptions'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { findMentionedMessages, findStarredMessages, findSnippetedMessageById, findSnippetedMessages, findDiscussionsFromRoom } from '../lib/messages'; API.v1.addRoute('chat.delete', { authRequired: true }, { @@ -404,12 +404,12 @@ API.v1.addRoute('chat.getPinnedMessages', { authRequired: true }, { if (!roomId) { throw new Meteor.Error('error-roomId-param-not-provided', 'The required "roomId" query param is missing.'); } - const room = Meteor.call('canAccessRoom', roomId, this.userId); - if (!room) { + + if (!canAccessRoom({ _id: roomId }, { _id: this.userId })) { throw new Meteor.Error('error-not-allowed', 'Not allowed'); } - const cursor = Messages.findPinnedByRoom(room._id, { + const cursor = Messages.findPinnedByRoom(roomId, { skip: offset, limit: count, }); @@ -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/cloud.js b/app/api/server/v1/cloud.js index 7b1b3e729121f..4b7a33df18dbc 100644 --- a/app/api/server/v1/cloud.js +++ b/app/api/server/v1/cloud.js @@ -1,7 +1,7 @@ import { check } from 'meteor/check'; import { API } from '../api'; -import { hasRole } from '../../../authorization'; +import { hasRole } from '../../../authorization/server'; import { saveRegistrationData } from '../../../cloud/server/functions/saveRegistrationData'; import { retrieveRegistrationStatus } from '../../../cloud/server/functions/retrieveRegistrationStatus'; diff --git a/app/api/server/v1/commands.js b/app/api/server/v1/commands.js index 3a46a677e4f99..a9f6c290d8dd4 100644 --- a/app/api/server/v1/commands.js +++ b/app/api/server/v1/commands.js @@ -1,8 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; +import objectPath from 'object-path'; -import { slashCommands } from '../../../utils'; -import { Rooms, Messages } from '../../../models'; +import { slashCommands } from '../../../utils/server'; +import { Messages } from '../../../models/server'; +import { canAccessRoom } from '../../../authorization/server'; import { API } from '../api'; API.v1.addRoute('commands.get', { authRequired: true }, { @@ -23,6 +25,116 @@ API.v1.addRoute('commands.get', { authRequired: true }, { }, }); +// TODO: replace with something like client/lib/minimongo +const processQueryOptionsOnResult = (result, options = {}) => { + if (result === undefined || result === null) { + return undefined; + } + + if (Array.isArray(result)) { + if (options.sort) { + result = result.sort((a, b) => { + let r = 0; + for (const field in options.sort) { + if (options.sort.hasOwnProperty(field)) { + const direction = options.sort[field]; + let valueA; + let valueB; + if (field.indexOf('.') > -1) { + valueA = objectPath.get(a, field); + valueB = objectPath.get(b, field); + } else { + valueA = a[field]; + valueB = b[field]; + } + if (valueA > valueB) { + r = direction; + break; + } + if (valueA < valueB) { + r = -direction; + break; + } + } + } + return r; + }); + } + + if (typeof options.skip === 'number') { + result.splice(0, options.skip); + } + + if (typeof options.limit === 'number' && options.limit !== 0) { + result.splice(options.limit); + } + } + + if (!options.fields) { + options.fields = {}; + } + + const fieldsToRemove = []; + const fieldsToGet = []; + + for (const field in options.fields) { + if (options.fields.hasOwnProperty(field)) { + if (options.fields[field] === 0) { + fieldsToRemove.push(field); + } else if (options.fields[field] === 1) { + fieldsToGet.push(field); + } + } + } + + if (fieldsToRemove.length > 0 && fieldsToGet.length > 0) { + console.warn('Can\'t mix remove and get fields'); + fieldsToRemove.splice(0, fieldsToRemove.length); + } + + if (fieldsToGet.length > 0 && fieldsToGet.indexOf('_id') === -1) { + fieldsToGet.push('_id'); + } + + const pickFields = (obj, fields) => { + const picked = {}; + fields.forEach((field) => { + if (field.indexOf('.') !== -1) { + objectPath.set(picked, field, objectPath.get(obj, field)); + } else { + picked[field] = obj[field]; + } + }); + return picked; + }; + + if (fieldsToRemove.length > 0 || fieldsToGet.length > 0) { + if (Array.isArray(result)) { + result = result.map((record) => { + if (fieldsToRemove.length > 0) { + return Object.fromEntries(Object.entries(record).filter(([key]) => !fieldsToRemove.includes(key))); + } + + if (fieldsToGet.length > 0) { + return pickFields(record, fieldsToGet); + } + + return null; + }); + } else { + if (fieldsToRemove.length > 0) { + return Object.fromEntries(Object.entries(result).filter(([key]) => !fieldsToRemove.includes(key))); + } + + if (fieldsToGet.length > 0) { + return pickFields(result, fieldsToGet); + } + } + } + + return result; +}; + API.v1.addRoute('commands.list', { authRequired: true }, { get() { const { offset, count } = this.getPaginationItems(); @@ -35,7 +147,7 @@ API.v1.addRoute('commands.list', { authRequired: true }, { } const totalCount = commands.length; - commands = Rooms.processQueryOptionsOnResult(commands, { + commands = processQueryOptionsOnResult(commands, { sort: sort || { name: 1 }, skip: offset, limit: count, @@ -78,8 +190,9 @@ API.v1.addRoute('commands.run', { authRequired: true }, { return API.v1.failure('The command provided does not exist (or is disabled).'); } - // This will throw an error if they can't or the room is invalid - Meteor.call('canAccessRoom', body.roomId, user._id); + if (!canAccessRoom({ _id: body.roomId }, user)) { + return API.v1.unauthorized(); + } const params = body.params ? body.params : ''; const message = { @@ -127,8 +240,9 @@ API.v1.addRoute('commands.preview', { authRequired: true }, { return API.v1.failure('The command provided does not exist (or is disabled).'); } - // This will throw an error if they can't or the room is invalid - Meteor.call('canAccessRoom', query.roomId, user._id); + if (!canAccessRoom({ _id: query.roomId }, user)) { + return API.v1.unauthorized(); + } const params = query.params ? query.params : ''; @@ -177,8 +291,9 @@ API.v1.addRoute('commands.preview', { authRequired: true }, { return API.v1.failure('The command provided does not exist (or is disabled).'); } - // This will throw an error if they can't or the room is invalid - Meteor.call('canAccessRoom', body.roomId, user._id); + if (!canAccessRoom({ _id: body.roomId }, user)) { + return API.v1.unauthorized(); + } const params = body.params ? body.params : ''; const message = { diff --git a/app/api/server/v1/dns.ts b/app/api/server/v1/dns.ts index 6ebda79036b02..a0b0fa5788e6f 100644 --- a/app/api/server/v1/dns.ts +++ b/app/api/server/v1/dns.ts @@ -4,8 +4,51 @@ import { Match, check } from 'meteor/check'; import { API } from '../api'; import { resolveSRV, resolveTXT } from '../../../federation/server/functions/resolveDNS'; +/** + * @openapi + * /api/v1/dns.resolve.srv: + * get: + * description: Resolves DNS service records (SRV records) for a hostname + * security: + * $ref: '#/security/authenticated' + * parameters: + * - name: url + * in: query + * description: The hostname + * required: true + * schema: + * type: string + * example: open.rocket.chat + * responses: + * 200: + * description: The resolved records + * content: + * application/json: + * schema: + * allOf: + * - $ref: '#/components/schemas/ApiSuccessV1' + * - type: object + * properties: + * resolved: + * type: object + * properties: + * target: + * type: string + * priority: + * type: number + * weight: + * type: number + * port: + * type: number + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ API.v1.addRoute('dns.resolve.srv', { authRequired: true }, { - get() { + async get() { check(this.queryParams, Match.ObjectIncluding({ url: String, })); @@ -15,14 +58,48 @@ 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 }); }, }); +/** + * @openapi + * /api/v1/dns.resolve.txt: + * get: + * description: Resolves DNS text records (TXT records) for a hostname + * security: + * $ref: '#/security/authenticated' + * parameters: + * - name: url + * in: query + * description: The hostname + * required: true + * schema: + * type: string + * example: open.rocket.chat + * responses: + * 200: + * description: The resolved records + * content: + * application/json: + * schema: + * allOf: + * - $ref: '#/components/schemas/ApiSuccessV1' + * - type: object + * properties: + * resolved: + * type: string + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ API.v1.addRoute('dns.resolve.txt', { authRequired: true }, { - post() { + async post() { check(this.queryParams, Match.ObjectIncluding({ url: String, })); @@ -32,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/e2e.js b/app/api/server/v1/e2e.js index 9329378a67004..3723ce41d7cff 100644 --- a/app/api/server/v1/e2e.js +++ b/app/api/server/v1/e2e.js @@ -22,6 +22,37 @@ API.v1.addRoute('e2e.getUsersOfRoomWithoutKey', { authRequired: true }, { }, }); +/** + * @openapi + * /api/v1/e2e.setRoomKeyID: + * post: + * description: Sets the end-to-end encryption key ID for a room + * security: + * - autenticated: {} + * requestBody: + * description: A tuple containing the room ID and the key ID + * content: + * application/json: + * schema: + * type: object + * properties: + * rid: + * type: string + * keyID: + * type: string + * responses: + * 200: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiSuccessV1' + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ API.v1.addRoute('e2e.setRoomKeyID', { authRequired: true }, { post() { const { rid, keyID } = this.bodyParams; @@ -34,6 +65,37 @@ API.v1.addRoute('e2e.setRoomKeyID', { authRequired: true }, { }, }); +/** + * @openapi + * /api/v1/e2e.setUserPublicAndPrivateKeys: + * post: + * description: Sets the end-to-end encryption keys for the authenticated user + * security: + * - autenticated: {} + * requestBody: + * description: A tuple containing the public and the private keys + * content: + * application/json: + * schema: + * type: object + * properties: + * public_key: + * type: string + * private_key: + * type: string + * responses: + * 200: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiSuccessV1' + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ API.v1.addRoute('e2e.setUserPublicAndPrivateKeys', { authRequired: true }, { post() { const { public_key, private_key } = this.bodyParams; @@ -49,6 +111,39 @@ API.v1.addRoute('e2e.setUserPublicAndPrivateKeys', { authRequired: true }, { }, }); +/** + * @openapi + * /api/v1/e2e.updateGroupKey: + * post: + * description: Updates the end-to-end encryption key for a user on a room + * security: + * - autenticated: {} + * requestBody: + * description: A tuple containing the user ID, the room ID, and the key + * content: + * application/json: + * schema: + * type: object + * properties: + * uid: + * type: string + * rid: + * type: string + * key: + * type: string + * responses: + * 200: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiSuccessV1' + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ API.v1.addRoute('e2e.updateGroupKey', { authRequired: true }, { post() { const { uid, rid, key } = this.bodyParams; diff --git a/app/api/server/v1/email-inbox.js b/app/api/server/v1/email-inbox.js index e7452fc5ffe1e..61368a2d0a8ab 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 d5f489b9798e6..092e41c1de973 100644 --- a/app/api/server/v1/emoji-custom.js +++ b/app/api/server/v1/emoji-custom.js @@ -1,30 +1,11 @@ import { Meteor } from 'meteor/meteor'; -import Busboy from 'busboy'; -import { EmojiCustom } from '../../../models'; +import { EmojiCustom } from '../../../models/server/raw'; import { API } from '../api'; +import { getUploadFormData } from '../lib/getUploadFormData'; import { findEmojisCustom } from '../lib/emoji-custom'; import { Media } from '../../../../server/sdk'; -// DEPRECATED -// Will be removed after v3.0.0 -API.v1.addRoute('emoji-custom', { authRequired: true }, { - get() { - const warningMessage = 'The endpoint "emoji-custom" is deprecated and will be removed after version v3.0.0'; - console.warn(warningMessage); - const { query } = this.parseJsonQuery(); - const emojis = Meteor.call('listEmojiCustom', query); - - return API.v1.success(this.deprecationWarning({ - endpoint: 'emoji-custom', - versionWillBeRemoved: '3.0.0', - response: { - emojis, - }, - })); - }, -}); - API.v1.addRoute('emoji-custom.list', { authRequired: true }, { get() { const { query } = this.parseJsonQuery(); @@ -38,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: [], }, }); @@ -71,106 +52,69 @@ API.v1.addRoute('emoji-custom.all', { authRequired: true }, { API.v1.addRoute('emoji-custom.create', { authRequired: true }, { post() { + const { emoji, ...fields } = Promise.await(getUploadFormData({ + request: this.request, + })); + + if (!emoji) { + throw new Meteor.Error('invalid-field'); + } + + const isUploadable = Promise.await(Media.isImage(emoji.fileBuffer)); + if (!isUploadable) { + throw new Meteor.Error('emoji-is-not-image', 'Emoji file provided cannot be uploaded since it\'s not an image'); + } + + const [, extension] = emoji.mimetype.split('/'); + fields.extension = extension; + + fields.newFile = true; + fields.aliases = fields.aliases || ''; + Meteor.runAsUser(this.userId, () => { - const fields = {}; - const busboy = new Busboy({ headers: this.request.headers }); - const emojiData = []; - let emojiMimetype = ''; - - Meteor.wrapAsync((callback) => { - busboy.on('file', Meteor.bindEnvironment((fieldname, file, filename, encoding, mimetype) => { - if (fieldname !== 'emoji') { - return callback(new Meteor.Error('invalid-field')); - } - - file.on('data', Meteor.bindEnvironment((data) => emojiData.push(data))); - - file.on('end', Meteor.bindEnvironment(() => { - const extension = mimetype.split('/')[1]; - emojiMimetype = mimetype; - fields.extension = extension; - })); - })); - busboy.on('field', (fieldname, val) => { - fields[fieldname] = val; - }); - busboy.on('finish', Meteor.bindEnvironment(() => { - fields.newFile = true; - fields.aliases = fields.aliases || ''; - try { - const emojiBuffer = Buffer.concat(emojiData); - const isUploadable = Promise.await(Media.isImage(emojiBuffer)); - if (!isUploadable) { - throw new Meteor.Error('emoji-is-not-image', 'Emoji file provided cannot be uploaded since it\'s not an image'); - } - - Meteor.call('insertOrUpdateEmoji', fields); - Meteor.call('uploadEmojiCustom', emojiBuffer, emojiMimetype, fields); - callback(); - } catch (error) { - return callback(error); - } - })); - this.request.pipe(busboy); - })(); + Meteor.call('insertOrUpdateEmoji', fields); + Meteor.call('uploadEmojiCustom', emoji.fileBuffer, emoji.mimetype, fields); }); }, }); API.v1.addRoute('emoji-custom.update', { authRequired: true }, { post() { + const { emoji, ...fields } = Promise.await(getUploadFormData({ + request: this.request, + })); + + if (!fields._id) { + throw new Meteor.Error('The required "_id" query param is missing.'); + } + + const emojiToUpdate = Promise.await(EmojiCustom.findOneById(fields._id)); + if (!emojiToUpdate) { + throw new Meteor.Error('Emoji not found.'); + } + + fields.previousName = emojiToUpdate.name; + fields.previousExtension = emojiToUpdate.extension; + fields.aliases = fields.aliases || ''; + fields.newFile = Boolean(emoji?.fileBuffer.length); + + if (fields.newFile) { + const isUploadable = Promise.await(Media.isImage(emoji.fileBuffer)); + if (!isUploadable) { + throw new Meteor.Error('emoji-is-not-image', 'Emoji file provided cannot be uploaded since it\'s not an image'); + } + + const [, extension] = emoji.mimetype.split('/'); + fields.extension = extension; + } else { + fields.extension = emojiToUpdate.extension; + } + Meteor.runAsUser(this.userId, () => { - const fields = {}; - const busboy = new Busboy({ headers: this.request.headers }); - const emojiData = []; - let emojiMimetype = ''; - - Meteor.wrapAsync((callback) => { - busboy.on('file', Meteor.bindEnvironment((fieldname, file, filename, encoding, mimetype) => { - if (fieldname !== 'emoji') { - return callback(new Meteor.Error('invalid-field')); - } - file.on('data', Meteor.bindEnvironment((data) => emojiData.push(data))); - - file.on('end', Meteor.bindEnvironment(() => { - const extension = mimetype.split('/')[1]; - emojiMimetype = mimetype; - fields.extension = extension; - })); - })); - busboy.on('field', (fieldname, val) => { - fields[fieldname] = val; - }); - busboy.on('finish', Meteor.bindEnvironment(() => { - try { - if (!fields._id) { - return callback(new Meteor.Error('The required "_id" query param is missing.')); - } - const emojiToUpdate = EmojiCustom.findOneById(fields._id); - if (!emojiToUpdate) { - return callback(new Meteor.Error('Emoji not found.')); - } - fields.previousName = emojiToUpdate.name; - fields.previousExtension = emojiToUpdate.extension; - fields.aliases = fields.aliases || ''; - fields.newFile = Boolean(emojiData.length); - const emojiBuffer = Buffer.concat(emojiData); - const isUploadable = Promise.await(Media.isImage(emojiBuffer)); - if (!isUploadable) { - throw new Meteor.Error('emoji-is-not-image', 'Emoji file provided cannot be uploaded since it\'s not an image'); - } - - Meteor.call('insertOrUpdateEmoji', fields); - if (emojiData.length) { - Meteor.call('uploadEmojiCustom', emojiBuffer, emojiMimetype, fields); - } - callback(); - } catch (error) { - return callback(error); - } - })); - this.request.pipe(busboy); - })(); + Meteor.call('insertOrUpdateEmoji', fields); + if (fields.newFile) { + Meteor.call('uploadEmojiCustom', emoji.fileBuffer, emoji.mimetype, fields); + } }); }, }); diff --git a/app/api/server/v1/groups.js b/app/api/server/v1/groups.js index 8a81dddefd63e..141bb94d49d61 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, }); }, }); @@ -481,16 +485,16 @@ API.v1.addRoute('groups.listAll', { authRequired: true }, { const { sort, fields, query } = this.parseJsonQuery(); const ourQuery = Object.assign({}, query, { t: 'p' }); - let rooms = Rooms.find(ourQuery).fetch(); - const totalCount = rooms.length; - - rooms = Rooms.processQueryOptionsOnResult(rooms, { + const cursor = Rooms.find(ourQuery, { sort: sort || { name: 1 }, skip: offset, limit: count, fields, }); + const totalCount = cursor.count(); + const rooms = cursor.fetch(); + return API.v1.success({ groups: rooms.map((room) => this.composeRoomWithLastMessage(room, this.userId)), offset, diff --git a/app/api/server/v1/im.js b/app/api/server/v1/im.js index 21d164ee862b0..41d3d5dfb273c 100644 --- a/app/api/server/v1/im.js +++ b/app/api/server/v1/im.js @@ -1,8 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { Subscriptions, Uploads, Users, Messages, Rooms } from '../../../models/server'; -import { hasPermission } from '../../../authorization/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'; import { API } from '../api'; @@ -19,7 +20,7 @@ function findDirectMessageRoom(params, user, allowAdminOverride) { nameOrId: params.username || params.roomId, }); - const canAccess = Meteor.call('canAccessRoom', room._id, user._id) + const canAccess = canAccessRoom(room, user) || (allowAdminOverride && hasPermission(user._id, 'view-room-administration')); if (!canAccess || !room || room.t !== 'd') { throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "username" param provided does not match any direct message'); @@ -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 e6586a7c12a73..54bd2a563d14f 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 480c3e8743eb1..c05544eb4b821 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 fd17ec3661908..f901247547db3 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 new file mode 100644 index 0000000000000..c424342d97128 --- /dev/null +++ b/app/api/server/v1/ldap.ts @@ -0,0 +1,60 @@ +import { Match, check } from 'meteor/check'; + +import { hasRole } from '../../../authorization/server'; +import { settings } from '../../../settings/server'; +import { API } from '../api'; +import { SystemLogger } from '../../../../server/lib/logger/system'; +import { LDAP } from '../../../../server/sdk'; + +API.v1.addRoute('ldap.testConnection', { authRequired: true }, { + async post() { + if (!this.userId) { + throw new Error('error-invalid-user'); + } + + if (!hasRole(this.userId, 'admin')) { + throw new Error('error-not-authorized'); + } + + if (settings.get('LDAP_Enable') !== true) { + throw new Error('LDAP_disabled'); + } + + try { + await LDAP.testConnection(); + } catch (error) { + SystemLogger.error(error); + throw new Error('Connection_failed'); + } + + return API.v1.success({ + message: 'Connection_success' as const, + }); + }, +}); + +API.v1.addRoute('ldap.testSearch', { authRequired: true }, { + async post() { + check(this.bodyParams, Match.ObjectIncluding({ + username: String, + })); + + if (!this.userId) { + throw new Error('error-invalid-user'); + } + + if (!hasRole(this.userId, 'admin')) { + throw new Error('error-not-authorized'); + } + + if (settings.get('LDAP_Enable') !== true) { + throw new Error('LDAP_disabled'); + } + + await LDAP.testSearch(this.bodyParams.username); + + return API.v1.success({ + message: 'LDAP_User_Found' as const, + }); + }, +}); diff --git a/app/api/server/v1/misc.js b/app/api/server/v1/misc.js index 01376658e4eb3..dedde268fe9e4 100644 --- a/app/api/server/v1/misc.js +++ b/app/api/server/v1/misc.js @@ -7,47 +7,155 @@ import { EJSON } from 'meteor/ejson'; import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import { escapeHTML } from '@rocket.chat/string-helpers'; -import { hasRole, hasPermission } from '../../../authorization/server'; -import { Info } from '../../../utils/server'; +import { hasPermission } from '../../../authorization/server'; import { Users } from '../../../models/server'; import { settings } from '../../../settings/server'; import { API } from '../api'; import { getDefaultUserFields } from '../../../utils/server/functions/getDefaultUserFields'; import { getURL } from '../../../utils/lib/getURL'; -import { StdOut } from '../../../logger/server/streamer'; -import { SystemLogger } from '../../../logger/server'; - - -// DEPRECATED -// Will be removed after v3.0.0 -API.v1.addRoute('info', { authRequired: false }, { - get() { - const warningMessage = 'The endpoint "/v1/info" is deprecated and will be removed after version v3.0.0'; - console.warn(warningMessage); - const user = this.getLoggedInUser(); - - if (user && hasRole(user._id, 'admin')) { - return API.v1.success(this.deprecationWarning({ - endpoint: 'info', - versionWillBeRemoved: '3.0.0', - response: { - info: Info, - }, - })); - } - - return API.v1.success(this.deprecationWarning({ - endpoint: 'info', - versionWillBeRemoved: '3.0.0', - response: { - info: { - version: Info.version, - }, - }, - })); - }, -}); - +import { getLogs } from '../../../../server/stream/stdout'; +import { SystemLogger } from '../../../../server/lib/logger/system'; + +/** + * @openapi + * /api/v1/me: + * get: + * description: Gets user data of the authenticated user + * security: + * - authenticated: [] + * responses: + * 200: + * description: The user data of the authenticated user + * content: + * application/json: + * schema: + * allOf: + * - $ref: '#/components/schemas/ApiSuccessV1' + * - type: object + * properties: + * name: + * type: string + * username: + * type: string + * nickname: + * type: string + * emails: + * type: array + * items: + * type: object + * properties: + * address: + * type: string + * verified: + * type: boolean + * email: + * type: string + * status: + * $ref: '#/components/schemas/UserStatus' + * statusDefault: + * $ref: '#/components/schemas/UserStatus' + * statusText: + * $ref: '#/components/schemas/UserStatus' + * statusConnection: + * $ref: '#/components/schemas/UserStatus' + * bio: + * type: string + * avatarOrigin: + * type: string + * enum: [none, local, upload, url] + * utcOffset: + * type: number + * language: + * type: string + * settings: + * type: object + * properties: + * preferences: + * type: object + * enableAutoAway: + * type: boolean + * idleTimeLimit: + * type: number + * roles: + * type: array + * active: + * type: boolean + * defaultRoom: + * type: string + * customFields: + * type: array + * requirePasswordChange: + * type: boolean + * requirePasswordChangeReason: + * type: string + * services: + * type: object + * properties: + * github: + * type: object + * gitlab: + * type: object + * tokenpass: + * type: object + * blockstack: + * type: object + * password: + * type: object + * properties: + * exists: + * type: boolean + * totp: + * type: object + * properties: + * enabled: + * type: boolean + * email2fa: + * type: object + * properties: + * enabled: + * type: boolean + * statusLivechat: + * type: string + * enum: [available, 'not-available'] + * banners: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * title: + * type: string + * text: + * type: string + * textArguments: + * type: array + * items: {} + * modifiers: + * type: array + * items: + * type: string + * infoUrl: + * type: string + * oauth: + * type: object + * properties: + * authorizedClients: + * type: array + * items: + * type: string + * _updatedAt: + * type: string + * format: date-time + * avatarETag: + * type: string + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ API.v1.addRoute('me', { authRequired: true }, { get() { const fields = getDefaultUserFields(); @@ -222,12 +330,48 @@ API.v1.addRoute('directory', { authRequired: true }, { }, }); +/** + * @openapi + * /api/v1/stdout.queue: + * get: + * description: Retrieves last 1000 lines of server logs + * security: + * - authenticated: ['view-logs'] + * responses: + * 200: + * description: The user data of the authenticated user + * content: + * application/json: + * schema: + * allOf: + * - $ref: '#/components/schemas/ApiSuccessV1' + * - type: object + * properties: + * queue: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * string: + * type: string + * ts: + * type: string + * format: date-time + * default: + * description: Unexpected error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiFailureV1' + */ API.v1.addRoute('stdout.queue', { authRequired: true }, { get() { if (!hasPermission(this.userId, 'view-logs')) { return API.v1.unauthorized(); } - return API.v1.success({ queue: StdOut.queue }); + return API.v1.success({ queue: getLogs() }); }, }); @@ -274,7 +418,7 @@ const methodCall = () => ({ } catch (error) { SystemLogger.error(`Exception while invoking method ${ method }`, error.message); if (settings.get('Log_Level') === '2') { - Meteor._debug(`Exception while invoking method ${ method }`, error.stack); + Meteor._debug(`Exception while invoking method ${ method }`, error); } return API.v1.success(mountResult({ id, error })); } diff --git a/app/api/server/v1/permissions.js b/app/api/server/v1/permissions.js deleted file mode 100644 index e74c956625177..0000000000000 --- a/app/api/server/v1/permissions.js +++ /dev/null @@ -1,120 +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'; - -/** - This API returns all permissions that exists - on the server, with respective roles. - - Method: GET - Route: api/v1/permissions - */ -API.v1.addRoute('permissions', { authRequired: true }, { - get() { - const warningMessage = 'The endpoint "permissions" is deprecated and will be removed after version v0.69'; - console.warn(warningMessage); - - const result = Meteor.runAsUser(this.userId, () => Meteor.call('permissions/get')); - - return API.v1.success(result); - }, -}); - -// DEPRECATED -// TODO: Remove this after three versions have been released. That means at 0.85 this should be gone. -API.v1.addRoute('permissions.list', { authRequired: true }, { - get() { - const result = Meteor.runAsUser(this.userId, () => Meteor.call('permissions/get')); - - return API.v1.success(this.deprecationWarning({ - endpoint: 'permissions.list', - versionWillBeRemoved: '0.85', - response: { - permissions: result, - }, - })); - }, -}); - -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 0000000000000..988f4907e351f --- /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 92895cb9f6a8c..0000000000000 --- a/app/api/server/v1/roles.js +++ /dev/null @@ -1,276 +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(); - - Meteor.runAsUser(this.userId, () => { - Meteor.call('authorization:addUserToRole', this.bodyParams.roleName, user.username, this.bodyParams.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 0000000000000..f7e68deabce90 --- /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 { hasAnyRoleAsync } 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 hasAnyRoleAsync(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 bbc23d165e3fc..df793f68226be 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 } 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'; @@ -65,9 +65,7 @@ API.v1.addRoute('rooms.get', { authRequired: true }, { API.v1.addRoute('rooms.upload/:rid', { authRequired: true }, { post() { - const room = Meteor.call('canAccessRoom', this.urlParams.rid, this.userId); - - if (!room) { + if (!canAccessRoom({ _id: this.urlParams.rid }, { _id: this.userId })) { return API.v1.unauthorized(); } @@ -191,9 +189,11 @@ API.v1.addRoute('rooms.info', { authRequired: true }, { get() { const room = findRoomByIdOrName({ params: this.requestParams() }); const { fields } = this.parseJsonQuery(); - if (!Meteor.call('canAccessRoom', room._id, this.userId, {})) { + + if (!room || !canAccessRoom(room, { _id: this.userId })) { return API.v1.failure('not-allowed', 'Not Allowed'); } + return API.v1.success({ room: Rooms.findOneByIdOrName(room._id, { fields }) }); }, }); @@ -244,9 +244,11 @@ API.v1.addRoute('rooms.getDiscussions', { authRequired: true }, { const room = findRoomByIdOrName({ params: this.requestParams() }); const { offset, count } = this.getPaginationItems(); const { sort, fields, query } = this.parseJsonQuery(); - if (!Meteor.call('canAccessRoom', room._id, this.userId, {})) { + + if (!room || !canAccessRoom(room, { _id: this.userId })) { return API.v1.failure('not-allowed', 'Not Allowed'); } + const ourQuery = Object.assign(query, { prid: room._id }); const discussions = Rooms.find(ourQuery, { @@ -284,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(); @@ -314,6 +330,28 @@ API.v1.addRoute('rooms.autocomplete.channelAndPrivate', { authRequired: true }, }, }); +API.v1.addRoute('rooms.autocomplete.channelAndPrivate.withPagination', { authRequired: true }, { + get() { + const { selector } = this.queryParams; + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + + if (!selector) { + return API.v1.failure('The \'selector\' param is required'); + } + + return API.v1.success(Promise.await(findChannelAndPrivateAutocompleteWithPagination({ + uid: this.userId, + selector: JSON.parse(selector), + pagination: { + offset, + count, + sort, + }, + }))); + }, +}); + API.v1.addRoute('rooms.autocomplete.availableForTeams', { authRequired: true }, { get() { const { name } = this.queryParams; diff --git a/app/api/server/v1/settings.js b/app/api/server/v1/settings.js deleted file mode 100644 index c7a7ca33c97f8..0000000000000 --- a/app/api/server/v1/settings.js +++ /dev/null @@ -1,169 +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.storeSettingValue({ - _id: this.urlParams._id, - value: this.bodyParams.value, - }); - 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 0000000000000..ca0a8ac5178da --- /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 c3235e703e35c..4f3a655aa7d43 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 50f7e4a8ff7d1..a6c514a7c8eca 100644 --- a/app/api/server/v1/users.js +++ b/app/api/server/v1/users.js @@ -2,12 +2,11 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import _ from 'underscore'; -import Busboy from 'busboy'; import { Users, Subscriptions } from '../../../models/server'; import { Users as UsersRaw } from '../../../models/server/raw'; import { hasPermission } from '../../../authorization'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { getURL } from '../../../utils'; import { validateCustomFields, @@ -16,10 +15,11 @@ import { checkUsernameAvailability, setUserAvatar, saveCustomFields, -} from '../../../lib'; + setStatusText, +} from '../../../lib/server'; import { getFullUserDataByIdOrUsername } from '../../../lib/server/functions/getFullUserData'; import { API } from '../api'; -import { setStatusText } from '../../../lib/server'; +import { getUploadFormData } from '../lib/getUploadFormData'; import { findUsersToAutocomplete, getInclusiveFields, getNonEmptyFields, getNonEmptyQuery } from '../lib/users'; import { getUserForCheck, emailCheck } from '../../../2fa/server/code'; import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey'; @@ -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.'); @@ -314,12 +319,14 @@ API.v1.addRoute('users.resetAvatar', { authRequired: true }, { post() { const user = this.getUserFromParams(); - if (user._id === this.userId) { + if (settings.get('Accounts_AllowUserAvatarChange') && user._id === this.userId) { Meteor.runAsUser(this.userId, () => Meteor.call('resetAvatar')); - } else if (hasPermission(this.userId, 'edit-other-user-info')) { - Meteor.runAsUser(user._id, () => Meteor.call('resetAvatar')); + } else if (hasPermission(this.userId, 'edit-other-user-avatar')) { + Meteor.runAsUser(this.userId, () => Meteor.call('resetAvatar', user._id)); } else { - return API.v1.unauthorized(); + throw new Meteor.Error('error-not-allowed', 'Reset avatar is not allowed', { + method: 'users.resetAvatar', + }); } return API.v1.success(); @@ -333,8 +340,9 @@ API.v1.addRoute('users.setAvatar', { authRequired: true }, { userId: Match.Maybe(String), username: Match.Maybe(String), })); + const canEditOtherUserAvatar = hasPermission(this.userId, 'edit-other-user-avatar'); - if (!settings.get('Accounts_AllowUserAvatarChange')) { + if (!settings.get('Accounts_AllowUserAvatarChange') && !canEditOtherUserAvatar) { throw new Meteor.Error('error-not-allowed', 'Change avatar is not allowed', { method: 'users.setAvatar', }); @@ -343,64 +351,44 @@ API.v1.addRoute('users.setAvatar', { authRequired: true }, { let user; if (this.isUserFromParams()) { user = Meteor.users.findOne(this.userId); - } else if (hasPermission(this.userId, 'edit-other-user-avatar')) { + } else if (canEditOtherUserAvatar) { user = this.getUserFromParams(); } else { return API.v1.unauthorized(); } - Meteor.runAsUser(user._id, () => { - if (this.bodyParams.avatarUrl) { - setUserAvatar(user, this.bodyParams.avatarUrl, '', 'url'); - } else { - const busboy = new Busboy({ headers: this.request.headers }); - const fields = {}; - const getUserFromFormData = (fields) => { - if (fields.userId) { - return Users.findOneById(fields.userId, { _id: 1 }); - } - if (fields.username) { - return Users.findOneByUsernameIgnoringCase(fields.username, { _id: 1 }); - } - }; - - Meteor.wrapAsync((callback) => { - busboy.on('file', Meteor.bindEnvironment((fieldname, file, filename, encoding, mimetype) => { - if (fieldname !== 'image') { - return callback(new Meteor.Error('invalid-field')); - } - const imageData = []; - file.on('data', Meteor.bindEnvironment((data) => { - imageData.push(data); - })); - - file.on('end', Meteor.bindEnvironment(() => { - const sentTheUserByFormData = fields.userId || fields.username; - if (sentTheUserByFormData) { - user = getUserFromFormData(fields); - if (!user) { - return callback(new Meteor.Error('error-invalid-user', 'The optional "userId" or "username" param provided does not match any users')); - } - const isAnotherUser = this.userId !== user._id; - if (isAnotherUser && !hasPermission(this.userId, 'edit-other-user-info')) { - return callback(new Meteor.Error('error-not-allowed', 'Not allowed')); - } - } - try { - setUserAvatar(user, Buffer.concat(imageData), mimetype, 'rest'); - callback(); - } catch (e) { - callback(e); - } - })); - })); - busboy.on('field', (fieldname, val) => { - fields[fieldname] = val; - }); - this.request.pipe(busboy); - })(); + if (this.bodyParams.avatarUrl) { + setUserAvatar(user, this.bodyParams.avatarUrl, '', 'url'); + return API.v1.success(); + } + + const { image, ...fields } = Promise.await(getUploadFormData({ + request: this.request, + })); + + if (!image) { + return API.v1.failure('The \'image\' param is required'); + } + + const sentTheUserByFormData = fields.userId || fields.username; + if (sentTheUserByFormData) { + if (fields.userId) { + user = Users.findOneById(fields.userId, { fields: { username: 1 } }); + } else if (fields.username) { + user = Users.findOneByUsernameIgnoringCase(fields.username, { fields: { username: 1 } }); } - }); + + if (!user) { + throw new Meteor.Error('error-invalid-user', 'The optional "userId" or "username" param provided does not match any users'); + } + + const isAnotherUser = this.userId !== user._id; + if (isAnotherUser && !hasPermission(this.userId, 'edit-other-user-avatar')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } + } + + setUserAvatar(user, image.fileBuffer, image.mimetype, 'rest'); return API.v1.success(); }, @@ -610,7 +598,7 @@ API.v1.addRoute('users.setPreferences', { authRequired: true }, { unreadAlert: Match.Maybe(Boolean), notificationsSoundVolume: Match.Maybe(Number), desktopNotifications: Match.Maybe(String), - mobileNotifications: Match.Maybe(String), + pushNotifications: Match.Maybe(String), enableAutoAway: Match.Maybe(Boolean), highlights: Match.Maybe(Array), desktopNotificationRequireInteraction: Match.Maybe(Boolean), @@ -923,10 +911,17 @@ API.v1.addRoute('users.resetTOTP', { authRequired: true, twoFactorRequired: true API.v1.addRoute('users.listTeams', { authRequired: true }, { get() { - const { userId } = this.bodyParams; + check(this.queryParams, Match.ObjectIncluding({ + userId: Match.Maybe(String), + })); + const { userId } = this.queryParams; + + if (!userId) { + throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); + } // If the caller has permission to view all teams, there's no need to filter the teams - const adminId = hasPermission(this.userId, 'view-all-teams') ? '' : this.userId; + const adminId = hasPermission(this.userId, 'view-all-teams') ? undefined : this.userId; const teams = Promise.await(Team.findBySubscribedUserIds(userId, adminId)); @@ -945,7 +940,7 @@ API.v1.addRoute('users.logout', { authRequired: true }, { } // this method logs the user out automatically, if successful returns 1, otherwise 0 - if (!Users.removeResumeService(userId)) { + if (!Users.unsetLoginTokens(userId)) { throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); } @@ -954,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/apple/server/index.js b/app/apple/server/index.js index bfc42742322d0..7b8670be5e50d 100644 --- a/app/apple/server/index.js +++ b/app/apple/server/index.js @@ -1,2 +1,2 @@ -import './startup.js'; +import './startup'; import './loginHandler.js'; diff --git a/app/apple/server/startup.js b/app/apple/server/startup.js deleted file mode 100644 index c0b04ed323349..0000000000000 --- a/app/apple/server/startup.js +++ /dev/null @@ -1,35 +0,0 @@ -import _ from 'underscore'; -import { Meteor } from 'meteor/meteor'; -import { ServiceConfiguration } from 'meteor/service-configuration'; - -import { settings } from '../../settings'; - -settings.addGroup('OAuth', function() { - this.section('Apple', function() { - this.add('Accounts_OAuth_Apple', false, { type: 'boolean', public: true }); - }); -}); - -const configureService = _.debounce(Meteor.bindEnvironment(() => { - if (!settings.get('Accounts_OAuth_Apple')) { - return ServiceConfiguration.configurations.remove({ - service: 'apple', - }); - } - - ServiceConfiguration.configurations.upsert({ - service: 'apple', - }, { - $set: { - // We'll hide this button on Web Client - showButton: false, - enabled: settings.get('Accounts_OAuth_Apple'), - }, - }); -}), 1000); - -Meteor.startup(() => { - settings.get('Accounts_OAuth_Apple', () => { - configureService(); - }); -}); diff --git a/app/apple/server/startup.ts b/app/apple/server/startup.ts new file mode 100644 index 0000000000000..97ba8b3a239f8 --- /dev/null +++ b/app/apple/server/startup.ts @@ -0,0 +1,28 @@ +import { ServiceConfiguration } from 'meteor/service-configuration'; + +import { settings, settingsRegistry } from '../../settings/server'; + +settingsRegistry.addGroup('OAuth', function() { + this.section('Apple', function() { + this.add('Accounts_OAuth_Apple', false, { type: 'boolean', public: true }); + }); +}); + + +settings.watch('Accounts_OAuth_Apple', (enabled) => { + if (!enabled) { + return ServiceConfiguration.configurations.remove({ + service: 'apple', + }); + } + + ServiceConfiguration.configurations.upsert({ + service: 'apple', + }, { + $set: { + // We'll hide this button on Web Client + showButton: false, + enabled: settings.get('Accounts_OAuth_Apple'), + }, + }); +}); diff --git a/app/apps/client/RealAppsEngineUIHost.js b/app/apps/client/RealAppsEngineUIHost.js index 29802587f9b7b..d27fdf36ac05d 100644 --- a/app/apps/client/RealAppsEngineUIHost.js +++ b/app/apps/client/RealAppsEngineUIHost.js @@ -4,8 +4,8 @@ import { AppsEngineUIHost } from '@rocket.chat/apps-engine/client/AppsEngineUIHo import { Rooms } from '../../models/client'; import { APIClient } from '../../utils/client'; -import { baseURI } from '../../utils/client/lib/baseuri'; import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; +import { baseURI } from '../../../client/lib/baseURI'; export class RealAppsEngineUIHost extends AppsEngineUIHost { constructor() { diff --git a/app/apps/client/gameCenter/gameCenter.js b/app/apps/client/gameCenter/gameCenter.js index 8ffee03cd9c00..a23f34837d9eb 100644 --- a/app/apps/client/gameCenter/gameCenter.js +++ b/app/apps/client/gameCenter/gameCenter.js @@ -2,8 +2,9 @@ import { Template } from 'meteor/templating'; import { ReactiveVar } from 'meteor/reactive-var'; import { modal } from '../../../ui-utils/client'; -import { APIClient, t, handleError } from '../../../utils/client'; +import { APIClient, t } from '../../../utils/client'; import './gameCenter.html'; +import { handleError } from '../../../../client/lib/utils/handleError'; const getExternalComponents = async (instance) => { try { diff --git a/app/apps/client/gameCenter/invitePlayers.js b/app/apps/client/gameCenter/invitePlayers.js index 15579db899c2a..93f1d9c730999 100644 --- a/app/apps/client/gameCenter/invitePlayers.js +++ b/app/apps/client/gameCenter/invitePlayers.js @@ -10,7 +10,8 @@ import { Session } from 'meteor/session'; import { AutoComplete } from '../../../meteor-autocomplete/client'; import { roomTypes } from '../../../utils/client'; import { ChatRoom } from '../../../models/client'; -import { call, modal } from '../../../ui-utils/client'; +import { modal } from '../../../ui-utils/client'; +import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; import './invitePlayers.html'; @@ -77,7 +78,7 @@ Template.InvitePlayers.events({ const privateGroupName = `${ name.replace(/\s/g, '-') }-${ Random.id(10) }`; try { - const result = await call('createPrivateGroup', privateGroupName, users); + const result = await callWithErrorHandling('createPrivateGroup', privateGroupName, users); roomTypes.openRouteLink(result.t, result); @@ -90,7 +91,7 @@ Template.InvitePlayers.events({ return; } - call('sendMessage', { + callWithErrorHandling('sendMessage', { _id: Random.id(), rid: result.rid, msg: TAPi18n.__('Apps_Game_Center_Play_Game_Together', { name }), diff --git a/app/apps/client/orchestrator.js b/app/apps/client/orchestrator.js index d4a6f20f47824..4c30b31460cf0 100644 --- a/app/apps/client/orchestrator.js +++ b/app/apps/client/orchestrator.js @@ -1,7 +1,8 @@ import { AppClientManager } from '@rocket.chat/apps-engine/client/AppClientManager'; import { Meteor } from 'meteor/meteor'; -import toastr from 'toastr'; +import { Tracker } from 'meteor/tracker'; +import { dispatchToastMessage } from '../../../client/lib/toast'; import { hasAtLeastOnePermission } from '../../authorization'; import { settings } from '../../settings/client'; import { CachedCollectionManager } from '../../ui-cached-collection'; @@ -52,7 +53,10 @@ class AppClientOrchestrator { handleError = (error) => { console.error(error); if (hasAtLeastOnePermission(['manage-apps'])) { - toastr.error(error.message); + dispatchToastMessage({ + type: 'error', + message: error.message, + }); } } @@ -65,11 +69,12 @@ class AppClientOrchestrator { getAppsFromMarketplace = async () => { const appsOverviews = await APIClient.get('apps', { marketplace: 'true' }); - return appsOverviews.map(({ latest, price, pricingPlans, purchaseType }) => ({ + return appsOverviews.map(({ latest, price, pricingPlans, purchaseType, isEnterpriseOnly }) => ({ ...latest, price, pricingPlans, purchaseType, + isEnterpriseOnly, })); } @@ -189,7 +194,8 @@ Meteor.startup(() => { }); }); - settings.get('Apps_Framework_enabled', (isEnabled) => { + Tracker.autorun(() => { + const isEnabled = settings.get('Apps_Framework_enabled'); Apps.load(isEnabled); }); }); diff --git a/app/apps/server/bridges/commands.ts b/app/apps/server/bridges/commands.ts index aed3d2cf72b26..fe57d2ff8bad8 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 0d09646f38938..154adbdeb1040 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/livechat.ts b/app/apps/server/bridges/livechat.ts index 0ae171c391280..597a1dd59036d 100644 --- a/app/apps/server/bridges/livechat.ts +++ b/app/apps/server/bridges/livechat.ts @@ -8,6 +8,7 @@ import { IDepartment, } from '@rocket.chat/apps-engine/definition/livechat'; import { IUser } from '@rocket.chat/apps-engine/definition/users'; +import { IMessage } from '@rocket.chat/apps-engine/definition/messages'; import { getRoom } from '../../../livechat/server/api/lib/livechat'; import { Livechat } from '../../../livechat/server/lib/Livechat'; @@ -18,6 +19,7 @@ import { LivechatRooms, } from '../../../models/server'; import { AppServerOrchestrator } from '../orchestrator'; +import { OmnichannelSourceType } from '../../../../definition/IRoom'; export class AppLivechatBridge extends LivechatBridge { // eslint-disable-next-line no-empty-function @@ -44,7 +46,13 @@ export class AppLivechatBridge extends LivechatBridge { guest: this.orch.getConverters()?.get('visitors').convertAppVisitor(message.visitor), message: this.orch.getConverters()?.get('messages').convertAppMessage(message), agent: undefined, - roomInfo: undefined, + roomInfo: { + source: { + type: OmnichannelSourceType.APP, + id: appId, + alias: this.orch.getManager()?.getOneById(appId)?.getNameSlug(), + }, + }, }); return msg._id; @@ -80,22 +88,33 @@ export class AppLivechatBridge extends LivechatBridge { guest: this.orch.getConverters()?.get('visitors').convertAppVisitor(visitor), agent: agentRoom, rid: Random.id(), - roomInfo: undefined, + roomInfo: { + source: { + type: OmnichannelSourceType.APP, + id: appId, + alias: this.orch.getManager()?.getOneById(appId)?.getNameSlug(), + }, + }, extraParams: undefined, }); return this.orch.getConverters()?.get('rooms').convertRoom(result.room); } - protected async closeRoom(room: ILivechatRoom, comment: string, appId: string): Promise { + protected async closeRoom(room: ILivechatRoom, comment: string, closer: IUser | undefined, appId: string): Promise { this.orch.debugLog(`The App ${ appId } is closing a livechat room.`); - return Livechat.closeRoom({ - visitor: this.orch.getConverters()?.get('visitors').convertAppVisitor(room.visitor), + const user = closer && this.orch.getConverters()?.get('users').convertById(closer.id); + const visitor = this.orch.getConverters()?.get('visitors').convertAppVisitor(room.visitor); + + const closeData: any = { room: this.orch.getConverters()?.get('rooms').convertAppRoom(room), comment, - user: undefined, - }); + ...user && { user }, + ...visitor && { visitor }, + }; + + return Livechat.closeRoom(closeData); } protected async findRooms(visitor: IVisitor, departmentId: string | null, appId: string): Promise> { @@ -166,10 +185,22 @@ export class AppLivechatBridge extends LivechatBridge { type, }; + let userId; + let transferredTo; + + if (targetAgent?.id) { + transferredTo = Users.findOneAgentById(targetAgent.id, { fields: { _id: 1, username: 1, name: 1 } }); + if (!transferredTo) { + throw new Error('Invalid target agent, cannot transfer'); + } + + userId = transferredTo._id; + } + return Livechat.transfer( this.orch.getConverters()?.get('rooms').convertAppRoom(currentRoom), this.orch.getConverters()?.get('visitors').convertAppVisitor(visitor), - { userId: targetAgent ? targetAgent.id : undefined, departmentId, transferredBy }, + { userId, departmentId, transferredBy, transferredTo }, ); } @@ -222,6 +253,19 @@ export class AppLivechatBridge extends LivechatBridge { return LivechatDepartment.findEnabledWithAgents().map(boundConverter); } + protected async _fetchLivechatRoomMessages(appId: string, roomId: string): Promise> { + this.orch.debugLog(`The App ${ appId } is getting the transcript for livechat room ${ roomId }.`); + const messageConverter = this.orch.getConverters()?.get('messages'); + + if (!messageConverter) { + throw new Error('Could not get the message converter to process livechat room messages'); + } + + const boundMessageConverter = messageConverter.convertMessage.bind(messageConverter); + + return Livechat.getRoomMessages({ rid: roomId }).map(boundMessageConverter); + } + protected async setCustomFields(data: { token: IVisitor['token']; key: string; value: string; overwrite: boolean }, appId: string): Promise { this.orch.debugLog(`The App ${ appId } is setting livechat visitor's custom fields.`); diff --git a/app/apps/server/bridges/scheduler.ts b/app/apps/server/bridges/scheduler.ts index 0676178c84de6..721c3a8c8aa07 100644 --- a/app/apps/server/bridges/scheduler.ts +++ b/app/apps/server/bridges/scheduler.ts @@ -1,4 +1,5 @@ import Agenda from 'agenda'; +import { ObjectID } from 'bson'; import { MongoInternals } from 'meteor/mongo'; import { StartupType, @@ -10,13 +11,15 @@ import { SchedulerBridge } from '@rocket.chat/apps-engine/server/bridges/Schedul import { AppServerOrchestrator } from '../orchestrator'; -function _callProcessor(processor: Function): (job: { attrs?: { data: object } }) => void { +function _callProcessor(processor: Function): (job: Agenda.Job) => void { return (job): void => { const data = job?.attrs?.data || {}; // This field is for internal use, no need to leak to app processor delete (data as any).appId; + data.jobId = job.attrs._id.toString(); + return processor(data); }; } @@ -68,10 +71,10 @@ export class AppSchedulerBridge extends SchedulerBridge { * @param {Array.} processors An array of processors * @param {string} appId * - * @returns Promise + * @returns {string[]} List of task ids run at startup, or void no startup run is set */ - protected async registerProcessors(processors: Array = [], appId: string): Promise { - const runAfterRegister: Promise[] = []; + protected async registerProcessors(processors: Array = [], appId: string): Promise> { + const runAfterRegister: Promise[] = []; this.orch.debugLog(`The App ${ appId } is registering job processors`, processors); processors.forEach(({ id, processor, startupSetting }: IProcessor) => { this.scheduler.define(id, _callProcessor(processor)); @@ -82,10 +85,10 @@ export class AppSchedulerBridge extends SchedulerBridge { switch (startupSetting.type) { case StartupType.ONETIME: - runAfterRegister.push(this.scheduleOnceAfterRegister({ id, when: startupSetting.when, data: startupSetting.data }, appId)); + runAfterRegister.push(this.scheduleOnceAfterRegister({ id, when: startupSetting.when, data: startupSetting.data }, appId) as Promise); break; case StartupType.RECURRING: - runAfterRegister.push(this.scheduleRecurring({ id, interval: startupSetting.interval, skipImmediate: startupSetting.skipImmediate, data: startupSetting.data }, appId)); + runAfterRegister.push(this.scheduleRecurring({ id, interval: startupSetting.interval, skipImmediate: startupSetting.skipImmediate, data: startupSetting.data }, appId) as Promise); break; default: this.orch.getRocketChatLogger().error(`Invalid startup setting type (${ String((startupSetting as any).type) }) for the processor ${ id }`); @@ -94,7 +97,7 @@ export class AppSchedulerBridge extends SchedulerBridge { }); if (runAfterRegister.length) { - await Promise.all(runAfterRegister); + return Promise.all(runAfterRegister) as Promise>; } } @@ -107,22 +110,23 @@ export class AppSchedulerBridge extends SchedulerBridge { * @param {Object} [job.data] An optional object that is passed to the processor * @param {string} appId * - * @returns Promise + * @returns {string} taskid */ - protected async scheduleOnce(job: IOnetimeSchedule, appId: string): Promise { - this.orch.debugLog(`The App ${ appId } is scheduling an onetime job`, job); + protected async scheduleOnce({ id, when, data }: IOnetimeSchedule, appId: string): Promise { + this.orch.debugLog(`The App ${ appId } is scheduling an onetime job (processor ${ id })`); try { await this.startScheduler(); - await this.scheduler.schedule(job.when, job.id, this.decorateJobData(job.data, appId)); + const job = await this.scheduler.schedule(when, id, this.decorateJobData(data, appId)); + return job.attrs._id.toString(); } catch (e) { this.orch.getRocketChatLogger().error(e); } } - private async scheduleOnceAfterRegister(job: IOnetimeSchedule, appId: string): Promise { + private async scheduleOnceAfterRegister(job: IOnetimeSchedule, appId: string): Promise { const scheduledJobs = await this.scheduler.jobs({ name: job.id, type: 'normal' }); if (!scheduledJobs.length) { - await this.scheduleOnce(job, appId); + return this.scheduleOnce(job, appId); } } @@ -136,13 +140,14 @@ export class AppSchedulerBridge extends SchedulerBridge { * @param {Object} [job.data] An optional object that is passed to the processor * @param {string} appId * - * @returns Promise + * @returns {string} taskid */ - protected async scheduleRecurring({ id, interval, skipImmediate = false, data }: IRecurringSchedule, appId: string): Promise { - this.orch.debugLog(`The App ${ appId } is scheduling a recurring job`, id); + protected async scheduleRecurring({ id, interval, skipImmediate = false, data }: IRecurringSchedule, appId: string): Promise { + this.orch.debugLog(`The App ${ appId } is scheduling a recurring job (processor ${ id })`); try { await this.startScheduler(); - await this.scheduler.every(interval, id, this.decorateJobData(data, appId), { skipImmediate }); + const job = await this.scheduler.every(interval, id, this.decorateJobData(data, appId), { skipImmediate }); + return job.attrs._id.toString(); } catch (e) { this.orch.getRocketChatLogger().error(e); } @@ -159,8 +164,17 @@ export class AppSchedulerBridge extends SchedulerBridge { protected async cancelJob(jobId: string, appId: string): Promise { this.orch.debugLog(`The App ${ appId } is canceling a job`, jobId); await this.startScheduler(); + + let cancelQuery; + try { + cancelQuery = { _id: new ObjectID(jobId.split('_')[0]) }; + } catch (jobDocIdError) { + // it is not a valid objectid, so it won't try to cancel by document id + cancelQuery = { name: jobId }; + } + try { - await this.scheduler.cancel({ name: jobId }); + await this.scheduler.cancel(cancelQuery); } catch (e) { this.orch.getRocketChatLogger().error(e); } @@ -184,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 ad1c234b0af27..ba0626434ff92 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/bridges/uploads.ts b/app/apps/server/bridges/uploads.ts index 27dcbbff3dd45..ab7d6bb2da14d 100644 --- a/app/apps/server/bridges/uploads.ts +++ b/app/apps/server/bridges/uploads.ts @@ -7,7 +7,6 @@ import { FileUpload } from '../../../file-upload/server'; import { determineFileType } from '../../lib/misc/determineFileType'; import { AppServerOrchestrator } from '../orchestrator'; - const getUploadDetails = (details: IUploadDetails): Partial => { if (details.visitorToken) { const { userId, ...result } = details; @@ -56,17 +55,16 @@ export class AppUploadBridge extends UploadBridge { return new Promise(Meteor.bindEnvironment((resolve, reject) => { try { - const uploadedFile = fileStore.insertSync(getUploadDetails(details), buffer); - - if (details.visitorToken) { - Meteor.call('sendFileLivechatMessage', details.rid, details.visitorToken, uploadedFile); - } else { - Meteor.runAsUser(details.userId, () => { + Meteor.runAsUser(details.userId, () => { + const uploadedFile = fileStore.insertSync(getUploadDetails(details), buffer); + this.orch.debugLog(`The App ${ appId } has created an upload`, uploadedFile); + if (details.visitorToken) { + Meteor.call('sendFileLivechatMessage', details.rid, details.visitorToken, uploadedFile); + } else { Meteor.call('sendFileMessage', details.rid, null, uploadedFile); - }); - } - - resolve(this.orch.getConverters()?.get('uploads').convertToApp(uploadedFile)); + } + resolve(this.orch.getConverters()?.get('uploads').convertToApp(uploadedFile)); + }); } catch (err) { reject(err); } diff --git a/app/apps/server/communication/rest.js b/app/apps/server/communication/rest.js index 3c245d75ed592..1386ba39cfa89 100644 --- a/app/apps/server/communication/rest.js +++ b/app/apps/server/communication/rest.js @@ -4,11 +4,12 @@ import { HTTP } from 'meteor/http'; import { API } from '../../../api/server'; import { getUploadFormData } from '../../../api/server/lib/getUploadFormData'; import { getWorkspaceAccessToken, getUserCloudAccessToken } from '../../../cloud/server'; -import { settings } from '../../../settings'; +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/communication/uikit.js b/app/apps/server/communication/uikit.js index d86aa6b8fb4c5..9f12a44b06140 100644 --- a/app/apps/server/communication/uikit.js +++ b/app/apps/server/communication/uikit.js @@ -18,9 +18,9 @@ apiServer.disable('x-powered-by'); let corsEnabled = false; let allowListOrigins = []; -settings.get('API_Enable_CORS', (_, value) => { corsEnabled = value; }); +settings.watch('API_Enable_CORS', (value) => { corsEnabled = value; }); -settings.get('API_CORS_Origin', (_, value) => { +settings.watch('API_CORS_Origin', (value) => { allowListOrigins = value ? value.trim().split(',').map((origin) => String(origin).trim().toLocaleLowerCase()) : []; }); @@ -263,7 +263,6 @@ const appsRoutes = (orch) => (req, res) => { res.sendStatus(200); } catch (e) { - console.error(e); res.status(500).send(e.message); } break; @@ -297,6 +296,10 @@ const appsRoutes = (orch) => (req, res) => { } break; } + + default: { + res.status(500).send({ error: 'Unknown action' }); + } } // TODO: validate payloads per type diff --git a/app/apps/server/communication/websockets.js b/app/apps/server/communication/websockets.js index fd9360dce473d..dd8da2b64c89e 100644 --- a/app/apps/server/communication/websockets.js +++ b/app/apps/server/communication/websockets.js @@ -1,5 +1,6 @@ import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import notifications from '../../../notifications/server/lib/Notifications'; export const AppEvents = Object.freeze({ @@ -49,10 +50,10 @@ export class AppServerListener { this.received.set(`${ AppEvents.APP_STATUS_CHANGE }_${ appId }`, { appId, status, when: new Date() }); if (AppStatusUtils.isEnabled(status)) { - await this.orch.getManager().enable(appId).catch(console.error); + await this.orch.getManager().enable(appId).catch(SystemLogger.error); this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_STATUS_CHANGE, { appId, status }); } else if (AppStatusUtils.isDisabled(status)) { - await this.orch.getManager().disable(appId, status, true).catch(console.error); + await this.orch.getManager().disable(appId, status, true).catch(SystemLogger.error); this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_STATUS_CHANGE, { appId, status }); } } @@ -68,7 +69,10 @@ export class AppServerListener { const storageItem = await this.orch.getStorage().retrieveOne(appId); - await this.orch.getManager().update(Buffer.from(storageItem.zip, 'base64')); + const appPackage = await this.orch.getAppSourceStorage().fetch(storageItem); + + await this.orch.getManager().updateLocal(storageItem, appPackage); + this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_UPDATED, appId); } diff --git a/app/apps/server/converters/settings.js b/app/apps/server/converters/settings.js index 82ffcd2b2f0f1..bc5949bc7ccd1 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 d95f5d10067f4..efbda7ae5fd1b 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/converters/users.js b/app/apps/server/converters/users.js index 79d4016e7c203..17895ff00cd8b 100644 --- a/app/apps/server/converters/users.js +++ b/app/apps/server/converters/users.js @@ -42,6 +42,7 @@ export class AppUsersConverter { updatedAt: user._updatedAt, lastLoginAt: user.lastLogin, appId: user.appId, + customFields: user.customFields, }; } diff --git a/app/apps/server/cron.js b/app/apps/server/cron.js index 3201612ea6b60..d38eebe060b29 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 50f67ae497c58..81eaad8110d8b 100644 --- a/app/apps/server/orchestrator.js +++ b/app/apps/server/orchestrator.js @@ -3,37 +3,48 @@ import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata'; import { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; import { Meteor } from 'meteor/meteor'; -import { Logger } from '../../logger'; -import { AppsLogsModel, AppsModel, AppsPersistenceModel, Permissions } from '../../models'; -import { settings } from '../../settings'; +import { Logger } from '../../../server/lib/logger/Logger'; +import { AppsLogsModel, AppsModel, AppsPersistenceModel } from '../../models/server'; +import { settings, settingsRegistry } from '../../settings/server'; import { RealAppBridges } from './bridges'; import { AppMethods, AppServerNotifier, AppsRestApi, AppUIKitInteractionApi } from './communication'; import { AppMessagesConverter, AppRoomsConverter, AppSettingsConverter, AppUsersConverter } from './converters'; import { AppDepartmentsConverter } from './converters/departments'; import { AppUploadsConverter } from './converters/uploads'; import { AppVisitorsConverter } from './converters/visitors'; -import { AppRealLogsStorage, AppRealStorage } from './storage'; +import { AppRealLogsStorage, AppRealStorage, ConfigurableAppSourceStorage } from './storage'; function isTesting() { return process.env.TEST_MODE === 'true'; } +let appsSourceStorageType; +let appsSourceStorageFilesystemPath; + export class AppServerOrchestrator { constructor() { this._isInitialized = false; } initialize() { + if (this._isInitialized) { + return; + } + this._rocketchatLogger = new Logger('Rocket.Chat Apps'); - Permissions.create('manage-apps', ['admin']); - this._marketplaceUrl = 'https://marketplace.rocket.chat'; + if (typeof process.env.OVERWRITE_INTERNAL_MARKETPLACE_URL === 'string' && process.env.OVERWRITE_INTERNAL_MARKETPLACE_URL !== '') { + this._marketplaceUrl = process.env.OVERWRITE_INTERNAL_MARKETPLACE_URL; + } else { + this._marketplaceUrl = 'https://marketplace.rocket.chat'; + } this._model = new AppsModel(); this._logModel = new AppsLogsModel(); this._persistModel = new AppsPersistenceModel(); this._storage = new AppRealStorage(this._model); this._logStorage = new AppRealLogsStorage(this._logModel); + this._appSourceStorage = new ConfigurableAppSourceStorage(appsSourceStorageType, appsSourceStorageFilesystemPath); this._converters = new Map(); this._converters.set('messages', new AppMessagesConverter(this)); @@ -46,7 +57,12 @@ export class AppServerOrchestrator { this._bridges = new RealAppBridges(this); - this._manager = new AppManager(this._storage, this._logStorage, this._bridges); + this._manager = new AppManager({ + metadataStorage: this._storage, + logStorage: this._logStorage, + bridges: this._bridges, + sourceStorage: this._appSourceStorage, + }); this._communicators = new Map(); this._communicators.set('methods', new AppMethods(this)); @@ -61,6 +77,9 @@ export class AppServerOrchestrator { return this._model; } + /** + * @returns {AppsPersistenceModel} + */ getPersistenceModel() { return this._persistModel; } @@ -93,6 +112,10 @@ export class AppServerOrchestrator { return this._manager.getExternalComponentManager().getProvidedComponents(); } + getAppSourceStorage() { + return this._appSourceStorage; + } + isInitialized() { return this._isInitialized; } @@ -109,6 +132,9 @@ export class AppServerOrchestrator { return settings.get('Apps_Framework_Development_Mode') && !isTesting(); } + /** + * @returns {Logger} + */ getRocketChatLogger() { return this._rocketchatLogger; } @@ -132,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() { @@ -174,7 +201,7 @@ export class AppServerOrchestrator { export const AppEvents = AppInterface; export const Apps = new AppServerOrchestrator(); -settings.addGroup('General', function() { +settingsRegistry.addGroup('General', function() { this.section('Apps', function() { this.add('Apps_Logs_TTL', '30_days', { type: 'select', @@ -211,11 +238,50 @@ settings.addGroup('General', function() { public: true, hidden: false, }); + + this.add('Apps_Framework_Source_Package_Storage_Type', 'gridfs', { + type: 'select', + values: [{ + key: 'gridfs', + i18nLabel: 'GridFS', + }, { + key: 'filesystem', + i18nLabel: 'FileSystem', + }], + public: true, + hidden: false, + alert: 'Apps_Framework_Source_Package_Storage_Type_Alert', + }); + + this.add('Apps_Framework_Source_Package_Storage_FileSystem_Path', '', { + type: 'string', + public: true, + enableQuery: { + _id: 'Apps_Framework_Source_Package_Storage_Type', + value: 'filesystem', + }, + alert: 'Apps_Framework_Source_Package_Storage_FileSystem_Alert', + }); }); }); +settings.watch('Apps_Framework_Source_Package_Storage_Type', (value) => { + if (!Apps.isInitialized()) { + appsSourceStorageType = value; + } else { + Apps.getAppSourceStorage().setStorage(value); + } +}); + +settings.watch('Apps_Framework_Source_Package_Storage_FileSystem_Path', (value) => { + if (!Apps.isInitialized()) { + appsSourceStorageFilesystemPath = value; + } else { + Apps.getAppSourceStorage().setFileSystemStoragePath(value); + } +}); -settings.get('Apps_Framework_enabled', (key, isEnabled) => { +settings.watch('Apps_Framework_enabled', (isEnabled) => { // In case this gets called before `Meteor.startup` if (!Apps.isInitialized()) { return; @@ -228,7 +294,7 @@ settings.get('Apps_Framework_enabled', (key, isEnabled) => { } }); -settings.get('Apps_Logs_TTL', (key, value) => { +settings.watch('Apps_Logs_TTL', (value) => { if (!Apps.isInitialized()) { return; } diff --git a/app/apps/server/storage/AppFileSystemSourceStorage.ts b/app/apps/server/storage/AppFileSystemSourceStorage.ts new file mode 100644 index 0000000000000..f14e73c67d028 --- /dev/null +++ b/app/apps/server/storage/AppFileSystemSourceStorage.ts @@ -0,0 +1,68 @@ +import { promises as fs } from 'fs'; +import { join, normalize } from 'path'; + +import { AppSourceStorage, IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; + +export class AppFileSystemSourceStorage extends AppSourceStorage { + private pathPrefix = 'fs:/'; + + private path: string; + + public setPath(path: string): void { + this.path = path; + } + + public checkPath(): void { + if (!this.path) { + throw new Error('Invalid path configured for file system App storage'); + } + } + + public async store(item: IAppStorageItem, zip: Buffer): Promise { + this.checkPath(); + + const filePath = this.itemToFilename(item); + + await fs.writeFile(filePath, zip); + + return this.filenameToSourcePath(filePath); + } + + public async fetch(item: IAppStorageItem): Promise { + if (!item.sourcePath) { + throw new Error('Invalid source path'); + } + + return fs.readFile(this.sourcePathToFilename(item.sourcePath)); + } + + public async update(item: IAppStorageItem, zip: Buffer): Promise { + this.checkPath(); + + const filePath = this.itemToFilename(item); + + await fs.writeFile(filePath, zip); + + return this.filenameToSourcePath(filePath); + } + + public async remove(item: IAppStorageItem): Promise { + if (!item.sourcePath) { + return; + } + + return fs.unlink(this.sourcePathToFilename(item.sourcePath)); + } + + private itemToFilename(item: IAppStorageItem): string { + return `${ normalize(join(this.path, item.id)) }.zip`; + } + + private filenameToSourcePath(filename: string): string { + return this.pathPrefix + filename; + } + + private sourcePathToFilename(sourcePath: string): string { + return sourcePath.substring(this.pathPrefix.length); + } +} diff --git a/app/apps/server/storage/AppGridFSSourceStorage.ts b/app/apps/server/storage/AppGridFSSourceStorage.ts new file mode 100644 index 0000000000000..30d5980f21533 --- /dev/null +++ b/app/apps/server/storage/AppGridFSSourceStorage.ts @@ -0,0 +1,86 @@ +import { MongoInternals } from 'meteor/mongo'; +import { GridFSBucket, GridFSBucketWriteStream, ObjectId } from 'mongodb'; +import { AppSourceStorage, IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; + +import { streamToBuffer } from '../../../file-upload/server/lib/streamToBuffer'; + +export class AppGridFSSourceStorage extends AppSourceStorage { + private pathPrefix = 'GridFS:/'; + + private bucket: GridFSBucket; + + constructor() { + super(); + + const { GridFSBucket } = MongoInternals.NpmModules.mongodb.module; + const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; + + this.bucket = new GridFSBucket(db, { + bucketName: 'rocketchat_apps_packages', + chunkSizeBytes: 1024 * 255, + }); + } + + public async store(item: IAppStorageItem, zip: Buffer): Promise { + return new Promise((resolve, reject) => { + const filename = this.itemToFilename(item); + const writeStream: GridFSBucketWriteStream = this.bucket.openUploadStream(filename) + .on('finish', () => resolve(this.idToPath(writeStream.id))) + .on('error', (error) => reject(error)); + + writeStream.write(zip); + writeStream.end(); + }); + } + + public async fetch(item: IAppStorageItem): Promise { + return streamToBuffer(this.bucket.openDownloadStream(this.itemToObjectId(item))); + } + + public async update(item: IAppStorageItem, zip: Buffer): Promise { + return new Promise((resolve, reject) => { + const fileId = this.itemToFilename(item); + const writeStream: GridFSBucketWriteStream = this.bucket.openUploadStream(fileId) + .on('finish', () => { + resolve(this.idToPath(writeStream.id)); + // An error in the following line would not cause the update process to fail + // eslint-disable-next-line @typescript-eslint/no-empty-function + this.remove(item).catch(() => {}); + }) + + .on('error', (error) => reject(error)); + + writeStream.write(zip); + writeStream.end(); + }); + } + + public async remove(item: IAppStorageItem): Promise { + return new Promise((resolve, reject) => { + this.bucket.delete(this.itemToObjectId(item), (error) => { + if (error) { + if (error.message.includes('FileNotFound: no file with id')) { + console.warn(`This instance could not remove the ${ item.info.name } app package. If you are running Rocket.Chat in a cluster with multiple instances, possibly other instance removed the package. If this is not the case, it is possible that the file in the database got renamed or removed manually.`); + return resolve(); + } + + return reject(error); + } + + resolve(); + }); + }); + } + + private itemToFilename(item: IAppStorageItem): string { + return `${ item.info.nameSlug }-${ item.info.version }.package`; + } + + private idToPath(id: GridFSBucketWriteStream['id']): string { + return this.pathPrefix + id; + } + + private itemToObjectId(item: IAppStorageItem): ObjectId { + return new ObjectId(item.sourcePath?.substring(this.pathPrefix.length)); + } +} diff --git a/app/apps/server/storage/AppRealStorage.ts b/app/apps/server/storage/AppRealStorage.ts new file mode 100644 index 0000000000000..b90e5890edfa1 --- /dev/null +++ b/app/apps/server/storage/AppRealStorage.ts @@ -0,0 +1,92 @@ +import { AppMetadataStorage, IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; + +import { AppsModel } from '../../../models/server/models/apps-model'; + +export class AppRealStorage extends AppMetadataStorage { + constructor(private db: AppsModel) { + super('mongodb'); + } + + public create(item: IAppStorageItem): Promise { + return new Promise((resolve, reject) => { + item.createdAt = new Date(); + item.updatedAt = new Date(); + + let doc; + + try { + doc = this.db.findOne({ $or: [{ id: item.id }, { 'info.nameSlug': item.info.nameSlug }] }); + } catch (e) { + return reject(e); + } + + if (doc) { + return reject(new Error('App already exists.')); + } + + try { + const id = this.db.insert(item); + item._id = id; + + resolve(item); + } catch (e) { + reject(e); + } + }); + } + + public retrieveOne(id: string): Promise { + return new Promise((resolve, reject) => { + let doc; + + try { + doc = this.db.findOne({ $or: [{ _id: id }, { id }] }); + } catch (e) { + return reject(e); + } + + resolve(doc); + }); + } + + public retrieveAll(): Promise> { + return new Promise((resolve, reject) => { + let docs: Array; + + try { + docs = this.db.find({}).fetch(); + } catch (e) { + return reject(e); + } + + const items = new Map(); + + docs.forEach((i) => items.set(i.id, i)); + + resolve(items); + }); + } + + public update(item: IAppStorageItem): Promise { + return new Promise((resolve, reject) => { + try { + this.db.update({ id: item.id }, item); + resolve(item.id); + } catch (e) { + return reject(e); + } + }).then(this.retrieveOne.bind(this)); + } + + public remove(id: string): Promise<{ success: boolean }> { + return new Promise((resolve, reject) => { + try { + this.db.remove({ id }); + } catch (e) { + return reject(e); + } + + resolve({ success: true }); + }); + } +} diff --git a/app/apps/server/storage/ConfigurableAppSourceStorage.ts b/app/apps/server/storage/ConfigurableAppSourceStorage.ts new file mode 100644 index 0000000000000..2f0118b18cf31 --- /dev/null +++ b/app/apps/server/storage/ConfigurableAppSourceStorage.ts @@ -0,0 +1,53 @@ +import { AppSourceStorage, IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; + +import { AppFileSystemSourceStorage } from './AppFileSystemSourceStorage'; +import { AppGridFSSourceStorage } from './AppGridFSSourceStorage'; + +export class ConfigurableAppSourceStorage extends AppSourceStorage { + private filesystem: AppFileSystemSourceStorage; + + private gridfs: AppGridFSSourceStorage; + + private storage: AppSourceStorage; + + constructor(readonly storageType: string, filesystemStoragePath: string) { + super(); + + this.filesystem = new AppFileSystemSourceStorage(); + this.gridfs = new AppGridFSSourceStorage(); + + this.setStorage(storageType); + this.setFileSystemStoragePath(filesystemStoragePath); + } + + public setStorage(type: string): void { + switch (type) { + case 'filesystem': + this.storage = this.filesystem; + break; + case 'gridfs': + this.storage = this.gridfs; + break; + } + } + + public setFileSystemStoragePath(path: string): void { + this.filesystem.setPath(path); + } + + public async store(item: IAppStorageItem, zip: Buffer): Promise { + return this.storage.store(item, zip); + } + + public async fetch(item: IAppStorageItem): Promise { + return this.storage.fetch(item); + } + + public async update(item: IAppStorageItem, zip: Buffer): Promise { + return this.storage.update(item, zip); + } + + public async remove(item: IAppStorageItem): Promise { + return this.storage.remove(item); + } +} diff --git a/app/apps/server/storage/index.js b/app/apps/server/storage/index.js index fd1680c1c9c31..289c563763652 100644 --- a/app/apps/server/storage/index.js +++ b/app/apps/server/storage/index.js @@ -1,4 +1,5 @@ -import { AppRealLogsStorage } from './logs-storage'; -import { AppRealStorage } from './storage'; - -export { AppRealLogsStorage, AppRealStorage }; +export { AppRealLogsStorage } from './logs-storage'; +export { AppRealStorage } from './AppRealStorage'; +export { AppFileSystemSourceStorage } from './AppFileSystemSourceStorage'; +export { AppGridFSSourceStorage } from './AppGridFSSourceStorage'; +export { ConfigurableAppSourceStorage } from './ConfigurableAppSourceStorage'; diff --git a/app/apps/server/storage/storage.js b/app/apps/server/storage/storage.js deleted file mode 100644 index 8fc9c79b705ff..0000000000000 --- a/app/apps/server/storage/storage.js +++ /dev/null @@ -1,91 +0,0 @@ -import { AppStorage } from '@rocket.chat/apps-engine/server/storage'; - -export class AppRealStorage extends AppStorage { - constructor(data) { - super('mongodb'); - this.db = data; - } - - create(item) { - return new Promise((resolve, reject) => { - item.createdAt = new Date(); - item.updatedAt = new Date(); - - let doc; - - try { - doc = this.db.findOne({ $or: [{ id: item.id }, { 'info.nameSlug': item.info.nameSlug }] }); - } catch (e) { - return reject(e); - } - - if (doc) { - return reject(new Error('App already exists.')); - } - - try { - const id = this.db.insert(item); - item._id = id; - - resolve(item); - } catch (e) { - reject(e); - } - }); - } - - retrieveOne(id) { - return new Promise((resolve, reject) => { - let doc; - - try { - doc = this.db.findOne({ $or: [{ _id: id }, { id }] }); - } catch (e) { - return reject(e); - } - - resolve(doc); - }); - } - - retrieveAll() { - return new Promise((resolve, reject) => { - let docs; - - try { - docs = this.db.find({}).fetch(); - } catch (e) { - return reject(e); - } - - const items = new Map(); - - docs.forEach((i) => items.set(i.id, i)); - - resolve(items); - }); - } - - update(item) { - return new Promise((resolve, reject) => { - try { - this.db.update({ id: item.id }, item); - resolve(item.id); - } catch (e) { - return reject(e); - } - }).then(this.retrieveOne.bind(this)); - } - - remove(id) { - return new Promise((resolve, reject) => { - try { - this.db.remove({ id }); - } catch (e) { - return reject(e); - } - - resolve({ success: true }); - }); - } -} diff --git a/app/apps/server/tests/messages.tests.js b/app/apps/server/tests/messages.tests.js index 9dee0c68bcdae..9e4919731e8c1 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/assets/server/assets.js b/app/assets/server/assets.js index cd9c0f0bc6b63..78c10d88ed325 100644 --- a/app/assets/server/assets.js +++ b/app/assets/server/assets.js @@ -7,11 +7,12 @@ import _ from 'underscore'; import sizeOf from 'image-size'; import sharp from 'sharp'; -import { settings } from '../../settings/server'; +import { settings, settingsRegistry } from '../../settings/server'; import { getURL } from '../../utils/lib/getURL'; import { mime } from '../../utils/lib/mimeTypes'; import { hasPermission } from '../../authorization'; import { RocketChatFile } from '../../file'; +import { Settings } from '../../models/server'; const RocketChatAssetsInstance = new RocketChatFile.GridFS({ @@ -25,8 +26,6 @@ const assets = { constraints: { type: 'image', extensions: ['svg', 'png', 'jpg', 'jpeg'], - width: undefined, - height: undefined, }, wizard: { step: 3, @@ -35,12 +34,9 @@ const assets = { }, background: { label: 'login background (svg, png, jpg)', - defaultUrl: undefined, constraints: { type: 'image', extensions: ['svg', 'png', 'jpg', 'jpeg'], - width: undefined, - height: undefined, }, }, favicon_ico: { @@ -49,8 +45,6 @@ const assets = { constraints: { type: 'image', extensions: ['ico'], - width: undefined, - height: undefined, }, }, favicon: { @@ -59,8 +53,6 @@ const assets = { constraints: { type: 'image', extensions: ['svg'], - width: undefined, - height: undefined, }, }, favicon_16: { @@ -179,8 +171,6 @@ const assets = { constraints: { type: 'image', extensions: ['svg'], - width: undefined, - height: undefined, }, }, }; @@ -234,7 +224,7 @@ export const RocketChatAssets = new class { defaultUrl: assets[asset].defaultUrl, }; - settings.updateById(key, value); + Settings.updateValueById(key, value); return RocketChatAssets.processAsset(key, value); }, 200); })); @@ -255,7 +245,7 @@ export const RocketChatAssets = new class { defaultUrl: assets[asset].defaultUrl, }; - settings.updateById(key, value); + Settings.updateValueById(key, value); RocketChatAssets.processAsset(key, value); } @@ -317,9 +307,9 @@ export const RocketChatAssets = new class { } }(); -settings.addGroup('Assets'); +settingsRegistry.addGroup('Assets'); -settings.add('Assets_SvgFavicon_Enable', true, { +settingsRegistry.add('Assets_SvgFavicon_Enable', true, { type: 'boolean', group: 'Assets', i18nLabel: 'Enable_Svg_Favicon', @@ -328,7 +318,7 @@ settings.add('Assets_SvgFavicon_Enable', true, { function addAssetToSetting(asset, value) { const key = `Assets_${ asset }`; - settings.add(key, { + settingsRegistry.add(key, { defaultUrl: value.defaultUrl, }, { type: 'asset', @@ -344,7 +334,7 @@ function addAssetToSetting(asset, value) { if (typeof currentValue === 'object' && currentValue.defaultUrl !== assets[asset].defaultUrl) { currentValue.defaultUrl = assets[asset].defaultUrl; - settings.updateById(key, currentValue); + Settings.updateValueById(key, currentValue); } } @@ -353,7 +343,7 @@ for (const key of Object.keys(assets)) { addAssetToSetting(key, value); } -settings.get(/^Assets_/, (key, value) => RocketChatAssets.processAsset(key, value)); +settings.watchByRegex(/^Assets_/, (key, value) => RocketChatAssets.processAsset(key, value)); Meteor.startup(function() { return Meteor.setTimeout(function() { diff --git a/app/authentication/server/lib/logLoginAttempts.ts b/app/authentication/server/lib/logLoginAttempts.ts index 699c7c00ddd0f..ed73cf395e6da 100644 --- a/app/authentication/server/lib/logLoginAttempts.ts +++ b/app/authentication/server/lib/logLoginAttempts.ts @@ -1,5 +1,6 @@ import { ILoginAttempt } from '../ILoginAttempt'; import { settings } from '../../../settings/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; export const logFailedLoginAttempts = (login: ILoginAttempt): void => { if (!settings.get('Login_Logs_Enabled')) { @@ -25,5 +26,5 @@ export const logFailedLoginAttempts = (login: ILoginAttempt): void => { if (!settings.get('Login_Logs_UserAgent')) { userAgent = '-'; } - console.log('Failed login detected - Username[%s] ClientAddress[%s] ForwardedFor[%s] XRealIp[%s] UserAgent[%s]', user, clientAddress, forwardedFor, realIp, userAgent); + SystemLogger.info(`Failed login detected - Username[${ user }] ClientAddress[${ clientAddress }] ForwardedFor[${ forwardedFor }] XRealIp[${ realIp }] UserAgent[${ userAgent }]`); }; diff --git a/app/authentication/server/lib/restrictLoginAttempts.ts b/app/authentication/server/lib/restrictLoginAttempts.ts index a746d6e7938e4..2b63656d87e13 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 '../../../utils/lib/date.helper'; -import Sessions from '../../../models/server/raw/Sessions'; +import { addMinutesToADate } from '../../../../lib/utils/addMinutesToADate'; import { getClientAddress } from '../../../../server/lib/getClientAddress'; import { sendMessage } from '../../../lib/server/functions'; import { Logger } from '../../../logger/server'; @@ -16,7 +14,6 @@ const logger = new Logger('LoginProtection'); export const notifyFailedLogin = async (ipOrUsername: string, blockedUntil: Date, failedAttempts: number): Promise => { const channelToNotify = settings.get('Block_Multiple_Failed_Logins_Notify_Failed_Channel'); if (!channelToNotify) { - /* @ts-expect-error */ logger.error('Cannot notify failed logins: channel provided is invalid'); return; } @@ -24,7 +21,6 @@ export const notifyFailedLogin = async (ipOrUsername: string, blockedUntil: Date // to avoid issues when "fname" is presented in the UI, check if the name matches it as well const room = await Rooms.findOneByNameOrFname(channelToNotify); if (!room) { - /* @ts-expect-error */ logger.error('Cannot notify failed logins: channel provided doesn\'t exists'); return; } @@ -54,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) { @@ -94,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, }; @@ -144,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 680041fd2d530..3f3b5acc5f8d6 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 { @@ -25,16 +25,15 @@ Accounts.config({ forbidClientAccountCreation: true, }); -const updateMailConfig = _.debounce(() => { - Accounts._options.loginExpirationInDays = settings.get('Accounts_LoginExpiration'); - Accounts.emailTemplates.siteName = settings.get('Site_Name'); +Meteor.startup(() => { + settings.watchMultiple(['Accounts_LoginExpiration', 'Site_Name', 'From_Email'], () => { + Accounts._options.loginExpirationInDays = settings.get('Accounts_LoginExpiration'); - Accounts.emailTemplates.from = `${ settings.get('Site_Name') } <${ settings.get('From_Email') }>`; -}, 1000); + Accounts.emailTemplates.siteName = settings.get('Site_Name'); -Meteor.startup(() => { - settings.get(/^(Accounts_LoginExpiration|Site_Name|From_Email)$/, updateMailConfig); + Accounts.emailTemplates.from = `${ settings.get('Site_Name') } <${ settings.get('From_Email') }>`; + }); }); Accounts.emailTemplates.userToActivate = { @@ -187,8 +186,8 @@ Accounts.onCreateUser(function(options, user = {}) { if (!user.active) { const destinations = []; - - Roles.findUsersInRole('admin').forEach((adminUser) => { + const usersInRole = Promise.await(Roles.findUsersInRole('admin')); + Promise.await(usersInRole.toArray()).forEach((adminUser) => { if (Array.isArray(adminUser.emails)) { adminUser.emails.forEach((email) => { destinations.push(`${ adminUser.name }<${ email.address }>`); @@ -206,6 +205,7 @@ Accounts.onCreateUser(function(options, user = {}) { Mailer.send(email); } + callbacks.run('onCreateUser', options, user); return user; }); diff --git a/app/authentication/server/startup/settings.ts b/app/authentication/server/startup/settings.ts index 8f160e2b72efb..9ee111123f40a 100644 --- a/app/authentication/server/startup/settings.ts +++ b/app/authentication/server/startup/settings.ts @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; -import { settings } from '../../../settings/server'; +import { settingsRegistry } from '../../../settings/server'; Meteor.startup(function() { - settings.addGroup('Accounts', function() { + settingsRegistry.addGroup('Accounts', function() { const enableQueryCollectData = { _id: 'Block_Multiple_Failed_Logins_Enabled', value: true }; this.section('Login_Attempts', function() { diff --git a/app/authorization/client/hasPermission.js b/app/authorization/client/hasPermission.js deleted file mode 100644 index a48549979c7ab..0000000000000 --- a/app/authorization/client/hasPermission.js +++ /dev/null @@ -1,80 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Template } from 'meteor/templating'; - -import { ChatPermissions } from './lib/ChatPermissions'; -import * as Models from '../../models'; -import { AuthorizationUtils } from '../lib/AuthorizationUtils'; - -function atLeastOne(permissions = [], scope, userId) { - userId = userId || Meteor.userId(); - const user = Models.Users.findOneById(userId, { fields: { roles: 1 } }); - - return permissions.some((permissionId) => { - if (user && user.roles) { - if (AuthorizationUtils.isPermissionRestrictedForRoleList(permissionId, user.roles)) { - return false; - } - } - - const permission = ChatPermissions.findOne(permissionId, { fields: { roles: 1 } }); - const roles = (permission && permission.roles) || []; - - return roles.some((roleName) => { - const role = Models.Roles.findOne(roleName, { fields: { scope: 1 } }); - const roleScope = role && role.scope; - const model = Models[roleScope]; - - return model && model.isUserInRole && model.isUserInRole(userId, roleName, scope); - }); - }); -} - -function all(permissions = [], scope, userId) { - userId = userId || Meteor.userId(); - const user = Models.Users.findOneById(userId, { fields: { roles: 1 } }); - - return permissions.every((permissionId) => { - if (user && user.roles) { - if (AuthorizationUtils.isPermissionRestrictedForRoleList(permissionId, user.roles)) { - return false; - } - } - - const permission = ChatPermissions.findOne(permissionId, { fields: { roles: 1 } }); - const roles = (permission && permission.roles) || []; - - return roles.some((roleName) => { - const role = Models.Roles.findOne(roleName, { fields: { scope: 1 } }); - const roleScope = role && role.scope; - const model = Models[roleScope]; - - return model && model.isUserInRole && model.isUserInRole(userId, roleName, scope); - }); - }); -} - -function _hasPermission(permissions, scope, strategy, userId) { - userId = userId || Meteor.userId(); - if (!userId) { - return false; - } - - if (!Models.AuthzCachedCollection.ready.get()) { - return false; - } - - permissions = [].concat(permissions); - return strategy(permissions, scope, userId); -} - -Template.registerHelper('hasPermission', function(permission, scope) { - return _hasPermission(permission, scope, atLeastOne); -}); -Template.registerHelper('userHasAllPermission', function(userId, permission, scope) { - return _hasPermission(permission, scope, all, userId); -}); - -export const hasAllPermission = (permissions, scope) => _hasPermission(permissions, scope, all); -export const hasAtLeastOnePermission = (permissions, scope) => _hasPermission(permissions, scope, atLeastOne); -export const userHasAllPermission = (permissions, scope, userId) => _hasPermission(permissions, scope, all, userId); -export const hasPermission = hasAllPermission; diff --git a/app/authorization/client/hasPermission.ts b/app/authorization/client/hasPermission.ts new file mode 100644 index 0000000000000..23a0aeeca8a61 --- /dev/null +++ b/app/authorization/client/hasPermission.ts @@ -0,0 +1,82 @@ +import { Meteor } from 'meteor/meteor'; + +import { ChatPermissions } from './lib/ChatPermissions'; +import * as Models from '../../models/client'; +import { AuthorizationUtils } from '../lib/AuthorizationUtils'; +import { IUser } from '../../../definition/IUser'; +import { IRole } from '../../../definition/IRole'; +import { IPermission } from '../../../definition/IPermission'; + +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: string | undefined, userId: IUser['_id']): boolean => { + const user: IUser | null = Models.Users.findOneById(userId, { fields: { roles: 1 } }); + + const checkEachPermission = quantifier.bind(permissionIds); + + return checkEachPermission((permissionId) => { + if (user?.roles) { + if (AuthorizationUtils.isPermissionRestrictedForRoleList(permissionId, user.roles)) { + return false; + } + } + + const permission: IPermission | null = ChatPermissions.findOne(permissionId, { fields: { roles: 1 } }); + const roles = permission?.roles ?? []; + + return roles.some((roleName) => { + const role = Models.Roles.findOne(roleName, { fields: { scope: 1 } }); + const roleScope = role?.scope; + + if (!isValidScope(roleScope)) { + return false; + } + + const model = Models[roleScope as keyof typeof Models]; + return model.isUserInRole && model.isUserInRole(userId, roleName, scope); + }); + }); + }; + +const atLeastOne = createPermissionValidator(Array.prototype.some); + +const all = createPermissionValidator(Array.prototype.every); + +const validatePermissions = ( + permissions: IPermission['_id'] | IPermission['_id'][], + scope: string | undefined, + predicate: (permissionIds: IPermission['_id'][], scope: string | undefined, userId: IUser['_id']) => boolean, + userId?: IUser['_id'] | null, +): boolean => { + userId = userId ?? Meteor.userId(); + + if (!userId) { + return false; + } + + if (!Models.AuthzCachedCollection.ready.get()) { + return false; + } + + return predicate(([] as IPermission['_id'][]).concat(permissions), scope, userId); +}; + +export const hasAllPermission = ( + permissions: IPermission['_id'] | IPermission['_id'][], + scope?: string, +): boolean => validatePermissions(permissions, scope, all); + +export const hasAtLeastOnePermission = ( + permissions: IPermission['_id'] | IPermission['_id'][], + scope?: string, +): boolean => validatePermissions(permissions, scope, atLeastOne); + +export const userHasAllPermission = ( + permissions: IPermission['_id'] | IPermission['_id'][], + scope?: string, + userId?: IUser['_id'] | null, +): boolean => validatePermissions(permissions, scope, all, userId); + +export const hasPermission = hasAllPermission; diff --git a/app/authorization/client/startup.js b/app/authorization/client/startup.js index 202f0089d3d10..4bad4a825cdc1 100644 --- a/app/authorization/client/startup.js +++ b/app/authorization/client/startup.js @@ -30,10 +30,10 @@ Meteor.startup(() => { const events = { changed: (role) => { delete role.type; - Roles.upsert({ _id: role.name }, role); + Roles.upsert({ _id: role._id }, role); }, removed: (role) => { - Roles.remove({ _id: role.name }); + Roles.remove({ _id: role._id }); }, }; diff --git a/app/authorization/server/functions/addUserRoles.js b/app/authorization/server/functions/addUserRoles.js deleted file mode 100644 index 46302e81eb8d7..0000000000000 --- a/app/authorization/server/functions/addUserRoles.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; - -import { getRoles } from './getRoles'; -import { Users, Roles } from '../../../models'; - -export const addUserRoles = (userId, roleNames, scope) => { - if (!userId || !roleNames) { - return false; - } - - const user = Users.db.findOneById(userId); - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - function: 'RocketChat.authz.addUserRoles', - }); - } - - roleNames = [].concat(roleNames); - const existingRoleNames = _.pluck(getRoles(), '_id'); - const invalidRoleNames = _.difference(roleNames, existingRoleNames); - - if (!_.isEmpty(invalidRoleNames)) { - for (const role of invalidRoleNames) { - Roles.createOrUpdate(role); - } - } - - Roles.addUserRoles(userId, roleNames, scope); - - return true; -}; diff --git a/app/authorization/server/functions/addUserRoles.ts b/app/authorization/server/functions/addUserRoles.ts new file mode 100644 index 0000000000000..dda983ff6a3cb --- /dev/null +++ b/app/authorization/server/functions/addUserRoles.ts @@ -0,0 +1,36 @@ +import { Meteor } from 'meteor/meteor'; +import _ from 'underscore'; + +import { getRoles } from './getRoles'; +import { Users } from '../../../models/server'; +import { IRole, IUser } from '../../../../definition/IUser'; +import { Roles } from '../../../models/server/raw'; + +export const addUserRoles = (userId: IUser['_id'], roleNames: IRole['name'][], scope?: string): boolean => { + if (!userId || !roleNames) { + return false; + } + + const user = Users.db.findOneById(userId); + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + function: 'RocketChat.authz.addUserRoles', + }); + } + + 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) { + Promise.await(Roles.createOrUpdate(role)); + } + } + + 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 5a943ec031a66..d232f890af2ea 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 9d20c72d29a94..0000000000000 --- 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 0000000000000..27de1000bb0a5 --- /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 27c369acf9ffd..0000000000000 --- 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 0000000000000..740431af3f068 --- /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/hasRole.js b/app/authorization/server/functions/hasRole.js deleted file mode 100644 index 545adc3f737b6..0000000000000 --- a/app/authorization/server/functions/hasRole.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Roles } from '../../../models/server/raw'; - -export const hasRoleAsync = async (userId, roleNames, scope) => { - if (!userId || userId === '') { - return false; - } - - return Roles.isUserInRoles(userId, roleNames, scope); -}; - -export const hasRole = (userId, roleNames, scope) => Promise.await(hasRoleAsync(userId, roleNames, scope)); - -export const subscriptionHasRole = (sub, role) => sub.roles && sub.roles.includes(role); diff --git a/app/authorization/server/functions/hasRole.ts b/app/authorization/server/functions/hasRole.ts new file mode 100644 index 0000000000000..17a6a5d7b5269 --- /dev/null +++ b/app/authorization/server/functions/hasRole.ts @@ -0,0 +1,16 @@ +import { Roles } from '../../../models/server/raw'; +import { ISubscription } from '../../../../definition/ISubscription'; + +export const hasAnyRoleAsync = async (userId: string, roleNames: string[], scope?: string | undefined): Promise => { + if (!userId || userId === '') { + return false; + } + + return Roles.isUserInRoles(userId, roleNames, scope); +}; + +export const hasRole = (userId: string, roleNames: string, scope?: string | undefined): boolean => Promise.await(hasAnyRoleAsync(userId, [roleNames], scope)); + +export const hasAnyRole = (userId: string, roleNames: string[], scope?: string | undefined): boolean => Promise.await(hasAnyRoleAsync(userId, roleNames, scope)); + +export const subscriptionHasRole = (sub: ISubscription, role: string): boolean | undefined => sub.roles && sub.roles.includes(role); diff --git a/app/authorization/server/functions/removeUserFromRoles.js b/app/authorization/server/functions/removeUserFromRoles.js index b08c2778addba..a55d722bb891b 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.ts b/app/authorization/server/functions/upsertPermissions.ts new file mode 100644 index 0000000000000..6c41b1b11af09 --- /dev/null +++ b/app/authorization/server/functions/upsertPermissions.ts @@ -0,0 +1,266 @@ +/* eslint no-multi-spaces: 0 */ +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 = 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 + // 2. admin, moderator, and user roles should not be deleted as they are referenced in the code. + const permissions = [ + { _id: 'access-permissions', roles: ['admin'] }, + { _id: 'access-setting-permissions', roles: ['admin'] }, + { _id: 'add-oauth-service', roles: ['admin'] }, + { _id: 'add-user-to-joined-room', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'add-user-to-any-c-room', roles: ['admin'] }, + { _id: 'add-user-to-any-p-room', roles: [] }, + { _id: 'api-bypass-rate-limit', roles: ['admin', 'bot', 'app'] }, + { _id: 'archive-room', roles: ['admin', 'owner'] }, + { _id: 'assign-admin-role', roles: ['admin'] }, + { _id: 'assign-roles', roles: ['admin'] }, + { _id: 'ban-user', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'bulk-register-user', roles: ['admin'] }, + { _id: 'change-livechat-room-visitor', roles: ['admin', 'livechat-manager', 'livechat-agent'] }, + { _id: 'create-c', roles: ['admin', 'user', 'bot', 'app'] }, + { _id: 'create-d', roles: ['admin', 'user', 'bot', 'app'] }, + { _id: 'create-p', roles: ['admin', 'user', 'bot', 'app'] }, + { _id: 'create-personal-access-tokens', roles: ['admin', 'user'] }, + { _id: 'create-user', roles: ['admin'] }, + { _id: 'clean-channel-history', roles: ['admin'] }, + { _id: 'delete-c', roles: ['admin', 'owner'] }, + { _id: 'delete-d', roles: ['admin'] }, + { _id: 'delete-message', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'delete-own-message', roles: ['admin', 'user'] }, + { _id: 'delete-p', roles: ['admin', 'owner'] }, + { _id: 'delete-user', roles: ['admin'] }, + { _id: 'edit-message', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'edit-other-user-active-status', roles: ['admin'] }, + { _id: 'edit-other-user-info', roles: ['admin'] }, + { _id: 'edit-other-user-password', roles: ['admin'] }, + { _id: 'edit-other-user-avatar', roles: ['admin'] }, + { _id: 'edit-other-user-e2ee', roles: ['admin'] }, + { _id: 'edit-other-user-totp', roles: ['admin'] }, + { _id: 'edit-privileged-setting', roles: ['admin'] }, + { _id: 'edit-room', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'edit-room-avatar', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'edit-room-retention-policy', roles: ['admin'] }, + { _id: 'force-delete-message', roles: ['admin', 'owner'] }, + { _id: 'join-without-join-code', roles: ['admin', 'bot', 'app'] }, + { _id: 'leave-c', roles: ['admin', 'user', 'bot', 'anonymous', 'app'] }, + { _id: 'leave-p', roles: ['admin', 'user', 'bot', 'anonymous', 'app'] }, + { _id: 'logout-other-user', roles: ['admin'] }, + { _id: 'manage-assets', roles: ['admin'] }, + { _id: 'manage-email-inbox', roles: ['admin'] }, + { _id: 'manage-emoji', roles: ['admin'] }, + { _id: 'manage-user-status', roles: ['admin'] }, + { _id: 'manage-outgoing-integrations', roles: ['admin'] }, + { _id: 'manage-incoming-integrations', roles: ['admin'] }, + { _id: 'manage-own-outgoing-integrations', roles: ['admin'] }, + { _id: 'manage-own-incoming-integrations', roles: ['admin'] }, + { _id: 'manage-oauth-apps', roles: ['admin'] }, + { _id: 'manage-selected-settings', roles: ['admin'] }, + { _id: 'mention-all', roles: ['admin', 'owner', 'moderator', 'user'] }, + { _id: 'mention-here', roles: ['admin', 'owner', 'moderator', 'user'] }, + { _id: 'mute-user', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'remove-user', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'run-import', roles: ['admin'] }, + { _id: 'run-migration', roles: ['admin'] }, + { _id: 'set-moderator', roles: ['admin', 'owner'] }, + { _id: 'set-owner', roles: ['admin', 'owner'] }, + { _id: 'send-many-messages', roles: ['admin', 'bot', 'app'] }, + { _id: 'set-leader', roles: ['admin', 'owner'] }, + { _id: 'unarchive-room', roles: ['admin'] }, + { _id: 'view-c-room', roles: ['admin', 'user', 'bot', 'app', 'anonymous'] }, + { _id: 'user-generate-access-token', roles: ['admin'] }, + { _id: 'view-d-room', roles: ['admin', 'user', 'bot', 'app', 'guest'] }, + { _id: 'view-full-other-user-info', roles: ['admin'] }, + { _id: 'view-history', roles: ['admin', 'user', 'anonymous'] }, + { _id: 'view-joined-room', roles: ['guest', 'bot', 'app', 'anonymous'] }, + { _id: 'view-join-code', roles: ['admin'] }, + { _id: 'view-logs', roles: ['admin'] }, + { _id: 'view-other-user-channels', roles: ['admin'] }, + { _id: 'view-p-room', roles: ['admin', 'user', 'anonymous', 'guest'] }, + { _id: 'view-privileged-setting', roles: ['admin'] }, + { _id: 'view-room-administration', roles: ['admin'] }, + { _id: 'view-statistics', roles: ['admin'] }, + { _id: 'view-user-administration', roles: ['admin'] }, + { _id: 'preview-c-room', roles: ['admin', 'user', 'anonymous'] }, + { _id: 'view-outside-room', roles: ['admin', 'owner', 'moderator', 'user'] }, + { _id: 'view-broadcast-member-list', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'call-management', roles: ['admin', 'owner', 'moderator'] }, + { _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'] }, + { _id: 'close-others-livechat-room', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'on-hold-livechat-room', roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'] }, + { _id: 'on-hold-others-livechat-room', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'save-others-livechat-room-info', roles: ['livechat-manager', 'livechat-monitor'] }, + { _id: 'remove-closed-livechat-rooms', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'view-livechat-analytics', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'view-livechat-queue', roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'] }, + { _id: 'transfer-livechat-guest', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'manage-livechat-managers', roles: ['livechat-manager', 'admin'] }, + { _id: 'manage-livechat-agents', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'manage-livechat-departments', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'view-livechat-departments', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'add-livechat-department-agents', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'view-livechat-current-chats', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'view-livechat-real-time-monitoring', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'view-livechat-triggers', roles: ['livechat-manager', 'admin'] }, + { _id: 'view-livechat-customfields', roles: ['livechat-manager', 'admin'] }, + { _id: 'view-livechat-installation', roles: ['livechat-manager', 'admin'] }, + { _id: 'view-livechat-appearance', roles: ['livechat-manager', 'admin'] }, + { _id: 'view-livechat-webhooks', roles: ['livechat-manager', 'admin'] }, + { _id: 'view-livechat-facebook', roles: ['livechat-manager', 'admin'] }, + { _id: 'view-livechat-business-hours', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'view-livechat-room-closed-same-department', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'view-livechat-room-closed-by-another-agent', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'view-livechat-room-customfields', roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'] }, + { _id: 'edit-livechat-room-customfields', roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'] }, + { _id: 'send-omnichannel-chat-transcript', roles: ['livechat-manager', 'admin'] }, + { _id: 'mail-messages', roles: ['admin'] }, + { _id: 'toggle-room-e2e-encryption', roles: ['owner'] }, + { _id: 'message-impersonate', roles: ['bot', 'app'] }, + { _id: 'create-team', roles: ['admin', 'user'] }, + { _id: 'delete-team', roles: ['admin', 'owner'] }, + { _id: 'convert-team', roles: ['admin', 'owner'] }, + { _id: 'edit-team', roles: ['admin', 'owner'] }, + { _id: 'add-team-member', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'edit-team-member', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'add-team-channel', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'edit-team-channel', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'remove-team-channel', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'view-all-team-channels', roles: ['admin', 'owner'] }, + { _id: 'view-all-teams', roles: ['admin'] }, + { _id: 'remove-closed-livechat-room', roles: ['livechat-manager', 'admin'] }, + { _id: 'remove-livechat-department', roles: ['livechat-manager', 'admin'] }, + { _id: 'manage-apps', roles: ['admin'] }, + { _id: 'post-readonly', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'set-readonly', roles: ['admin', 'owner'] }, + { _id: 'set-react-when-readonly', roles: ['admin', 'owner'] }, + { _id: 'manage-cloud', roles: ['admin'] }, + { _id: 'manage-sounds', roles: ['admin'] }, + { _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 await (const permission of permissions) { + await Permissions.create(permission._id, permission.roles); + } + + const defaultRoles = [ + { name: 'admin', scope: 'Users', description: 'Admin' }, + { name: 'moderator', scope: 'Subscriptions', description: 'Moderator' }, + { name: 'leader', scope: 'Subscriptions', description: 'Leader' }, + { name: 'owner', scope: 'Subscriptions', description: 'Owner' }, + { name: 'user', scope: 'Users', description: '' }, + { name: 'bot', scope: 'Users', description: '' }, + { name: 'app', scope: 'Users', description: '' }, + { name: 'guest', scope: 'Users', description: '' }, + { name: 'anonymous', scope: 'Users', description: '' }, + { name: 'livechat-agent', scope: 'Users', description: 'Livechat Agent' }, + { name: 'livechat-manager', scope: 'Users', description: 'Livechat Manager' }, + ]; + + for await (const role of defaultRoles) { + await Roles.createOrUpdate(role.name, role.scope as 'Users' | 'Subscriptions', role.description, true, false); + } + + const getPreviousPermissions = async function(settingId?: string): Promise> { + const previousSettingPermissions: { + [key: string]: IPermission; + } = {}; + + const selector = { level: 'settings' as const, ...settingId && { settingId } }; + + await Permissions.find(selector).forEach( + function(permission: IPermission) { + previousSettingPermissions[permission._id] = permission; + }); + return previousSettingPermissions; + }; + + const createSettingPermission = async function(setting: ISetting, previousSettingPermissions: { + [key: string]: IPermission; + }): Promise { + const permissionId = getSettingPermissionId(setting._id); + 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, + section: setting.section, + sorter: setting.sorter, + roles: [], + }; + // copy previously assigned roles if available + if (previousSettingPermissions[permissionId] && previousSettingPermissions[permissionId].roles) { + permission.roles = previousSettingPermissions[permissionId].roles; + } + if (setting.group) { + permission.groupPermissionId = getSettingPermissionId(setting.group); + } + if (setting.section) { + permission.sectionPermissionId = getSettingPermissionId(setting.section); + } + + const existent = await Permissions.findOne({ + _id: permissionId, + ...permission, + }, { fields: { _id: 1 } }); + + if (!existent) { + try { + 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 + await Permissions.update({ _id: permissionId }, { $set: permission }, { upsert: true }); + } + } + } + + delete previousSettingPermissions[permissionId]; + }; + + const createPermissionsForExistingSettings = async function(): Promise { + const previousSettingPermissions = await getPreviousPermissions(); + + (await Settings.findNotHidden().toArray()).forEach((setting) => { + createSettingPermission(setting, previousSettingPermissions); + }); + + // remove permissions for non-existent settings + for await (const obsoletePermission of Object.keys(previousSettingPermissions)) { + if (previousSettingPermissions.hasOwnProperty(obsoletePermission)) { + await Permissions.deleteOne({ _id: obsoletePermission }); + } + } + }; + + // for each setting which already exists, create a permission to allow changing just this one setting + createPermissionsForExistingSettings(); + + // register a callback for settings for be create in higher-level-packages + 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/index.js b/app/authorization/server/index.js index c248f1c6e0e90..f4d7cc8c08b77 100644 --- a/app/authorization/server/index.js +++ b/app/authorization/server/index.js @@ -21,7 +21,6 @@ import './methods/removeRoleFromPermission'; import './methods/removeUserFromRole'; import './methods/saveRole'; import './streamer/permissions'; -import './startup'; export { getRoles, diff --git a/app/authorization/server/methods/addPermissionToRole.js b/app/authorization/server/methods/addPermissionToRole.js deleted file mode 100644 index 5ca74ed3dbc9b..0000000000000 --- a/app/authorization/server/methods/addPermissionToRole.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Permissions } from '../../../models/server'; -import { hasPermission } from '../functions/hasPermission'; -import { CONSTANTS, AuthorizationUtils } from '../../lib'; - -Meteor.methods({ - 'authorization:addPermissionToRole'(permissionId, role) { - if (AuthorizationUtils.isPermissionRestrictedForRole(permissionId, role)) { - throw new Meteor.Error('error-action-not-allowed', 'Permission is restricted', { - method: 'authorization:addPermissionToRole', - action: 'Adding_permission', - }); - } - - const uid = Meteor.userId(); - const permission = Permissions.findOneById(permissionId); - - 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', { - method: 'authorization:addPermissionToRole', - action: 'Adding_permission', - }); - } - // for setting-based-permissions, authorize the group access as well - if (permission.groupPermissionId) { - Permissions.addRole(permission.groupPermissionId, role); - } - - return Permissions.addRole(permission._id, role); - }, -}); diff --git a/app/authorization/server/methods/addPermissionToRole.ts b/app/authorization/server/methods/addPermissionToRole.ts new file mode 100644 index 0000000000000..42990b114437c --- /dev/null +++ b/app/authorization/server/methods/addPermissionToRole.ts @@ -0,0 +1,40 @@ +import { Meteor } from 'meteor/meteor'; + + +import { hasPermission } from '../functions/hasPermission'; +import { CONSTANTS, AuthorizationUtils } from '../../lib'; +import { Permissions } from '../../../models/server/raw'; + +Meteor.methods({ + async 'authorization:addPermissionToRole'(permissionId, role) { + if (AuthorizationUtils.isPermissionRestrictedForRole(permissionId, role)) { + throw new Meteor.Error('error-action-not-allowed', 'Permission is restricted', { + method: 'authorization:addPermissionToRole', + action: 'Adding_permission', + }); + } + + const uid = Meteor.userId(); + 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', { + method: 'authorization:addPermissionToRole', + action: 'Adding_permission', + }); + } + // for setting-based-permissions, authorize the group access as well + if (permission.groupPermissionId) { + Permissions.addRole(permission.groupPermissionId, role); + } + + return Permissions.addRole(permission._id, role); + }, +}); diff --git a/app/authorization/server/methods/addUserToRole.js b/app/authorization/server/methods/addUserToRole.js deleted file mode 100644 index a7fdd21ec24dc..0000000000000 --- a/app/authorization/server/methods/addUserToRole.js +++ /dev/null @@ -1,66 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; - -import { Users, Roles } from '../../../models/server'; -import { settings } from '../../../settings/server'; -import { hasPermission } from '../functions/hasPermission'; -import { api } from '../../../../server/sdk/api'; - -Meteor.methods({ - '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', - action: 'Accessing_permissions', - }); - } - - if (!roleName || !_.isString(roleName) || !username || !_.isString(username)) { - throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { - method: 'authorization:addUserToRole', - }); - } - - if (roleName === 'admin' && !hasPermission(Meteor.userId(), 'assign-admin-role')) { - throw new Meteor.Error('error-action-not-allowed', 'Assigning admin is not allowed', { - method: 'authorization:addUserToRole', - action: 'Assign_admin', - }); - } - - const user = Users.findOneByUsernameIgnoringCase(username, { - fields: { - _id: 1, - }, - }); - - if (!user || !user._id) { - throw new Meteor.Error('error-user-not-found', 'User not found', { - method: 'authorization:addUserToRole', - }); - } - - // verify if user can be added to given scope - if (scope && !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); - - if (settings.get('UI_DisplayRoles')) { - api.broadcast('user.roleUpdate', { - type: 'added', - _id: roleName, - u: { - _id: user._id, - username, - }, - scope, - }); - } - - return add; - }, -}); diff --git a/app/authorization/server/methods/addUserToRole.ts b/app/authorization/server/methods/addUserToRole.ts new file mode 100644 index 0000000000000..3182d327ff476 --- /dev/null +++ b/app/authorization/server/methods/addUserToRole.ts @@ -0,0 +1,67 @@ +import { Meteor } from 'meteor/meteor'; +import _ from 'underscore'; + +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({ + 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', + action: 'Accessing_permissions', + }); + } + + if (!roleName || !_.isString(roleName) || !username || !_.isString(username)) { + throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { + method: 'authorization:addUserToRole', + }); + } + + if (roleName === 'admin' && !hasPermission(Meteor.userId(), 'assign-admin-role')) { + throw new Meteor.Error('error-action-not-allowed', 'Assigning admin is not allowed', { + method: 'authorization:addUserToRole', + action: 'Assign_admin', + }); + } + + const user = Users.findOneByUsernameIgnoringCase(username, { + fields: { + _id: 1, + }, + }); + + if (!user || !user._id) { + throw new Meteor.Error('error-user-not-found', 'User not found', { + method: 'authorization:addUserToRole', + }); + } + + // verify if user can be added to given 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 = await Roles.addUserRoles(user._id, [roleName], scope); + + if (settings.get('UI_DisplayRoles')) { + api.broadcast('user.roleUpdate', { + type: 'added', + _id: roleName, + u: { + _id: user._id, + username, + }, + scope, + }); + } + + return add; + }, +}); diff --git a/app/authorization/server/methods/deleteRole.js b/app/authorization/server/methods/deleteRole.js deleted file mode 100644 index 8613e1761b0a5..0000000000000 --- a/app/authorization/server/methods/deleteRole.js +++ /dev/null @@ -1,40 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import * as Models from '../../../models/server'; -import { hasPermission } from '../functions/hasPermission'; - -Meteor.methods({ - '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', - action: 'Accessing_permissions', - }); - } - - const role = Models.Roles.findOne(roleName); - if (!role) { - throw new Meteor.Error('error-invalid-role', 'Invalid role', { - method: 'authorization:deleteRole', - }); - } - - if (role.protected) { - throw new Meteor.Error('error-delete-protected-role', 'Cannot delete a protected role', { - method: 'authorization:deleteRole', - }); - } - - const roleScope = role.scope || 'Users'; - const model = Models[roleScope]; - const existingUsers = model && model.findUsersInRoles && model.findUsersInRoles(roleName); - - if (existingUsers && existingUsers.count() > 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); - }, -}); diff --git a/app/authorization/server/methods/deleteRole.ts b/app/authorization/server/methods/deleteRole.ts new file mode 100644 index 0000000000000..8925942b23f3d --- /dev/null +++ b/app/authorization/server/methods/deleteRole.ts @@ -0,0 +1,38 @@ +import { Meteor } from 'meteor/meteor'; + +import { Roles } from '../../../models/server/raw'; +import { hasPermission } from '../functions/hasPermission'; + +Meteor.methods({ + 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', + action: 'Accessing_permissions', + }); + } + + const role = await Roles.findOne(roleName); + if (!role) { + throw new Meteor.Error('error-invalid-role', 'Invalid role', { + method: 'authorization:deleteRole', + }); + } + + if (role.protected) { + throw new Meteor.Error('error-delete-protected-role', 'Cannot delete a protected role', { + method: 'authorization:deleteRole', + }); + } + + const users = await(await Roles.findUsersInRole(roleName)).count(); + + if (users > 0) { + throw new Meteor.Error('error-role-in-use', 'Cannot delete role because it\'s in use', { + method: 'authorization:deleteRole', + }); + } + + return Roles.removeById(role.name); + }, +}); diff --git a/app/authorization/server/methods/removeRoleFromPermission.js b/app/authorization/server/methods/removeRoleFromPermission.js deleted file mode 100644 index e0aa20ed34dbb..0000000000000 --- a/app/authorization/server/methods/removeRoleFromPermission.js +++ /dev/null @@ -1,27 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Permissions } from '../../../models/server'; -import { hasPermission } from '../functions/hasPermission'; -import { CONSTANTS } from '../../lib'; - -Meteor.methods({ - 'authorization:removeRoleFromPermission'(permissionId, role) { - const uid = Meteor.userId(); - const permission = Permissions.findOneById(permissionId); - - 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', { - method: 'authorization:removeRoleFromPermission', - action: 'Removing_permission', - }); - } - - // for setting based permissions, revoke the group permission once all setting permissions - // related to this group have been removed - - if (permission.groupPermissionId) { - Permissions.removeRole(permission.groupPermissionId, role); - } - Permissions.removeRole(permission._id, role); - }, -}); diff --git a/app/authorization/server/methods/removeRoleFromPermission.ts b/app/authorization/server/methods/removeRoleFromPermission.ts new file mode 100644 index 0000000000000..c31592a0ceca6 --- /dev/null +++ b/app/authorization/server/methods/removeRoleFromPermission.ts @@ -0,0 +1,32 @@ +import { Meteor } from 'meteor/meteor'; + +import { hasPermission } from '../functions/hasPermission'; +import { CONSTANTS } from '../../lib'; +import { Permissions } from '../../../models/server/raw'; + +Meteor.methods({ + async 'authorization:removeRoleFromPermission'(permissionId, role) { + const uid = Meteor.userId(); + 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', { + method: 'authorization:removeRoleFromPermission', + action: 'Removing_permission', + }); + } + + // for setting based permissions, revoke the group permission once all setting permissions + // related to this group have been removed + + if (permission.groupPermissionId) { + Permissions.removeRole(permission.groupPermissionId, role); + } + Permissions.removeRole(permission._id, role); + }, +}); diff --git a/app/authorization/server/methods/removeUserFromRole.js b/app/authorization/server/methods/removeUserFromRole.js index 9a36a8895870c..d98ff825af9b8 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.js deleted file mode 100644 index 5e09f211240d7..0000000000000 --- a/app/authorization/server/methods/saveRole.js +++ /dev/null @@ -1,36 +0,0 @@ -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'; - -Meteor.methods({ - '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', - action: 'Accessing_permissions', - }); - } - - if (!roleData.name) { - throw new Meteor.Error('error-role-name-required', 'Role name is required', { - method: 'authorization:saveRole', - }); - } - - if (['Users', 'Subscriptions'].includes(roleData.scope) === false) { - roleData.scope = 'Users'; - } - - const update = Roles.createOrUpdate(roleData.name, roleData.scope, roleData.description, false, roleData.mandatory2fa); - if (settings.get('UI_DisplayRoles')) { - api.broadcast('user.roleUpdate', { - type: 'changed', - _id: roleData.name, - }); - } - return update; - }, -}); diff --git a/app/authorization/server/methods/saveRole.ts b/app/authorization/server/methods/saveRole.ts new file mode 100644 index 0000000000000..04f431ba9906e --- /dev/null +++ b/app/authorization/server/methods/saveRole.ts @@ -0,0 +1,36 @@ +import { Meteor } from 'meteor/meteor'; + +import { settings } from '../../../settings/server'; +import { hasPermission } from '../functions/hasPermission'; +import { api } from '../../../../server/sdk/api'; +import { Roles } from '../../../models/server/raw'; + +Meteor.methods({ + 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', + action: 'Accessing_permissions', + }); + } + + if (!roleData.name) { + throw new Meteor.Error('error-role-name-required', 'Role name is required', { + method: 'authorization:saveRole', + }); + } + + if (['Users', 'Subscriptions'].includes(roleData.scope) === false) { + roleData.scope = 'Users'; + } + + 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', + _id: roleData.name, + }); + } + return update; + }, +}); diff --git a/app/authorization/server/startup.js b/app/authorization/server/startup.js deleted file mode 100644 index b2d1c41cc9669..0000000000000 --- a/app/authorization/server/startup.js +++ /dev/null @@ -1,254 +0,0 @@ -/* eslint no-multi-spaces: 0 */ -import { Meteor } from 'meteor/meteor'; - -import { Roles, Permissions, Settings } from '../../models/server'; -import { settings } from '../../settings/server'; -import { getSettingPermissionId, CONSTANTS } from '../lib'; - -Meteor.startup(function() { - // 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 - // 2. admin, moderator, and user roles should not be deleted as they are referenced in the code. - const permissions = [ - { _id: 'access-permissions', roles: ['admin'] }, - { _id: 'access-setting-permissions', roles: ['admin'] }, - { _id: 'add-oauth-service', roles: ['admin'] }, - { _id: 'add-user-to-joined-room', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'add-user-to-any-c-room', roles: ['admin'] }, - { _id: 'add-user-to-any-p-room', roles: [] }, - { _id: 'api-bypass-rate-limit', roles: ['admin', 'bot', 'app'] }, - { _id: 'archive-room', roles: ['admin', 'owner'] }, - { _id: 'assign-admin-role', roles: ['admin'] }, - { _id: 'assign-roles', roles: ['admin'] }, - { _id: 'ban-user', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'bulk-register-user', roles: ['admin'] }, - { _id: 'change-livechat-room-visitor', roles: ['admin', 'livechat-manager', 'livechat-agent'] }, - { _id: 'create-c', roles: ['admin', 'user', 'bot', 'app'] }, - { _id: 'create-d', roles: ['admin', 'user', 'bot', 'app'] }, - { _id: 'create-p', roles: ['admin', 'user', 'bot', 'app'] }, - { _id: 'create-personal-access-tokens', roles: ['admin', 'user'] }, - { _id: 'create-user', roles: ['admin'] }, - { _id: 'clean-channel-history', roles: ['admin'] }, - { _id: 'delete-c', roles: ['admin', 'owner'] }, - { _id: 'delete-d', roles: ['admin'] }, - { _id: 'delete-message', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'delete-own-message', roles: ['admin', 'user'] }, - { _id: 'delete-p', roles: ['admin', 'owner'] }, - { _id: 'delete-user', roles: ['admin'] }, - { _id: 'edit-message', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'edit-other-user-active-status', roles: ['admin'] }, - { _id: 'edit-other-user-info', roles: ['admin'] }, - { _id: 'edit-other-user-password', roles: ['admin'] }, - { _id: 'edit-other-user-avatar', roles: ['admin'] }, - { _id: 'edit-other-user-e2ee', roles: ['admin'] }, - { _id: 'edit-other-user-totp', roles: ['admin'] }, - { _id: 'edit-privileged-setting', roles: ['admin'] }, - { _id: 'edit-room', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'edit-room-avatar', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'edit-room-retention-policy', roles: ['admin'] }, - { _id: 'force-delete-message', roles: ['admin', 'owner'] }, - { _id: 'join-without-join-code', roles: ['admin', 'bot', 'app'] }, - { _id: 'leave-c', roles: ['admin', 'user', 'bot', 'anonymous', 'app'] }, - { _id: 'leave-p', roles: ['admin', 'user', 'bot', 'anonymous', 'app'] }, - { _id: 'logout-other-user', roles: ['admin'] }, - { _id: 'manage-assets', roles: ['admin'] }, - { _id: 'manage-email-inbox', roles: ['admin'] }, - { _id: 'manage-emoji', roles: ['admin'] }, - { _id: 'manage-user-status', roles: ['admin'] }, - { _id: 'manage-outgoing-integrations', roles: ['admin'] }, - { _id: 'manage-incoming-integrations', roles: ['admin'] }, - { _id: 'manage-own-outgoing-integrations', roles: ['admin'] }, - { _id: 'manage-own-incoming-integrations', roles: ['admin'] }, - { _id: 'manage-oauth-apps', roles: ['admin'] }, - { _id: 'manage-selected-settings', roles: ['admin'] }, - { _id: 'mention-all', roles: ['admin', 'owner', 'moderator', 'user'] }, - { _id: 'mention-here', roles: ['admin', 'owner', 'moderator', 'user'] }, - { _id: 'mute-user', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'remove-user', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'run-import', roles: ['admin'] }, - { _id: 'run-migration', roles: ['admin'] }, - { _id: 'set-moderator', roles: ['admin', 'owner'] }, - { _id: 'set-owner', roles: ['admin', 'owner'] }, - { _id: 'send-many-messages', roles: ['admin', 'bot', 'app'] }, - { _id: 'set-leader', roles: ['admin', 'owner'] }, - { _id: 'unarchive-room', roles: ['admin'] }, - { _id: 'view-c-room', roles: ['admin', 'user', 'bot', 'app', 'anonymous'] }, - { _id: 'user-generate-access-token', roles: ['admin'] }, - { _id: 'view-d-room', roles: ['admin', 'user', 'bot', 'app', 'guest'] }, - { _id: 'view-full-other-user-info', roles: ['admin'] }, - { _id: 'view-history', roles: ['admin', 'user', 'anonymous'] }, - { _id: 'view-joined-room', roles: ['guest', 'bot', 'app', 'anonymous'] }, - { _id: 'view-join-code', roles: ['admin'] }, - { _id: 'view-logs', roles: ['admin'] }, - { _id: 'view-other-user-channels', roles: ['admin'] }, - { _id: 'view-p-room', roles: ['admin', 'user', 'anonymous', 'guest'] }, - { _id: 'view-privileged-setting', roles: ['admin'] }, - { _id: 'view-room-administration', roles: ['admin'] }, - { _id: 'view-statistics', roles: ['admin'] }, - { _id: 'view-user-administration', roles: ['admin'] }, - { _id: 'preview-c-room', roles: ['admin', 'user', 'anonymous'] }, - { _id: 'view-outside-room', roles: ['admin', 'owner', 'moderator', 'user'] }, - { _id: 'view-broadcast-member-list', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'call-management', roles: ['admin', 'owner', 'moderator'] }, - { _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: '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'] }, - { _id: 'close-others-livechat-room', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { _id: 'on-hold-livechat-room', roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'] }, - { _id: 'on-hold-others-livechat-room', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { _id: 'save-others-livechat-room-info', roles: ['livechat-manager', 'livechat-monitor'] }, - { _id: 'remove-closed-livechat-rooms', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { _id: 'view-livechat-analytics', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { _id: 'view-livechat-queue', roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'] }, - { _id: 'transfer-livechat-guest', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { _id: 'manage-livechat-managers', roles: ['livechat-manager', 'admin'] }, - { _id: 'manage-livechat-agents', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { _id: 'manage-livechat-departments', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { _id: 'view-livechat-departments', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { _id: 'add-livechat-department-agents', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { _id: 'view-livechat-current-chats', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { _id: 'view-livechat-real-time-monitoring', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { _id: 'view-livechat-triggers', roles: ['livechat-manager', 'admin'] }, - { _id: 'view-livechat-customfields', roles: ['livechat-manager', 'admin'] }, - { _id: 'view-livechat-installation', roles: ['livechat-manager', 'admin'] }, - { _id: 'view-livechat-appearance', roles: ['livechat-manager', 'admin'] }, - { _id: 'view-livechat-webhooks', roles: ['livechat-manager', 'admin'] }, - { _id: 'view-livechat-facebook', roles: ['livechat-manager', 'admin'] }, - { _id: 'view-livechat-business-hours', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { _id: 'view-livechat-room-closed-same-department', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { _id: 'view-livechat-room-closed-by-another-agent', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { _id: 'view-livechat-room-customfields', roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'] }, - { _id: 'edit-livechat-room-customfields', roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'] }, - { _id: 'send-omnichannel-chat-transcript', roles: ['livechat-manager', 'admin'] }, - { _id: 'mail-messages', roles: ['admin'] }, - { _id: 'toggle-room-e2e-encryption', roles: ['owner'] }, - { _id: 'message-impersonate', roles: ['bot', 'app'] }, - { _id: 'create-team', roles: ['admin', 'user'] }, - { _id: 'delete-team', roles: ['admin', 'owner'] }, - { _id: 'convert-team', roles: ['admin', 'owner'] }, - { _id: 'edit-team', roles: ['admin', 'owner'] }, - { _id: 'add-team-member', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'edit-team-member', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'add-team-channel', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'edit-team-channel', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'remove-team-channel', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'view-all-team-channels', roles: ['admin', 'owner'] }, - { _id: 'view-all-teams', roles: ['admin'] }, - { _id: 'remove-closed-livechat-room', roles: ['livechat-manager', 'admin'] }, - { _id: 'remove-livechat-department', roles: ['livechat-manager', 'admin'] }, - ]; - - for (const permission of permissions) { - Permissions.create(permission._id, permission.roles); - } - - const defaultRoles = [ - { name: 'admin', scope: 'Users', description: 'Admin' }, - { name: 'moderator', scope: 'Subscriptions', description: 'Moderator' }, - { name: 'leader', scope: 'Subscriptions', description: 'Leader' }, - { name: 'owner', scope: 'Subscriptions', description: 'Owner' }, - { name: 'user', scope: 'Users', description: '' }, - { name: 'bot', scope: 'Users', description: '' }, - { name: 'app', scope: 'Users', description: '' }, - { name: 'guest', scope: 'Users', description: '' }, - { name: 'anonymous', scope: 'Users', description: '' }, - { name: 'livechat-agent', scope: 'Users', description: 'Livechat Agent' }, - { name: 'livechat-manager', scope: 'Users', description: 'Livechat Manager' }, - ]; - - for (const role of defaultRoles) { - Roles.createOrUpdate(role.name, role.scope, role.description, true, false); - } - - const getPreviousPermissions = function(settingId) { - const previousSettingPermissions = {}; - - const selector = { level: CONSTANTS.SETTINGS_LEVEL }; - if (settingId) { - selector.settingId = settingId; - } - - Permissions.find(selector).fetch().forEach( - function(permission) { - previousSettingPermissions[permission._id] = permission; - }); - return previousSettingPermissions; - }; - - const createSettingPermission = function(setting, previousSettingPermissions) { - const permissionId = getSettingPermissionId(setting._id); - const permission = { - level: CONSTANTS.SETTINGS_LEVEL, - // copy those setting-properties which are needed to properly publish the setting-based permissions - settingId: setting._id, - group: setting.group, - section: setting.section, - sorter: setting.sorter, - roles: [], - }; - // copy previously assigned roles if available - if (previousSettingPermissions[permissionId] && previousSettingPermissions[permissionId].roles) { - permission.roles = previousSettingPermissions[permissionId].roles; - } - if (setting.group) { - permission.groupPermissionId = getSettingPermissionId(setting.group); - } - if (setting.section) { - permission.sectionPermissionId = getSettingPermissionId(setting.section); - } - - const existent = Permissions.findOne({ - _id: permissionId, - ...permission, - }, { fields: { _id: 1 } }); - - if (!existent) { - try { - Permissions.upsert({ _id: permissionId }, { $set: permission }); - } 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 }); - } - } - } - - delete previousSettingPermissions[permissionId]; - }; - - const createPermissionsForExistingSettings = function() { - const previousSettingPermissions = getPreviousPermissions(); - - Settings.findNotHidden().fetch().forEach((setting) => { - createSettingPermission(setting, previousSettingPermissions); - }); - - // remove permissions for non-existent settings - for (const obsoletePermission in previousSettingPermissions) { - if (previousSettingPermissions.hasOwnProperty(obsoletePermission)) { - Permissions.remove({ _id: obsoletePermission }); - } - } - }; - - // for each setting which already exists, create a permission to allow changing just this one setting - createPermissionsForExistingSettings(); - - // register a callback for settings for be create in higher-level-packages - const createPermissionForAddedSetting = function(settingId) { - const previousSettingPermissions = getPreviousPermissions(settingId); - const setting = Settings.findOneById(settingId); - if (setting) { - if (!setting.hidden) { - createSettingPermission(setting, previousSettingPermissions); - } - } - }; - - settings.onload('*', createPermissionForAddedSetting); -}); diff --git a/app/authorization/server/streamer/permissions/index.js b/app/authorization/server/streamer/permissions/index.js deleted file mode 100644 index edffbdfe3e734..0000000000000 --- 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 0000000000000..fcc3bad0e34cd --- /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/autolinker/server/settings.js b/app/autolinker/server/settings.js deleted file mode 100644 index 81e65d05c0d2e..0000000000000 --- a/app/autolinker/server/settings.js +++ /dev/null @@ -1,20 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(function() { - const enableQuery = { - _id: 'AutoLinker', - value: true, - }; - - settings.add('AutoLinker', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, i18nLabel: 'Enabled' }); - - settings.add('AutoLinker_StripPrefix', false, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, i18nDescription: 'AutoLinker_StripPrefix_Description', enableQuery }); - settings.add('AutoLinker_Urls_Scheme', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, enableQuery }); - settings.add('AutoLinker_Urls_www', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, enableQuery }); - settings.add('AutoLinker_Urls_TLD', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, enableQuery }); - settings.add('AutoLinker_UrlsRegExp', '(://|www\\.).+', { type: 'string', group: 'Message', section: 'AutoLinker', public: true, enableQuery }); - settings.add('AutoLinker_Email', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, enableQuery }); - settings.add('AutoLinker_Phone', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, i18nDescription: 'AutoLinker_Phone_Description', enableQuery }); -}); diff --git a/app/autolinker/server/settings.ts b/app/autolinker/server/settings.ts new file mode 100644 index 0000000000000..e8e9c60f1be47 --- /dev/null +++ b/app/autolinker/server/settings.ts @@ -0,0 +1,20 @@ +import { Meteor } from 'meteor/meteor'; + +import { settingsRegistry } from '../../settings/server'; + +Meteor.startup(function() { + const enableQuery = { + _id: 'AutoLinker', + value: true, + }; + + settingsRegistry.add('AutoLinker', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, i18nLabel: 'Enabled' }); + + settingsRegistry.add('AutoLinker_StripPrefix', false, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, i18nDescription: 'AutoLinker_StripPrefix_Description', enableQuery }); + settingsRegistry.add('AutoLinker_Urls_Scheme', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, enableQuery }); + settingsRegistry.add('AutoLinker_Urls_www', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, enableQuery }); + settingsRegistry.add('AutoLinker_Urls_TLD', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, enableQuery }); + settingsRegistry.add('AutoLinker_UrlsRegExp', '(://|www\\.).+', { type: 'string', group: 'Message', section: 'AutoLinker', public: true, enableQuery }); + settingsRegistry.add('AutoLinker_Email', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, enableQuery }); + settingsRegistry.add('AutoLinker_Phone', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, i18nDescription: 'AutoLinker_Phone_Description', enableQuery }); +}); diff --git a/app/autotranslate/client/lib/autotranslate.js b/app/autotranslate/client/lib/autotranslate.js index f711bc383a4a6..1b742b21010b8 100644 --- a/app/autotranslate/client/lib/autotranslate.js +++ b/app/autotranslate/client/lib/autotranslate.js @@ -5,7 +5,7 @@ import mem from 'mem'; import { Subscriptions, Messages } from '../../../models'; import { hasPermission } from '../../../authorization'; -import { call } from '../../../ui-utils/client'; +import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; let userLanguage = 'en'; let username = ''; @@ -76,8 +76,8 @@ export const AutoTranslate = { c.stop(); [this.providersMetadata, this.supportedLanguages] = await Promise.all([ - call('autoTranslate.getProviderUiMetadata'), - call('autoTranslate.getSupportedLanguages', 'en'), + callWithErrorHandling('autoTranslate.getProviderUiMetadata'), + callWithErrorHandling('autoTranslate.getSupportedLanguages', 'en'), ]); }); diff --git a/app/autotranslate/server/autotranslate.js b/app/autotranslate/server/autotranslate.js index 12e3f61ac9436..152b8981b5dba 100644 --- a/app/autotranslate/server/autotranslate.js +++ b/app/autotranslate/server/autotranslate.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { escapeHTML } from '@rocket.chat/string-helpers'; -import { settings } from '../../settings'; +import { settings } from '../../settings/server'; import { callbacks } from '../../callbacks'; import { Subscriptions, Messages } from '../../models'; import { Markdown } from '../../markdown/server'; @@ -80,24 +80,6 @@ export class TranslationProviderRegistry { callbacks.add('afterSaveMessage', provider.translateMessage.bind(provider), callbacks.priority.MEDIUM, 'autotranslate'); } - - /** - * Make the activated provider by setting as the active. - */ - static loadActiveServiceProvider() { - /** Register the active service provider on the 'AfterSaveMessage' callback. - * So the registered provider will be invoked when a message is saved. - * All the other inactive service provider must be deactivated. - */ - settings.get('AutoTranslate_ServiceProvider', (key, providerName) => { - TranslationProviderRegistry.setCurrentProvider(providerName); - }); - - // Get Auto Translate Active flag - settings.get('AutoTranslate_Enabled', (key, value) => { - TranslationProviderRegistry.setEnable(value); - }); - } } /** @@ -352,5 +334,16 @@ export class AutoTranslate { } Meteor.startup(() => { - TranslationProviderRegistry.loadActiveServiceProvider(); + /** Register the active service provider on the 'AfterSaveMessage' callback. + * So the registered provider will be invoked when a message is saved. + * All the other inactive service provider must be deactivated. + */ + settings.watch('AutoTranslate_ServiceProvider', (providerName) => { + TranslationProviderRegistry.setCurrentProvider(providerName); + }); + + // Get Auto Translate Active flag + settings.watch('AutoTranslate_Enabled', (value) => { + TranslationProviderRegistry.setEnable(value); + }); }); diff --git a/app/autotranslate/server/deeplTranslate.js b/app/autotranslate/server/deeplTranslate.js index 91145470cf551..996d18d947839 100644 --- a/app/autotranslate/server/deeplTranslate.js +++ b/app/autotranslate/server/deeplTranslate.js @@ -7,7 +7,7 @@ import { HTTP } from 'meteor/http'; import _ from 'underscore'; import { TranslationProviderRegistry, AutoTranslate } from './autotranslate'; -import { SystemLogger } from '../../logger/server'; +import { SystemLogger } from '../../../server/lib/logger/system'; import { settings } from '../../settings'; /** @@ -28,7 +28,7 @@ class DeeplAutoTranslate extends AutoTranslate { this.name = 'deepl-translate'; this.apiEndPointUrl = 'https://api.deepl.com/v2/translate'; // Get the service provide API key. - settings.get('AutoTranslate_DeepLAPIKey', (key, value) => { + settings.watch('AutoTranslate_DeepLAPIKey', (value) => { this.apiKey = value; }); } diff --git a/app/autotranslate/server/googleTranslate.js b/app/autotranslate/server/googleTranslate.js index b2463718e6f55..24a70c3257e74 100644 --- a/app/autotranslate/server/googleTranslate.js +++ b/app/autotranslate/server/googleTranslate.js @@ -7,8 +7,8 @@ import { HTTP } from 'meteor/http'; import _ from 'underscore'; import { AutoTranslate, TranslationProviderRegistry } from './autotranslate'; -import { SystemLogger } from '../../logger/server'; -import { settings } from '../../settings'; +import { SystemLogger } from '../../../server/lib/logger/system'; +import { settings } from '../../settings/server'; /** * Represents google translate class @@ -26,7 +26,7 @@ class GoogleAutoTranslate extends AutoTranslate { this.name = 'google-translate'; this.apiEndPointUrl = 'https://translation.googleapis.com/language/translate/v2'; // Get the service provide API key. - settings.get('AutoTranslate_GoogleAPIKey', (key, value) => { + settings.watch('AutoTranslate_GoogleAPIKey', (value) => { this.apiKey = value; }); } diff --git a/app/autotranslate/server/logger.js b/app/autotranslate/server/logger.js index 8f104e75e88c6..70ca0cbf97b6a 100644 --- a/app/autotranslate/server/logger.js +++ b/app/autotranslate/server/logger.js @@ -1,9 +1,5 @@ -import { Logger } from '../../logger'; +import { Logger } from '../../logger/server'; -export const logger = new Logger('AutoTranslate', { - sections: { - google: 'Google', - deepl: 'DeepL', - microsoft: 'Microsoft', - }, -}); +const logger = new Logger('AutoTranslate'); + +export const msLogger = logger.section('Microsoft'); diff --git a/app/autotranslate/server/msTranslate.js b/app/autotranslate/server/msTranslate.js index a71d450fa72da..eb909a3792483 100644 --- a/app/autotranslate/server/msTranslate.js +++ b/app/autotranslate/server/msTranslate.js @@ -7,8 +7,8 @@ import { HTTP } from 'meteor/http'; import _ from 'underscore'; import { TranslationProviderRegistry, AutoTranslate } from './autotranslate'; -import { logger } from './logger'; -import { settings } from '../../settings'; +import { msLogger } from './logger'; +import { settings } from '../../settings/server'; /** * Microsoft translation service provider class representation. @@ -31,7 +31,7 @@ class MsAutoTranslate extends AutoTranslate { this.apiGetLanguages = 'https://api.cognitive.microsofttranslator.com/languages?api-version=3.0'; this.breakSentence = 'https://api.cognitive.microsofttranslator.com/breaksentence?api-version=3.0'; // Get the service provide API key. - settings.get('AutoTranslate_MicrosoftAPIKey', (key, value) => { + settings.watch('AutoTranslate_MicrosoftAPIKey', (value) => { this.apiKey = value; }); } @@ -137,7 +137,7 @@ class MsAutoTranslate extends AutoTranslate { try { return this._translate(msgs, targetLanguages); } catch (e) { - logger.microsoft.error('Error translating message', e); + msLogger.error({ err: e, msg: 'Error translating message' }); } return {}; } @@ -155,7 +155,7 @@ class MsAutoTranslate extends AutoTranslate { Text: attachment.description || attachment.text, }], targetLanguages); } catch (e) { - logger.microsoft.error('Error translating message attachment', e); + msLogger.error({ err: e, msg: 'Error translating message attachment' }); } return {}; } diff --git a/app/autotranslate/server/permissions.js b/app/autotranslate/server/permissions.js deleted file mode 100644 index 64ce0028fa872..0000000000000 --- 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 0000000000000..5ce05e8f1ef72 --- /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/autotranslate/server/settings.js b/app/autotranslate/server/settings.js deleted file mode 100644 index d58f5a9b3533b..0000000000000 --- a/app/autotranslate/server/settings.js +++ /dev/null @@ -1,74 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(function() { - settings.add('AutoTranslate_Enabled', false, { - type: 'boolean', - group: 'Message', - section: 'AutoTranslate', - public: true, - }); - - settings.add('AutoTranslate_ServiceProvider', 'google-translate', { - type: 'select', - group: 'Message', - section: 'AutoTranslate', - values: [{ - key: 'google-translate', - i18nLabel: 'AutoTranslate_Google', - }, { - key: 'deepl-translate', - i18nLabel: 'AutoTranslate_DeepL', - }, { - key: 'microsoft-translate', - i18nLabel: 'AutoTranslate_Microsoft', - }], - enableQuery: [{ _id: 'AutoTranslate_Enabled', value: true }], - i18nLabel: 'AutoTranslate_ServiceProvider', - public: true, - }); - - settings.add('AutoTranslate_GoogleAPIKey', '', { - type: 'string', - group: 'Message', - section: 'AutoTranslate_Google', - public: false, - i18nLabel: 'AutoTranslate_APIKey', - enableQuery: [ - { - _id: 'AutoTranslate_Enabled', value: true, - }, - { - _id: 'AutoTranslate_ServiceProvider', value: 'google-translate', - }], - }); - - settings.add('AutoTranslate_DeepLAPIKey', '', { - type: 'string', - group: 'Message', - section: 'AutoTranslate_DeepL', - public: false, - i18nLabel: 'AutoTranslate_APIKey', - enableQuery: [ - { - _id: 'AutoTranslate_Enabled', value: true, - }, { - _id: 'AutoTranslate_ServiceProvider', value: 'deepl-translate', - }], - }); - - settings.add('AutoTranslate_MicrosoftAPIKey', '', { - type: 'string', - group: 'Message', - section: 'AutoTranslate_Microsoft', - public: false, - i18nLabel: 'AutoTranslate_Microsoft_API_Key', - enableQuery: [ - { - _id: 'AutoTranslate_Enabled', value: true, - }, { - _id: 'AutoTranslate_ServiceProvider', value: 'microsoft-translate', - }], - }); -}); diff --git a/app/autotranslate/server/settings.ts b/app/autotranslate/server/settings.ts new file mode 100644 index 0000000000000..e250f885d8542 --- /dev/null +++ b/app/autotranslate/server/settings.ts @@ -0,0 +1,74 @@ +import { Meteor } from 'meteor/meteor'; + +import { settingsRegistry } from '../../settings/server'; + +Meteor.startup(function() { + settingsRegistry.add('AutoTranslate_Enabled', false, { + type: 'boolean', + group: 'Message', + section: 'AutoTranslate', + public: true, + }); + + settingsRegistry.add('AutoTranslate_ServiceProvider', 'google-translate', { + type: 'select', + group: 'Message', + section: 'AutoTranslate', + values: [{ + key: 'google-translate', + i18nLabel: 'AutoTranslate_Google', + }, { + key: 'deepl-translate', + i18nLabel: 'AutoTranslate_DeepL', + }, { + key: 'microsoft-translate', + i18nLabel: 'AutoTranslate_Microsoft', + }], + enableQuery: [{ _id: 'AutoTranslate_Enabled', value: true }], + i18nLabel: 'AutoTranslate_ServiceProvider', + public: true, + }); + + settingsRegistry.add('AutoTranslate_GoogleAPIKey', '', { + type: 'string', + group: 'Message', + section: 'AutoTranslate_Google', + public: false, + i18nLabel: 'AutoTranslate_APIKey', + enableQuery: [ + { + _id: 'AutoTranslate_Enabled', value: true, + }, + { + _id: 'AutoTranslate_ServiceProvider', value: 'google-translate', + }], + }); + + settingsRegistry.add('AutoTranslate_DeepLAPIKey', '', { + type: 'string', + group: 'Message', + section: 'AutoTranslate_DeepL', + public: false, + i18nLabel: 'AutoTranslate_APIKey', + enableQuery: [ + { + _id: 'AutoTranslate_Enabled', value: true, + }, { + _id: 'AutoTranslate_ServiceProvider', value: 'deepl-translate', + }], + }); + + settingsRegistry.add('AutoTranslate_MicrosoftAPIKey', '', { + type: 'string', + group: 'Message', + section: 'AutoTranslate_Microsoft', + public: false, + i18nLabel: 'AutoTranslate_Microsoft_API_Key', + enableQuery: [ + { + _id: 'AutoTranslate_Enabled', value: true, + }, { + _id: 'AutoTranslate_ServiceProvider', value: 'microsoft-translate', + }], + }); +}); diff --git a/app/bigbluebutton/server/bigbluebutton-api.js b/app/bigbluebutton/server/bigbluebutton-api.js index b90424914736e..8cb3f4d447c46 100644 --- a/app/bigbluebutton/server/bigbluebutton-api.js +++ b/app/bigbluebutton/server/bigbluebutton-api.js @@ -1,5 +1,6 @@ /* eslint-disable */ import crypto from 'crypto'; +import { SystemLogger } from '../../../server/lib/logger/system'; var BigBlueButtonApi, filterCustomParameters, include, noChecksumMethods, __indexOf = [].indexOf || function (item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; @@ -7,15 +8,11 @@ var BigBlueButtonApi, filterCustomParameters, include, noChecksumMethods, BigBlueButtonApi = (function () { function BigBlueButtonApi(url, salt, debug, opts) { var _base; - if (debug == null) { - debug = false; - } if (opts == null) { opts = {}; } this.url = url; this.salt = salt; - this.debug = debug; this.opts = opts; if ((_base = this.opts).shaType == null) { _base.shaType = 'sha1'; @@ -82,9 +79,7 @@ BigBlueButtonApi = (function () { if (filter == null) { filter = true; } - if (this.debug) { - console.log("Generating URL for", method); - } + SystemLogger.debug("Generating URL for", method); if (filter) { params = this.filterParams(params, method); } else { @@ -132,9 +127,7 @@ BigBlueButtonApi = (function () { BigBlueButtonApi.prototype.checksum = function (method, query) { var c, shaObj, str; query || (query = ""); - if (this.debug) { - console.log("- Calculating the checksum using: '" + method + "', '" + query + "', '" + this.salt + "'"); - } + SystemLogger.debug("- Calculating the checksum using: '" + method + "', '" + query + "', '" + this.salt + "'"); str = method + query + this.salt; if (this.opts.shaType === 'sha256') { shaObj = crypto.createHash('sha256', "TEXT") @@ -143,9 +136,7 @@ BigBlueButtonApi = (function () { } shaObj.update(str); c = shaObj.digest('hex'); - if (this.debug) { - console.log("- Checksum calculated:", c); - } + SystemLogger.debug("- Checksum calculated:", c); return c; }; diff --git a/app/blockstack/server/loginHandler.js b/app/blockstack/server/loginHandler.js index de00f0d1ccf4d..90ba370756d19 100644 --- a/app/blockstack/server/loginHandler.js +++ b/app/blockstack/server/loginHandler.js @@ -4,7 +4,7 @@ import { Accounts } from 'meteor/accounts-base'; import { updateOrCreateUser } from './userHandler'; import { handleAccessToken } from './tokenHandler'; import { logger } from './logger'; -import { settings } from '../../settings'; +import { settings } from '../../settings/server'; import { Users } from '../../models'; import { setUserAvatar } from '../../lib'; @@ -44,7 +44,7 @@ Accounts.registerLoginHandler('blockstack', (loginRequest) => { }); } } catch (e) { - console.error(e); + logger.error(e); } } diff --git a/app/blockstack/server/routes.js b/app/blockstack/server/routes.js index 81902030e4266..eb6947113deeb 100644 --- a/app/blockstack/server/routes.js +++ b/app/blockstack/server/routes.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; -import { settings } from '../../settings'; +import { settings } from '../../settings/server'; import { RocketChatAssets } from '../../assets/server'; WebApp.connectHandlers.use('/_blockstack/manifest', Meteor.bindEnvironment(function(req, res) { diff --git a/app/blockstack/server/settings.js b/app/blockstack/server/settings.js index f5e79db649757..d61fbb1b749ff 100644 --- a/app/blockstack/server/settings.js +++ b/app/blockstack/server/settings.js @@ -1,9 +1,8 @@ -import _ from 'underscore'; import { Meteor } from 'meteor/meteor'; import { ServiceConfiguration } from 'meteor/service-configuration'; import { logger } from './logger'; -import { settings } from '../../settings'; +import { settings, settingsRegistry } from '../../settings/server'; const defaults = { enable: false, @@ -18,7 +17,7 @@ const defaults = { }; Meteor.startup(() => { - settings.addGroup('Blockstack', function() { + settingsRegistry.addGroup('Blockstack', function() { this.add('Blockstack_Enable', defaults.enable, { type: 'boolean', i18nLabel: 'Enable', @@ -43,7 +42,12 @@ const getSettings = () => Object.assign({}, defaults, { generateUsername: settings.get('Blockstack_Generate_Username'), }); -const configureService = _.debounce(Meteor.bindEnvironment(() => { + +// Add settings to auth provider configs on startup +settings.watchMultiple(['Blockstack_Enable', + 'Blockstack_Auth_Description', + 'Blockstack_ButtonLabelText', + 'Blockstack_Generate_Username'], () => { const serviceConfig = getSettings(); if (!serviceConfig.enable) { @@ -60,11 +64,4 @@ const configureService = _.debounce(Meteor.bindEnvironment(() => { }); logger.debug('Init Blockstack auth', serviceConfig); -}), 1000); - -// Add settings to auth provider configs on startup -Meteor.startup(() => { - settings.get(/^Blockstack_.+/, () => { - configureService(); - }); }); diff --git a/app/bot-helpers/server/index.js b/app/bot-helpers/server/index.js index e88fef7641d06..dd6903c4b4f0e 100644 --- a/app/bot-helpers/server/index.js +++ b/app/bot-helpers/server/index.js @@ -2,9 +2,9 @@ import './settings'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { Users, Rooms } from '../../models'; -import { settings } from '../../settings'; -import { hasRole } from '../../authorization'; +import { Users, Rooms } from '../../models/server'; +import { settings } from '../../settings/server'; +import { hasRole } from '../../authorization/server'; /** * BotHelpers helps bots @@ -151,7 +151,7 @@ class BotHelpers { const botHelpers = new BotHelpers(); // init cursors with fields setting and update on setting change -settings.get('BotHelpers_userFields', function(settingKey, settingValue) { +settings.watch('BotHelpers_userFields', function(settingValue) { botHelpers.setupCursors(settingValue); }); diff --git a/app/bot-helpers/server/settings.js b/app/bot-helpers/server/settings.js deleted file mode 100644 index dc4f5640c940b..0000000000000 --- a/app/bot-helpers/server/settings.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(function() { - settings.addGroup('Bots', function() { - this.add('BotHelpers_userFields', '_id, name, username, emails, language, utcOffset', { - type: 'string', - section: 'Helpers', - i18nLabel: 'BotHelpers_userFields', - i18nDescription: 'BotHelpers_userFields_Description', - }); - }); -}); diff --git a/app/bot-helpers/server/settings.ts b/app/bot-helpers/server/settings.ts new file mode 100644 index 0000000000000..1a5891211b347 --- /dev/null +++ b/app/bot-helpers/server/settings.ts @@ -0,0 +1,14 @@ +import { Meteor } from 'meteor/meteor'; + +import { settingsRegistry } from '../../settings/server'; + +Meteor.startup(function() { + settingsRegistry.addGroup('Bots', function() { + this.add('BotHelpers_userFields', '_id, name, username, emails, language, utcOffset', { + type: 'string', + section: 'Helpers', + i18nLabel: 'BotHelpers_userFields', + i18nDescription: 'BotHelpers_userFields_Description', + }); + }); +}); diff --git a/app/callbacks/lib/callbacks.js b/app/callbacks/lib/callbacks.js index 525d869e191bd..e91c1af38831f 100644 --- a/app/callbacks/lib/callbacks.js +++ b/app/callbacks/lib/callbacks.js @@ -11,12 +11,12 @@ let logger = { }; if (Meteor.isClient) { - const { getConfig } = require('../../ui-utils/client/config'); + const { getConfig } = require('../../../client/lib/utils/getConfig'); timed = [getConfig('debug'), getConfig('timed-callbacks')].includes('true'); } if (Meteor.isServer) { - const { Logger } = require('../../logger/server/server'); + const { Logger } = require('../../../server/lib/logger/Logger'); logger = new Logger('Callbacks'); } diff --git a/app/cas/server/cas_rocketchat.js b/app/cas/server/cas_rocketchat.js index 47dd4b405e253..2c8989af060f8 100644 --- a/app/cas/server/cas_rocketchat.js +++ b/app/cas/server/cas_rocketchat.js @@ -2,12 +2,12 @@ import { Meteor } from 'meteor/meteor'; import { ServiceConfiguration } from 'meteor/service-configuration'; import { Logger } from '../../logger'; -import { settings } from '../../settings'; +import { settings, settingsRegistry } from '../../settings/server'; -export const logger = new Logger('CAS', {}); +export const logger = new Logger('CAS'); Meteor.startup(function() { - settings.addGroup('CAS', function() { + settingsRegistry.addGroup('CAS', function() { this.add('CAS_enabled', false, { type: 'boolean', group: 'CAS', public: true }); this.add('CAS_base_url', '', { type: 'string', group: 'CAS', public: true }); this.add('CAS_login_url', '', { type: 'string', group: 'CAS', public: true }); @@ -67,6 +67,6 @@ function updateServices(/* record*/) { }, 2000); } -settings.get(/^CAS_.+/, (key, value) => { +settings.watchByRegex(/^CAS_.+/, (key, value) => { updateServices(value); }); diff --git a/app/cas/server/cas_server.js b/app/cas/server/cas_server.js index 646e87a8f0539..cc569eeab4419 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/saveReactWhenReadOnly.js b/app/channel-settings/server/functions/saveReactWhenReadOnly.js index 97d20782b2356..115d98b805232 100644 --- a/app/channel-settings/server/functions/saveReactWhenReadOnly.js +++ b/app/channel-settings/server/functions/saveReactWhenReadOnly.js @@ -1,12 +1,18 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Rooms } from '../../../models'; +import { Rooms, Messages } from '../../../models'; -export const saveReactWhenReadOnly = function(rid, allowReact) { +export const saveReactWhenReadOnly = function(rid, allowReact, user, sendMessage = true) { if (!Match.test(rid, String)) { throw new Meteor.Error('invalid-room', 'Invalid room', { function: 'RocketChat.saveReactWhenReadOnly' }); } - return Rooms.setAllowReactingWhenReadOnlyById(rid, allowReact); + const result = Rooms.setAllowReactingWhenReadOnlyById(rid, allowReact); + + if (result && sendMessage) { + allowReact ? Messages.createRoomAllowedReactingByRoomIdAndUser(rid, user) + : Messages.createRoomDisallowedReactingByRoomIdAndUser(rid, user); + } + return result; }; diff --git a/app/channel-settings/server/functions/saveRoomName.js b/app/channel-settings/server/functions/saveRoomName.js index 5d3197d133b35..0cc31cfa77b44 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/functions/saveRoomReadOnly.js b/app/channel-settings/server/functions/saveRoomReadOnly.js index baf8dff166bf6..86e62061bf8c1 100644 --- a/app/channel-settings/server/functions/saveRoomReadOnly.js +++ b/app/channel-settings/server/functions/saveRoomReadOnly.js @@ -1,14 +1,20 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import { Rooms } from '../../../models'; +import { Rooms, Messages } from '../../../models'; import { hasPermission } from '../../../authorization'; -export const saveRoomReadOnly = function(rid, readOnly) { +export const saveRoomReadOnly = function(rid, readOnly, user, sendMessage = true) { if (!Match.test(rid, String)) { throw new Meteor.Error('invalid-room', 'Invalid room', { function: 'RocketChat.saveRoomReadOnly', }); } - return Rooms.setReadOnlyById(rid, readOnly, hasPermission); + const result = Rooms.setReadOnlyById(rid, readOnly, hasPermission); + + if (result && sendMessage) { + readOnly ? Messages.createRoomSetReadOnlyByRoomIdAndUser(rid, user) + : Messages.createRoomRemovedReadOnlyByRoomIdAndUser(rid, user); + } + return result; }; diff --git a/app/channel-settings/server/index.js b/app/channel-settings/server/index.js index abbd2bf327fff..032eaa2905c98 100644 --- a/app/channel-settings/server/index.js +++ b/app/channel-settings/server/index.js @@ -1,4 +1,3 @@ -import './startup'; import './methods/saveRoomSettings'; export { saveRoomTopic } from './functions/saveRoomTopic'; diff --git a/app/channel-settings/server/methods/saveRoomSettings.js b/app/channel-settings/server/methods/saveRoomSettings.js index 811c492fb70d4..59c0bb239f791 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/channel-settings/server/startup.js b/app/channel-settings/server/startup.js deleted file mode 100644 index 5e9aeb7baaf2f..0000000000000 --- a/app/channel-settings/server/startup.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Permissions } from '../../models'; - -Meteor.startup(function() { - Permissions.create('post-readonly', ['admin', 'owner', 'moderator']); - Permissions.create('set-readonly', ['admin', 'owner']); - Permissions.create('set-react-when-readonly', ['admin', 'owner']); -}); diff --git a/app/chatpal-search/client/style.css b/app/chatpal-search/client/style.css index 890e3c6891fb1..cc704ea30b0d9 100644 --- a/app/chatpal-search/client/style.css +++ b/app/chatpal-search/client/style.css @@ -1,5 +1,4 @@ .chatpal-admin-link { - text-decoration: underline !important; color: red !important; @@ -39,7 +38,6 @@ } .chatpal-search-typefilter li { - display: flex; flex: 0 0 33%; @@ -57,7 +55,6 @@ } .chatpal-admin-header { - margin-bottom: 20px; font-size: 18px; @@ -70,7 +67,6 @@ } .chatpal-search-result-single { - position: relative; min-height: 92px; @@ -84,7 +80,6 @@ } .chatpal-search-result-user { - position: relative; min-height: 40px; @@ -98,7 +93,6 @@ } .chatpal-search-result-user .chatpal-avatar { - position: absolute; width: 36px; @@ -106,7 +100,6 @@ } .chatpal-search-result-user .chatpal-avatar .chatpal-avatar-image { - width: 100%; height: 100%; @@ -135,7 +128,6 @@ } .chatpal-show-more-messages { - margin-bottom: 20px; cursor: pointer; @@ -161,7 +153,6 @@ } .chatpal-search-result-single h2 { - display: flex; margin-bottom: 20px; @@ -169,7 +160,6 @@ } .chatpal-search-result-single .chatpal-avatar { - position: absolute; width: 36px; @@ -177,7 +167,6 @@ } .chatpal-search-result-single .chatpal-avatar .chatpal-avatar-image { - width: 100%; height: 100%; @@ -196,7 +185,6 @@ } .chatpal-search-result-single .chatpal-date { - color: #a0a0a0; font-size: 12px; @@ -205,7 +193,6 @@ } .chatpal-search-result-single .chatpal-time { - margin-left: 3px; color: #a0a0a0; @@ -214,7 +201,6 @@ } .chatpal-search-result-single .chatpal-message { - overflow-x: hidden; margin-top: 5px; @@ -224,7 +210,6 @@ } .chatpal-search-result-single .chatpal-message em { - background-color: #faf9c8; font-style: normal; @@ -246,14 +231,12 @@ } .chatpal-paging { - margin: 30px 0 50px; text-align: center; } .chatpal-paging-text { - position: relative; top: -2px; @@ -261,7 +244,6 @@ } .chatpal-paging .chatpal-paging-button { - display: inline-block; cursor: pointer; @@ -285,7 +267,6 @@ } .chatpal-search-welcome { - padding-top: 40px; text-align: center; @@ -296,7 +277,6 @@ } .chatpal-search-result-list em { - background-color: #faf9c8; font-style: normal; @@ -319,7 +299,6 @@ } .chatpal-search-pills div { - display: inline-block; margin-top: 5px; @@ -337,7 +316,6 @@ } .apikey .key { - position: relative; margin: 20px 0; @@ -356,7 +334,6 @@ } .chatpal-suggestion { - display: flex; padding: 10px; diff --git a/app/chatpal-search/client/template/admin.js b/app/chatpal-search/client/template/admin.js index 625d8c4b2e0fa..703fabc2e7694 100644 --- a/app/chatpal-search/client/template/admin.js +++ b/app/chatpal-search/client/template/admin.js @@ -2,10 +2,10 @@ import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import toastr from 'toastr'; import { settings } from '../../../settings'; import { hasRole } from '../../../authorization'; +import { dispatchToastMessage } from '../../../../client/lib/toast'; Template.ChatpalAdmin.onCreated(function() { const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; @@ -32,22 +32,22 @@ Template.ChatpalAdmin.events({ const email = e.target.email.value; const tac = e.target.readtac.checked; - if (!tac) { return toastr.error(TAPi18n.__('Chatpal_ERROR_TAC_must_be_checked')); } - if (!email || email === '') { return toastr.error(TAPi18n.__('Chatpal_ERROR_Email_must_be_set')); } - if (!t.validateEmail(email)) { return toastr.error(TAPi18n.__('Chatpal_ERROR_Email_must_be_valid')); } + if (!tac) { return dispatchToastMessage({ type: 'error', message: TAPi18n.__('Chatpal_ERROR_TAC_must_be_checked') }); } + if (!email || email === '') { return dispatchToastMessage({ type: 'error', message: TAPi18n.__('Chatpal_ERROR_Email_must_be_set') }); } + if (!t.validateEmail(email)) { return dispatchToastMessage({ type: 'error', message: TAPi18n.__('Chatpal_ERROR_Email_must_be_valid') }); } // TODO register try { Meteor.call('chatpalUtilsCreateKey', email, (err, key) => { - if (!key) { return toastr.error(TAPi18n.__('Chatpal_ERROR_username_already_exists')); } + if (!key) { return dispatchToastMessage({ type: 'error', message: TAPi18n.__('Chatpal_ERROR_username_already_exists') }); } - toastr.info(TAPi18n.__('Chatpal_created_key_successfully')); + dispatchToastMessage({ type: 'info', message: TAPi18n.__('Chatpal_created_key_successfully') }); t.apiKey.set(key); }); } catch (e) { console.log(e); - toastr.error(TAPi18n.__('Chatpal_ERROR_username_already_exists'));// TODO error messages + dispatchToastMessage({ type: 'error', message: TAPi18n.__('Chatpal_ERROR_username_already_exists') });// TODO error messages } }, }); diff --git a/app/chatpal-search/client/template/result.js b/app/chatpal-search/client/template/result.js index 2468ecc30c9f1..1ed9a1bcafd17 100644 --- a/app/chatpal-search/client/template/result.js +++ b/app/chatpal-search/client/template/result.js @@ -2,10 +2,11 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { DateFormat } from '../../../lib'; import { roomTypes, getURL } from '../../../utils'; import { Subscriptions } from '../../../models'; import { getUserAvatarURL as getAvatarUrl } from '../../../utils/lib/getUserAvatarURL'; +import { formatTime } from '../../../../client/lib/utils/formatTime'; +import { formatDate } from '../../../../client/lib/utils/formatDate'; const getDMUrl = (username) => getURL(`/direct/${ username }`); @@ -116,10 +117,10 @@ Template.ChatpalSearchSingleMessage.helpers({ }, time() { - return DateFormat.formatTime(this.created); + return formatTime(this.created); }, date() { - return DateFormat.formatDate(this.created); + return formatDate(this.created); }, getAvatarUrl, }); diff --git a/app/chatpal-search/server/provider/index.js b/app/chatpal-search/server/provider/index.js index 4195805d53b7e..9a3ad7c56f751 100644 --- a/app/chatpal-search/server/provider/index.js +++ b/app/chatpal-search/server/provider/index.js @@ -29,13 +29,13 @@ class Backend { const response = HTTP.call('POST', `${ this._options.baseurl }${ this._options.updatepath }`, options); if (response.statusCode >= 200 && response.statusCode < 300) { - ChatpalLogger.debug(`indexed ${ docs.length } documents`, JSON.stringify(response.data, null, 2)); + ChatpalLogger.debug({ msg: `indexed ${ docs.length } documents`, data: response.data }); } else { throw new Error(response); } } catch (e) { // TODO how to deal with this - ChatpalLogger.error('indexing failed', JSON.stringify(e, null, 2)); + ChatpalLogger.error({ msg: 'indexing failed', err: e }); return false; } } @@ -83,7 +83,7 @@ class Backend { ...this._options.httpOptions, }; - ChatpalLogger.debug('query: ', JSON.stringify(options, null, 2)); + ChatpalLogger.debug({ query: options }); try { if (callback) { @@ -101,7 +101,7 @@ class Backend { throw new Error(response); } } catch (e) { - ChatpalLogger.error('query failed', JSON.stringify(e, null, 2)); + ChatpalLogger.error({ msg: 'query failed', err: e }); throw e; } } diff --git a/app/chatpal-search/server/provider/provider.js b/app/chatpal-search/server/provider/provider.js index ff8e6fb3bc1c4..e60d9c507aff9 100644 --- a/app/chatpal-search/server/provider/provider.js +++ b/app/chatpal-search/server/provider/provider.js @@ -286,8 +286,8 @@ class ChatpalProvider extends SearchProvider { this._stats = server.stats; - ChatpalLogger.debug('config:', JSON.stringify(this._indexConfig, null, 2)); - ChatpalLogger.debug('stats:', JSON.stringify(this._stats, null, 2)); + ChatpalLogger.debug({ config: this._indexConfig }); + ChatpalLogger.debug({ stats: this._stats }); this.index = new Index(this._indexConfig, this.indexFail || clear, this._stats.message.oldest || new Date().valueOf()); diff --git a/app/chatpal-search/server/utils/logger.js b/app/chatpal-search/server/utils/logger.js index c1d75b806a2c0..bfc73d4ecbc7f 100644 --- a/app/chatpal-search/server/utils/logger.js +++ b/app/chatpal-search/server/utils/logger.js @@ -1,4 +1,4 @@ import { Logger } from '../../../logger'; -const ChatpalLogger = new Logger('Chatpal Logger', {}); +const ChatpalLogger = new Logger('Chatpal Logger'); export default ChatpalLogger; diff --git a/app/cloud/server/functions/buildRegistrationData.js b/app/cloud/server/functions/buildRegistrationData.js index d8ecff67687f8..5346558e23ab6 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/connectWorkspace.js b/app/cloud/server/functions/connectWorkspace.js index 6e428803dc0cb..974eb47d06688 100644 --- a/app/cloud/server/functions/connectWorkspace.js +++ b/app/cloud/server/functions/connectWorkspace.js @@ -6,6 +6,7 @@ import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { Settings } from '../../../models'; import { settings } from '../../../settings'; import { saveRegistrationData } from './saveRegistrationData'; +import { SystemLogger } from '../../../../server/lib/logger/system'; export function connectWorkspace(token) { const { connectToCloud } = retrieveRegistrationStatus(); @@ -38,9 +39,9 @@ export function connectWorkspace(token) { }); } catch (e) { if (e.response && e.response.data && e.response.data.error) { - console.error(`Failed to register with Rocket.Chat Cloud. Error: ${ e.response.data.error }`); + SystemLogger.error(`Failed to register with Rocket.Chat Cloud. Error: ${ e.response.data.error }`); } else { - console.error(e); + SystemLogger.error(e); } return false; diff --git a/app/cloud/server/functions/finishOAuthAuthorization.js b/app/cloud/server/functions/finishOAuthAuthorization.js index 5bf24cb9992bd..60691ed20dceb 100644 --- a/app/cloud/server/functions/finishOAuthAuthorization.js +++ b/app/cloud/server/functions/finishOAuthAuthorization.js @@ -5,6 +5,7 @@ import { getRedirectUri } from './getRedirectUri'; import { settings } from '../../../settings'; import { Users } from '../../../models'; import { userScopes } from '../oauthScopes'; +import { SystemLogger } from '../../../../server/lib/logger/system'; export function finishOAuthAuthorization(code, state) { if (settings.get('Cloud_Workspace_Registration_State') !== state) { @@ -32,9 +33,9 @@ export function finishOAuthAuthorization(code, state) { }); } catch (e) { if (e.response && e.response.data && e.response.data.error) { - console.error(`Failed to get AccessToken from Rocket.Chat Cloud. Error: ${ e.response.data.error }`); + SystemLogger.error(`Failed to get AccessToken from Rocket.Chat Cloud. Error: ${ e.response.data.error }`); } else { - console.error(e); + SystemLogger.error(e); } return false; diff --git a/app/cloud/server/functions/getUserCloudAccessToken.js b/app/cloud/server/functions/getUserCloudAccessToken.js index 3be0f726b1215..f6a48a0ad63e9 100644 --- a/app/cloud/server/functions/getUserCloudAccessToken.js +++ b/app/cloud/server/functions/getUserCloudAccessToken.js @@ -8,6 +8,7 @@ import { userLoggedOut } from './userLoggedOut'; import { Users } from '../../../models'; import { settings } from '../../../settings'; import { userScopes } from '../oauthScopes'; +import { SystemLogger } from '../../../../server/lib/logger/system'; export function getUserCloudAccessToken(userId, forceNew = false, scope = '', save = true) { const { connectToCloud, workspaceRegistered } = retrieveRegistrationStatus(); @@ -63,10 +64,10 @@ export function getUserCloudAccessToken(userId, forceNew = false, scope = '', sa }); } catch (e) { if (e.response && e.response.data && e.response.data.error) { - console.error(`Failed to get User AccessToken from Rocket.Chat Cloud. Error: ${ e.response.data.error }`); + SystemLogger.error(`Failed to get User AccessToken from Rocket.Chat Cloud. Error: ${ e.response.data.error }`); if (e.response.data.error === 'oauth_invalid_client_credentials') { - console.error('Server has been unregistered from cloud'); + SystemLogger.error('Server has been unregistered from cloud'); unregisterWorkspace(); } @@ -74,7 +75,7 @@ export function getUserCloudAccessToken(userId, forceNew = false, scope = '', sa userLoggedOut(userId); } } else { - console.error(e); + SystemLogger.error(e); } return ''; diff --git a/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.js b/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.js index f7ad77aa38d1f..6acdbe8be0392 100644 --- a/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.js +++ b/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.js @@ -6,6 +6,7 @@ import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { unregisterWorkspace } from './unregisterWorkspace'; import { settings } from '../../../settings'; import { workspaceScopes } from '../oauthScopes'; +import { SystemLogger } from '../../../../server/lib/logger/system'; export function getWorkspaceAccessTokenWithScope(scope = '') { const { connectToCloud, workspaceRegistered } = retrieveRegistrationStatus(); @@ -43,14 +44,14 @@ export function getWorkspaceAccessTokenWithScope(scope = '') { }); } catch (e) { if (e.response && e.response.data && e.response.data.error) { - console.error(`Failed to get AccessToken from Rocket.Chat Cloud. Error: ${ e.response.data.error }`); + SystemLogger.error(`Failed to get AccessToken from Rocket.Chat Cloud. Error: ${ e.response.data.error }`); if (e.response.data.error === 'oauth_invalid_client_credentials') { - console.error('Server has been unregistered from cloud'); + SystemLogger.error('Server has been unregistered from cloud'); unregisterWorkspace(); } } else { - console.error(e); + SystemLogger.error(e); } return tokenResponse; diff --git a/app/cloud/server/functions/getWorkspaceLicense.js b/app/cloud/server/functions/getWorkspaceLicense.js index 00916c9b93db8..1483e04d3d165 100644 --- a/app/cloud/server/functions/getWorkspaceLicense.js +++ b/app/cloud/server/functions/getWorkspaceLicense.js @@ -5,6 +5,7 @@ import { settings } from '../../../settings'; import { Settings } from '../../../models'; import { callbacks } from '../../../callbacks'; import { LICENSE_VERSION } from '../license'; +import { SystemLogger } from '../../../../server/lib/logger/system'; export function getWorkspaceLicense() { const token = getWorkspaceAccessToken(); @@ -22,9 +23,9 @@ export function getWorkspaceLicense() { }); } catch (e) { if (e.response && e.response.data && e.response.data.error) { - console.error(`Failed to update license from Rocket.Chat Cloud. Error: ${ e.response.data.error }`); + SystemLogger.error(`Failed to update license from Rocket.Chat Cloud. Error: ${ e.response.data.error }`); } else { - console.error(e); + SystemLogger.error(e); } return { updated: false, license: '' }; diff --git a/app/cloud/server/functions/startRegisterWorkspace.js b/app/cloud/server/functions/startRegisterWorkspace.js index 84837dcdaf61d..2f9e4f90b7894 100644 --- a/app/cloud/server/functions/startRegisterWorkspace.js +++ b/app/cloud/server/functions/startRegisterWorkspace.js @@ -2,22 +2,22 @@ import { HTTP } from 'meteor/http'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { syncWorkspace } from './syncWorkspace'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; 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.updateById('Register_Server', true); + Settings.updateValueById('Register_Server', true); - const regInfo = buildWorkspaceRegistrationData(); + const regInfo = await buildWorkspaceRegistrationData(); const cloudUrl = settings.get('Cloud_Url'); @@ -28,9 +28,9 @@ export function startRegisterWorkspace(resend = false) { }); } catch (e) { if (e.response && e.response.data && e.response.data.error) { - console.error(`Failed to register with Rocket.Chat Cloud. ErrorCode: ${ e.response.data.error }`); + SystemLogger.error(`Failed to register with Rocket.Chat Cloud. ErrorCode: ${ e.response.data.error }`); } else { - console.error(e); + SystemLogger.error(e); } return false; diff --git a/app/cloud/server/functions/syncWorkspace.js b/app/cloud/server/functions/syncWorkspace.js index 360309c7cd5cb..1b1021402e25e 100644 --- a/app/cloud/server/functions/syncWorkspace.js +++ b/app/cloud/server/functions/syncWorkspace.js @@ -8,14 +8,15 @@ import { Settings } from '../../../models'; import { settings } from '../../../settings'; import { getAndCreateNpsSurvey } from '../../../../server/services/nps/getAndCreateNpsSurvey'; 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'); @@ -38,9 +39,9 @@ export function syncWorkspace(reconnectCheck = false) { getWorkspaceLicense(); } catch (e) { if (e.response && e.response.data && e.response.data.error) { - console.error(`Failed to sync with Rocket.Chat Cloud. Error: ${ e.response.data.error }`); + SystemLogger.error(`Failed to sync with Rocket.Chat Cloud. Error: ${ e.response.data.error }`); } else { - console.error(e); + SystemLogger.error(e); } return false; @@ -63,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(); @@ -78,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/functions/userLogout.js b/app/cloud/server/functions/userLogout.js index 5dea9eb07815e..405ab5bd0d73c 100644 --- a/app/cloud/server/functions/userLogout.js +++ b/app/cloud/server/functions/userLogout.js @@ -4,6 +4,7 @@ import { userLoggedOut } from './userLoggedOut'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { Users } from '../../../models'; import { settings } from '../../../settings'; +import { SystemLogger } from '../../../../server/lib/logger/system'; export function userLogout(userId) { const { connectToCloud, workspaceRegistered } = retrieveRegistrationStatus(); @@ -41,9 +42,9 @@ export function userLogout(userId) { }); } catch (e) { if (e.response && e.response.data && e.response.data.error) { - console.error(`Failed to get Revoke refresh token to logout of Rocket.Chat Cloud. Error: ${ e.response.data.error }`); + SystemLogger.error(`Failed to get Revoke refresh token to logout of Rocket.Chat Cloud. Error: ${ e.response.data.error }`); } else { - console.error(e); + SystemLogger.error(e); } } } diff --git a/app/cloud/server/index.js b/app/cloud/server/index.js index b574f4fe47766..eb239b095c680 100644 --- a/app/cloud/server/index.js +++ b/app/cloud/server/index.js @@ -6,21 +6,19 @@ 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 { Permissions } from '../../models'; +import { connectWorkspace } from './functions/connectWorkspace'; import { settings } from '../../settings/server'; - -if (Permissions) { - Permissions.create('manage-cloud', ['admin']); -} +import { SystemLogger } from '../../../server/lib/logger/system'; const licenseCronName = 'Cloud Workspace Sync'; Meteor.startup(function() { // run token/license sync if registered let TroubleshootDisableWorkspaceSync; - settings.get('Troubleshoot_Disable_Workspace_Sync', (key, value) => { + settings.watch('Troubleshoot_Disable_Workspace_Sync', (value) => { if (TroubleshootDisableWorkspaceSync === value) { return; } TroubleshootDisableWorkspaceSync = value; @@ -39,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 7723566601f5a..83847711a603b 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/colors/server/settings.js b/app/colors/server/settings.js deleted file mode 100644 index e2224144c3414..0000000000000 --- a/app/colors/server/settings.js +++ /dev/null @@ -1,9 +0,0 @@ -import { settings } from '../../settings'; - -settings.add('HexColorPreview_Enabled', true, { - type: 'boolean', - i18nLabel: 'Enabled', - group: 'Message', - section: 'Hex_Color_Preview', - public: true, -}); diff --git a/app/colors/server/settings.ts b/app/colors/server/settings.ts new file mode 100644 index 0000000000000..0351e8974ca5c --- /dev/null +++ b/app/colors/server/settings.ts @@ -0,0 +1,9 @@ +import { settingsRegistry } from '../../settings/server'; + +settingsRegistry.add('HexColorPreview_Enabled', true, { + type: 'boolean', + i18nLabel: 'Enabled', + group: 'Message', + section: 'Hex_Color_Preview', + public: true, +}); diff --git a/app/cors/client/index.js b/app/cors/client/index.js index e44dbe195eff9..54f26cdf0e05a 100644 --- a/app/cors/client/index.js +++ b/app/cors/client/index.js @@ -1 +1,11 @@ -import '../lib/common'; +import { Meteor } from 'meteor/meteor'; +import { Tracker } from 'meteor/tracker'; + + +import { settings } from '../../settings/client'; + +Meteor.startup(function() { + Tracker.autorun(function() { + Meteor.absoluteUrl.defaultOptions.secure = Boolean(settings.get('Force_SSL')); + }); +}); diff --git a/app/cors/lib/common.js b/app/cors/lib/common.js deleted file mode 100644 index e9e53068854e5..0000000000000 --- a/app/cors/lib/common.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(function() { - settings.onload('Force_SSL', function(key, value) { - Meteor.absoluteUrl.defaultOptions.secure = value; - }); -}); diff --git a/app/cors/server/cors.js b/app/cors/server/cors.js index b14d6b5588bbe..f2c63ee441a61 100644 --- a/app/cors/server/cors.js +++ b/app/cors/server/cors.js @@ -4,19 +4,13 @@ import { Meteor } from 'meteor/meteor'; import { WebApp, WebAppInternals } from 'meteor/webapp'; import _ from 'underscore'; -import { settings } from '../../settings'; +import { settings } from '../../settings/server'; import { Logger } from '../../logger'; -const logger = new Logger('CORS', {}); +const logger = new Logger('CORS'); -// Deprecated setting -let Support_Cordova_App = false; -settings.get('Support_Cordova_App', (key, value) => { - Support_Cordova_App = value; -}); - -settings.get('Enable_CSP', (_, enabled) => { +settings.watch('Enable_CSP', (enabled) => { WebAppInternals.setInlineScriptsAllowed(!enabled); }); @@ -37,6 +31,11 @@ WebApp.rawConnectHandlers.use(function(req, res, next) { settings.get('CDN_PREFIX_ALL') ? null : settings.get('CDN_JSCSS_PREFIX'), ].filter(Boolean).join(' '); + const inlineHashes = [ + // Hash for `window.close()`, required by the CAS login popup. + "'sha256-jqxtvDkBbRAl9Hpqv68WdNOieepg8tJSYu1xIy7zT34='", + ].filter(Boolean).join(' '); + res.setHeader( 'Content-Security-Policy', [ @@ -46,26 +45,12 @@ WebApp.rawConnectHandlers.use(function(req, res, next) { 'frame-src *', 'img-src * data:', 'media-src * data:', - `script-src 'self' 'unsafe-eval' ${ cdn_prefixes }`, + `script-src 'self' 'unsafe-eval' ${ inlineHashes } ${ cdn_prefixes }`, `style-src 'self' 'unsafe-inline' ${ cdn_prefixes }`, ].join('; '), ); } - // Deprecated behavior - if (Support_Cordova_App === true) { - if (/^\/(api|_timesync|sockjs|tap-i18n)(\/|$)/.test(req.url)) { - res.setHeader('Access-Control-Allow-Origin', '*'); - } - - const { setHeader } = res; - res.setHeader = function(key, val, ...args) { - if (key.toLowerCase() === 'access-control-allow-origin' && val === 'http://meteor.local') { - return; - } - return setHeader.apply(this, [key, val, ...args]); - }; - } return next(); }); diff --git a/app/cors/server/index.js b/app/cors/server/index.js index 95faa2aa07cf0..da8ac6e5a1170 100644 --- a/app/cors/server/index.js +++ b/app/cors/server/index.js @@ -1,2 +1,10 @@ import './cors'; -import '../lib/common'; +import { Meteor } from 'meteor/meteor'; + +import { settings } from '../../settings/server'; + +Meteor.startup(function() { + settings.watch('Force_SSL', (value) => { + Meteor.absoluteUrl.defaultOptions.secure = Boolean(value); + }); +}); diff --git a/app/crowd/server/crowd.js b/app/crowd/server/crowd.js index a4cc46ca45ba1..71379018eace4 100644 --- a/app/crowd/server/crowd.js +++ b/app/crowd/server/crowd.js @@ -2,16 +2,16 @@ import { Meteor } from 'meteor/meteor'; import { SHA256 } from 'meteor/sha'; import { SyncedCron } from 'meteor/littledata:synced-cron'; import { Accounts } from 'meteor/accounts-base'; -import _ from 'underscore'; -import { Logger } from '../../logger'; -import { _setRealName } from '../../lib'; -import { Users } from '../../models'; -import { settings } from '../../settings'; -import { hasRole } from '../../authorization'; +import { Logger } from '../../logger/server'; +import { _setRealName } from '../../lib/server'; +import { Users } from '../../models/server'; +import { settings } from '../../settings/server'; +import { hasRole } from '../../authorization/server'; import { deleteUser } from '../../lib/server/functions'; +import { setUserActiveStatus } from '../../lib/server/functions/setUserActiveStatus'; -const logger = new Logger('CROWD', {}); +const logger = new Logger('CROWD'); function fallbackDefaultAccountSystem(bind, username, password) { if (typeof username === 'string') { @@ -154,7 +154,6 @@ export class CROWD { address: crowdUser.email, verified: settings.get('Accounts_Verify_Email_For_External_Accounts'), }], - active: crowdUser.active, crowd: true, }; @@ -173,6 +172,8 @@ export class CROWD { Meteor.users.update(id, { $set: user, }); + + setUserActiveStatus(id, crowdUser.active); } sync() { @@ -207,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); }); } @@ -310,33 +311,26 @@ Accounts.registerLoginHandler('crowd', function(loginRequest) { const jobName = 'CROWD_Sync'; -const addCronJob = _.debounce(Meteor.bindEnvironment(function addCronJobDebounced() { - if (settings.get('CROWD_Sync_User_Data') !== true) { - logger.info('Disabling CROWD Background Sync'); - if (SyncedCron.nextScheduledAtDate(jobName)) { - SyncedCron.remove(jobName); - } - return; - } - - const crowd = new CROWD(); - - if (settings.get('CROWD_Sync_Interval')) { - logger.info('Enabling CROWD Background Sync'); - SyncedCron.add({ - name: jobName, - schedule: (parser) => parser.text(settings.get('CROWD_Sync_Interval')), - job() { - crowd.sync(); - }, - }); - } -}), 500); - Meteor.startup(() => { - Meteor.defer(() => { - settings.get('CROWD_Sync_Interval', addCronJob); - settings.get('CROWD_Sync_User_Data', addCronJob); + settings.watchMultiple(['CROWD_Sync_User_Data', 'CROWD_Sync_Interval'], function addCronJobDebounced([data, interval]) { + if (data !== true) { + logger.info('Disabling CROWD Background Sync'); + if (SyncedCron.nextScheduledAtDate(jobName)) { + SyncedCron.remove(jobName); + } + return; + } + const crowd = new CROWD(); + if (interval) { + logger.info('Enabling CROWD Background Sync'); + SyncedCron.add({ + name: jobName, + schedule: (parser) => parser.text(interval), + job() { + crowd.sync(); + }, + }); + } }); }); diff --git a/app/crowd/server/settings.js b/app/crowd/server/settings.js deleted file mode 100644 index b58362967294b..0000000000000 --- a/app/crowd/server/settings.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(function() { - settings.addGroup('AtlassianCrowd', function() { - const enableQuery = { _id: 'CROWD_Enable', value: true }; - const enableSyncQuery = [enableQuery, { _id: 'CROWD_Sync_User_Data', value: true }]; - - this.add('CROWD_Enable', false, { type: 'boolean', public: true, i18nLabel: 'Enabled' }); - this.add('CROWD_URL', '', { type: 'string', enableQuery, i18nLabel: 'URL' }); - this.add('CROWD_Reject_Unauthorized', true, { type: 'boolean', enableQuery }); - this.add('CROWD_APP_USERNAME', '', { type: 'string', enableQuery, i18nLabel: 'Username', secret: true }); - this.add('CROWD_APP_PASSWORD', '', { type: 'password', enableQuery, i18nLabel: 'Password', secret: true }); - this.add('CROWD_Sync_User_Data', false, { type: 'boolean', enableQuery, i18nLabel: 'Sync_Users' }); - this.add('CROWD_Sync_Interval', 'Every 60 mins', { type: 'string', enableQuery: enableSyncQuery, i18nLabel: 'Sync_Interval', i18nDescription: 'Crowd_sync_interval_Description' }); - this.add('CROWD_Remove_Orphaned_Users', false, { type: 'boolean', public: true, i18nLabel: 'Crowd_Remove_Orphaned_Users' }); - this.add('CROWD_Clean_Usernames', true, { type: 'boolean', enableQuery, i18nLabel: 'Clean_Usernames', i18nDescription: 'Crowd_clean_usernames_Description' }); - this.add('CROWD_Allow_Custom_Username', true, { type: 'boolean', i18nLabel: 'CROWD_Allow_Custom_Username' }); - this.add('CROWD_Test_Connection', 'crowd_test_connection', { type: 'action', actionText: 'Test_Connection', i18nLabel: 'Test_Connection' }); - this.add('CROWD_Sync_Users', 'crowd_sync_users', { type: 'action', actionText: 'Sync_Users', i18nLabel: 'Sync_Users' }); - }); -}); diff --git a/app/crowd/server/settings.ts b/app/crowd/server/settings.ts new file mode 100644 index 0000000000000..74488d84b7aea --- /dev/null +++ b/app/crowd/server/settings.ts @@ -0,0 +1,23 @@ +import { Meteor } from 'meteor/meteor'; + +import { settingsRegistry } from '../../settings/server'; + +Meteor.startup(function() { + settingsRegistry.addGroup('AtlassianCrowd', function() { + const enableQuery = { _id: 'CROWD_Enable', value: true }; + const enableSyncQuery = [enableQuery, { _id: 'CROWD_Sync_User_Data', value: true }]; + + this.add('CROWD_Enable', false, { type: 'boolean', public: true, i18nLabel: 'Enabled' }); + this.add('CROWD_URL', '', { type: 'string', enableQuery, i18nLabel: 'URL' }); + this.add('CROWD_Reject_Unauthorized', true, { type: 'boolean', enableQuery }); + this.add('CROWD_APP_USERNAME', '', { type: 'string', enableQuery, i18nLabel: 'Username', secret: true }); + this.add('CROWD_APP_PASSWORD', '', { type: 'password', enableQuery, i18nLabel: 'Password', secret: true }); + this.add('CROWD_Sync_User_Data', false, { type: 'boolean', enableQuery, i18nLabel: 'Sync_Users' }); + this.add('CROWD_Sync_Interval', 'Every 60 mins', { type: 'string', enableQuery: enableSyncQuery, i18nLabel: 'Sync_Interval', i18nDescription: 'Crowd_sync_interval_Description' }); + this.add('CROWD_Remove_Orphaned_Users', false, { type: 'boolean', public: true, i18nLabel: 'Crowd_Remove_Orphaned_Users' }); + this.add('CROWD_Clean_Usernames', true, { type: 'boolean', enableQuery, i18nLabel: 'Clean_Usernames', i18nDescription: 'Crowd_clean_usernames_Description' }); + this.add('CROWD_Allow_Custom_Username', true, { type: 'boolean', i18nLabel: 'CROWD_Allow_Custom_Username' }); + this.add('CROWD_Test_Connection', 'crowd_test_connection', { type: 'action', actionText: 'Test_Connection', i18nLabel: 'Test_Connection' }); + this.add('CROWD_Sync_Users', 'crowd_sync_users', { type: 'action', actionText: 'Sync_Users', i18nLabel: 'Sync_Users' }); + }); +}); diff --git a/app/custom-oauth/server/custom_oauth_server.js b/app/custom-oauth/server/custom_oauth_server.js index bd698fd90c2b8..2e62be9647eb2 100644 --- a/app/custom-oauth/server/custom_oauth_server.js +++ b/app/custom-oauth/server/custom_oauth_server.js @@ -7,11 +7,11 @@ import { ServiceConfiguration } from 'meteor/service-configuration'; import _ from 'underscore'; import { normalizers, fromTemplate, renameInvalidProperties } from './transform_helpers'; -import { mapRolesFromSSO, mapSSOGroupsToChannels, updateRolesFromSSO } from './oauth_helpers'; import { Logger } from '../../logger'; import { Users } from '../../models'; import { isURL } from '../../utils/lib/isURL'; import { registerAccessTokenService } from '../../lib/server/oauth/oauth'; +import { callbacks } from '../../callbacks/server'; const logger = new Logger('CustomOAuth'); @@ -79,23 +79,10 @@ export class CustomOAuth { this.nameField = (options.nameField || '').trim(); this.avatarField = (options.avatarField || '').trim(); this.mergeUsers = options.mergeUsers; - this.mergeRoles = options.mergeRoles || false; - this.mapChannels = options.mapChannels || false; this.rolesClaim = options.rolesClaim || 'roles'; - this.groupsClaim = options.groupsClaim || 'groups'; this.accessTokenParam = options.accessTokenParam; this.channelsAdmin = options.channelsAdmin || 'rocket.cat'; - if (this.mapChannels) { - const channelsMap = (options.channelsMap || '{}').trim(); - try { - this.channelsMap = JSON.parse(channelsMap); - } catch (err) { - logger.error(`Unexpected error : ${ err.message }`); - } - } - - if (this.identityTokenSentVia == null || this.identityTokenSentVia === 'default') { this.identityTokenSentVia = this.tokenSentVia; } @@ -190,7 +177,7 @@ export class CustomOAuth { data = JSON.parse(response.content); } - logger.debug('Identity response', JSON.stringify(data, null, 2)); + logger.debug({ msg: 'Identity response', data }); return this.normalizeIdentity(data); } catch (err) { @@ -347,13 +334,7 @@ export class CustomOAuth { return; } - if (this.mergeRoles) { - updateRolesFromSSO(user, serviceData, this.rolesClaim); - } - - if (this.mapChannels) { - mapSSOGroupsToChannels(user, serviceData, this.groupsClaim, this.channelsMap, this.channelsAdmin); - } + 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) { @@ -393,13 +374,7 @@ export class CustomOAuth { user.name = user.services[this.name].name; } - if (this.mergeRoles) { - user.roles = mapRolesFromSSO(user.services[this.name], this.rolesClaim); - } - - if (this.mapChannels) { - mapSSOGroupsToChannels(user, user.services[this.name], this.groupsClaim, this.channelsMap, this.channelsAdmin); - } + callbacks.run('afterValidateNewOAuthUser', { identity: user.services[this.name], serviceName: this.name, user }); return true; }); diff --git a/app/custom-oauth/server/oauth_helpers.js b/app/custom-oauth/server/oauth_helpers.js deleted file mode 100644 index f00eea03b6b75..0000000000000 --- a/app/custom-oauth/server/oauth_helpers.js +++ /dev/null @@ -1,73 +0,0 @@ -import { addUserRoles, removeUserFromRoles } from '../../authorization'; -import { Roles, Rooms } from '../../models'; -import { addUserToRoom, createRoom } from '../../lib/server/functions'; -import { Logger } from '../../logger'; - -export const logger = new Logger('OAuth', {}); - -// Returns list of roles from SSO identity -export function mapRolesFromSSO(identity, roleClaimName) { - let roles = []; - - if (identity && roleClaimName) { - // Adding roles - if (identity[roleClaimName] && Array.isArray(identity[roleClaimName])) { - roles = identity[roleClaimName].filter((val) => val !== 'offline_access' && val !== 'uma_authorization' && Roles.findOneByIdOrName(val)); - } - } - - return roles; -} - -// Updates the user with roles from SSO identity -export function updateRolesFromSSO(user, identity, roleClaimName) { - if (user && identity && roleClaimName) { - const rolesFromSSO = mapRolesFromSSO(identity, roleClaimName); - - if (!Array.isArray(user.roles)) { - user.roles = []; - } - - const toRemove = user.roles.filter((val) => !rolesFromSSO.includes(val)); - - // loop through roles that user has that sso doesnt have and remove - toRemove.forEach(function(role) { - removeUserFromRoles(user._id, role); - }); - - const toAdd = rolesFromSSO.filter((val) => !user.roles.includes(val)); - - // loop through roles sso has that user doesnt and add - toAdd.forEach(function(role) { - addUserRoles(user._id, role); - }); - } -} - -export function mapSSOGroupsToChannels(user, identity, groupClaimName, channelsMap, channelsAdmin) { - if (user && identity && groupClaimName) { - const groupsFromSSO = identity[groupClaimName] || []; - - for (const ssoGroup in channelsMap) { - if (typeof ssoGroup === 'string') { - let channels = channelsMap[ssoGroup]; - if (!Array.isArray(channels)) { - channels = [channels]; - } - for (const channel of channels) { - let room = Rooms.findOneByNonValidatedName(channel); - if (!room) { - room = createRoom('c', channel, channelsAdmin, [], false); - if (!room || !room.rid) { - logger.error(`could not create channel ${ channel }`); - return; - } - } - if (Array.isArray(groupsFromSSO) && groupsFromSSO.includes(ssoGroup)) { - addUserToRoom(room._id, user); - } - } - } - } - } -} diff --git a/app/custom-oauth/server/transform_helpers.tests.js b/app/custom-oauth/server/transform_helpers.tests.js index ec1475780e682..5139edb2410e2 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/index.js b/app/custom-sounds/server/index.js index d6b84fae0269c..34ce44c157b7c 100644 --- a/app/custom-sounds/server/index.js +++ b/app/custom-sounds/server/index.js @@ -1,5 +1,4 @@ import './startup/custom-sounds'; -import './startup/permissions'; import './startup/settings'; import './methods/deleteCustomSound'; import './methods/insertOrUpdateSound'; diff --git a/app/custom-sounds/server/methods/deleteCustomSound.js b/app/custom-sounds/server/methods/deleteCustomSound.js index b72c852bacfc6..d168fac12d1d4 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 d3fe25e0173b0..b1fa7c749747c 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 90bf6db20435a..475da52286be1 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/custom-sounds/server/startup/custom-sounds.js b/app/custom-sounds/server/startup/custom-sounds.js index 77a0d2c52b387..00349dfd706dc 100644 --- a/app/custom-sounds/server/startup/custom-sounds.js +++ b/app/custom-sounds/server/startup/custom-sounds.js @@ -3,6 +3,7 @@ import { WebApp } from 'meteor/webapp'; import { RocketChatFile } from '../../../file/server'; import { settings } from '../../../settings/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; export let RocketChatFileCustomSoundsInstance; @@ -19,7 +20,7 @@ Meteor.startup(function() { throw new Error(`Invalid RocketChatStore type [${ storeType }]`); } - console.log(`Using ${ storeType } for custom sounds storage`.green); + SystemLogger.info(`Using ${ storeType } for custom sounds storage`); let path = '~/uploads'; if (settings.get('CustomSounds_FileSystemPath') != null) { diff --git a/app/custom-sounds/server/startup/permissions.js b/app/custom-sounds/server/startup/permissions.js deleted file mode 100644 index 9e66458ea3677..0000000000000 --- a/app/custom-sounds/server/startup/permissions.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Permissions } from '../../../models'; - -Meteor.startup(() => { - if (Permissions) { - Permissions.create('manage-sounds', ['admin']); - } -}); diff --git a/app/custom-sounds/server/startup/settings.js b/app/custom-sounds/server/startup/settings.js deleted file mode 100644 index 442288baae358..0000000000000 --- a/app/custom-sounds/server/startup/settings.js +++ /dev/null @@ -1,24 +0,0 @@ -import { settings } from '../../../settings'; - -settings.addGroup('CustomSoundsFilesystem', function() { - this.add('CustomSounds_Storage_Type', 'GridFS', { - type: 'select', - values: [{ - key: 'GridFS', - i18nLabel: 'GridFS', - }, { - key: 'FileSystem', - i18nLabel: 'FileSystem', - }], - i18nLabel: 'FileUpload_Storage_Type', - }); - - this.add('CustomSounds_FileSystemPath', '', { - type: 'string', - enableQuery: { - _id: 'CustomSounds_Storage_Type', - value: 'FileSystem', - }, - i18nLabel: 'FileUpload_FileSystemPath', - }); -}); diff --git a/app/custom-sounds/server/startup/settings.ts b/app/custom-sounds/server/startup/settings.ts new file mode 100644 index 0000000000000..ded4dc15cd4a4 --- /dev/null +++ b/app/custom-sounds/server/startup/settings.ts @@ -0,0 +1,24 @@ +import { settingsRegistry } from '../../../settings/server'; + +settingsRegistry.addGroup('CustomSoundsFilesystem', function() { + this.add('CustomSounds_Storage_Type', 'GridFS', { + type: 'select', + values: [{ + key: 'GridFS', + i18nLabel: 'GridFS', + }, { + key: 'FileSystem', + i18nLabel: 'FileSystem', + }], + i18nLabel: 'FileUpload_Storage_Type', + }); + + this.add('CustomSounds_FileSystemPath', '', { + type: 'string', + enableQuery: { + _id: 'CustomSounds_Storage_Type', + value: 'FileSystem', + }, + i18nLabel: 'FileUpload_FileSystemPath', + }); +}); diff --git a/app/discussion/client/createDiscussionMessageAction.js b/app/discussion/client/createDiscussionMessageAction.js index 7d107be268b22..120001c9e52d3 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 668cf6ac75b54..e2a8b29c845bb 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/config.js b/app/discussion/server/config.js deleted file mode 100644 index 87780561842bf..0000000000000 --- a/app/discussion/server/config.js +++ /dev/null @@ -1,41 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(() => { - settings.addGroup('Discussion', function() { - // the channel for which discussions are created if none is explicitly chosen - - this.add('Discussion_enabled', true, { - group: 'Discussion', - i18nLabel: 'Enable', - type: 'boolean', - public: true, - }); - }); - - const globalQuery = { - _id: 'RetentionPolicy_Enabled', - value: true, - }; - - settings.add('RetentionPolicy_DoNotPruneDiscussion', true, { - group: 'RetentionPolicy', - section: 'Global Policy', - type: 'boolean', - public: true, - i18nLabel: 'RetentionPolicy_DoNotPruneDiscussion', - i18nDescription: 'RetentionPolicy_DoNotPruneDiscussion_Description', - enableQuery: globalQuery, - }); - - settings.add('RetentionPolicy_DoNotPruneThreads', true, { - group: 'RetentionPolicy', - section: 'Global Policy', - type: 'boolean', - public: true, - i18nLabel: 'RetentionPolicy_DoNotPruneThreads', - i18nDescription: 'RetentionPolicy_DoNotPruneThreads_Description', - enableQuery: globalQuery, - }); -}); diff --git a/app/discussion/server/config.ts b/app/discussion/server/config.ts new file mode 100644 index 0000000000000..c459c2ff2243e --- /dev/null +++ b/app/discussion/server/config.ts @@ -0,0 +1,37 @@ +import { settingsRegistry } from '../../settings/server'; + +settingsRegistry.addGroup('Discussion', function() { + // the channel for which discussions are created if none is explicitly chosen + + this.add('Discussion_enabled', true, { + group: 'Discussion', + i18nLabel: 'Enable', + type: 'boolean', + public: true, + }); +}); + +const globalQuery = { + _id: 'RetentionPolicy_Enabled', + value: true, +}; + +settingsRegistry.add('RetentionPolicy_DoNotPruneDiscussion', true, { + group: 'RetentionPolicy', + section: 'Global Policy', + type: 'boolean', + public: true, + i18nLabel: 'RetentionPolicy_DoNotPruneDiscussion', + i18nDescription: 'RetentionPolicy_DoNotPruneDiscussion_Description', + enableQuery: globalQuery, +}); + +settingsRegistry.add('RetentionPolicy_DoNotPruneThreads', true, { + group: 'RetentionPolicy', + section: 'Global Policy', + type: 'boolean', + public: true, + i18nLabel: 'RetentionPolicy_DoNotPruneThreads', + i18nDescription: 'RetentionPolicy_DoNotPruneThreads_Description', + enableQuery: globalQuery, +}); diff --git a/app/discussion/server/methods/createDiscussion.js b/app/discussion/server/methods/createDiscussion.js index b9d28a0d58df7..8c1ea7dbb3d35 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.js deleted file mode 100644 index 3d54e4c66b16b..0000000000000 --- a/app/discussion/server/permissions.js +++ /dev/null @@ -1,15 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Permissions } from '../../models'; - -Meteor.startup(() => { - // Add permissions for discussion - const permissions = [ - { _id: 'start-discussion', roles: ['admin', 'user', 'guest', 'app'] }, - { _id: 'start-discussion-other-user', roles: ['admin', 'user', 'owner', 'app'] }, - ]; - - for (const permission of permissions) { - Permissions.create(permission._id, permission.roles); - } -}); diff --git a/app/discussion/server/permissions.ts b/app/discussion/server/permissions.ts new file mode 100644 index 0000000000000..da3ac2ee2290a --- /dev/null +++ b/app/discussion/server/permissions.ts @@ -0,0 +1,16 @@ +import { Meteor } from 'meteor/meteor'; + +import { Permissions } from '../../models/server/raw'; + + +Meteor.startup(() => { + // Add permissions for discussion + const permissions = [ + { _id: 'start-discussion', roles: ['admin', 'user', 'guest', 'app'] }, + { _id: 'start-discussion-other-user', roles: ['admin', 'user', 'owner', 'app'] }, + ]; + + for (const permission of permissions) { + Permissions.create(permission._id, permission.roles); + } +}); diff --git a/app/dolphin/lib/common.js b/app/dolphin/lib/common.js index 0e74e6d1fca2e..051a1a339a40f 100644 --- a/app/dolphin/lib/common.js +++ b/app/dolphin/lib/common.js @@ -25,7 +25,7 @@ function DolphinOnCreateUser(options, user) { if (user && user.services && user.services.dolphin && user.services.dolphin.NickName) { user.username = user.services.dolphin.NickName; } - return user; + return options; } if (Meteor.isServer) { diff --git a/app/dolphin/server/startup.js b/app/dolphin/server/startup.js deleted file mode 100644 index bb4c871b18b1a..0000000000000 --- a/app/dolphin/server/startup.js +++ /dev/null @@ -1,10 +0,0 @@ -import { settings } from '../../settings'; - -settings.add('Accounts_OAuth_Dolphin_URL', '', { type: 'string', group: 'OAuth', public: true, section: 'Dolphin', i18nLabel: 'URL' }); -settings.add('Accounts_OAuth_Dolphin', false, { type: 'boolean', group: 'OAuth', section: 'Dolphin', i18nLabel: 'Accounts_OAuth_Custom_Enable' }); -settings.add('Accounts_OAuth_Dolphin_id', '', { type: 'string', group: 'OAuth', section: 'Dolphin', i18nLabel: 'Accounts_OAuth_Custom_id' }); -settings.add('Accounts_OAuth_Dolphin_secret', '', { type: 'string', group: 'OAuth', section: 'Dolphin', i18nLabel: 'Accounts_OAuth_Custom_Secret', secret: true }); -settings.add('Accounts_OAuth_Dolphin_login_style', 'redirect', { type: 'select', group: 'OAuth', section: 'Dolphin', i18nLabel: 'Accounts_OAuth_Custom_Login_Style', persistent: true, values: [{ key: 'redirect', i18nLabel: 'Redirect' }, { key: 'popup', i18nLabel: 'Popup' }, { key: '', i18nLabel: 'Default' }] }); -settings.add('Accounts_OAuth_Dolphin_button_label_text', '', { type: 'string', group: 'OAuth', section: 'Dolphin', i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Text', persistent: true }); -settings.add('Accounts_OAuth_Dolphin_button_label_color', '#FFFFFF', { type: 'string', group: 'OAuth', section: 'Dolphin', i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Color', persistent: true }); -settings.add('Accounts_OAuth_Dolphin_button_color', '#1d74f5', { type: 'string', group: 'OAuth', section: 'Dolphin', i18nLabel: 'Accounts_OAuth_Custom_Button_Color', persistent: true }); diff --git a/app/dolphin/server/startup.ts b/app/dolphin/server/startup.ts new file mode 100644 index 0000000000000..66120fbc72fc3 --- /dev/null +++ b/app/dolphin/server/startup.ts @@ -0,0 +1,10 @@ +import { settingsRegistry } from '../../settings/server'; + +settingsRegistry.add('Accounts_OAuth_Dolphin_URL', '', { type: 'string', group: 'OAuth', public: true, section: 'Dolphin', i18nLabel: 'URL' }); +settingsRegistry.add('Accounts_OAuth_Dolphin', false, { type: 'boolean', group: 'OAuth', section: 'Dolphin', i18nLabel: 'Accounts_OAuth_Custom_Enable' }); +settingsRegistry.add('Accounts_OAuth_Dolphin_id', '', { type: 'string', group: 'OAuth', section: 'Dolphin', i18nLabel: 'Accounts_OAuth_Custom_id' }); +settingsRegistry.add('Accounts_OAuth_Dolphin_secret', '', { type: 'string', group: 'OAuth', section: 'Dolphin', i18nLabel: 'Accounts_OAuth_Custom_Secret', secret: true }); +settingsRegistry.add('Accounts_OAuth_Dolphin_login_style', 'redirect', { type: 'select', group: 'OAuth', section: 'Dolphin', i18nLabel: 'Accounts_OAuth_Custom_Login_Style', persistent: true, values: [{ key: 'redirect', i18nLabel: 'Redirect' }, { key: 'popup', i18nLabel: 'Popup' }, { key: '', i18nLabel: 'Default' }] }); +settingsRegistry.add('Accounts_OAuth_Dolphin_button_label_text', '', { type: 'string', group: 'OAuth', section: 'Dolphin', i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Text', persistent: true }); +settingsRegistry.add('Accounts_OAuth_Dolphin_button_label_color', '#FFFFFF', { type: 'string', group: 'OAuth', section: 'Dolphin', i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Color', persistent: true }); +settingsRegistry.add('Accounts_OAuth_Dolphin_button_color', '#1d74f5', { type: 'string', group: 'OAuth', section: 'Dolphin', i18nLabel: 'Accounts_OAuth_Custom_Button_Color', persistent: true }); diff --git a/app/drupal/server/startup.js b/app/drupal/server/startup.js deleted file mode 100644 index 749d51c065b2f..0000000000000 --- a/app/drupal/server/startup.js +++ /dev/null @@ -1,16 +0,0 @@ -import { settings } from '../../settings'; - -settings.addGroup('OAuth', function() { - this.section('Drupal', function() { - const enableQuery = { - _id: 'Accounts_OAuth_Drupal', - value: true, - }; - - this.add('Accounts_OAuth_Drupal', false, { type: 'boolean' }); - this.add('API_Drupal_URL', '', { type: 'string', public: true, enableQuery, i18nDescription: 'API_Drupal_URL_Description' }); - this.add('Accounts_OAuth_Drupal_id', '', { type: 'string', enableQuery }); - this.add('Accounts_OAuth_Drupal_secret', '', { type: 'string', enableQuery, secret: true }); - this.add('Accounts_OAuth_Drupal_callback_url', '_oauth/drupal', { type: 'relativeUrl', readonly: true, force: true, enableQuery }); - }); -}); diff --git a/app/drupal/server/startup.ts b/app/drupal/server/startup.ts new file mode 100644 index 0000000000000..16118de8c6859 --- /dev/null +++ b/app/drupal/server/startup.ts @@ -0,0 +1,16 @@ +import { settingsRegistry } from '../../settings/server'; + +settingsRegistry.addGroup('OAuth', function() { + this.section('Drupal', function() { + const enableQuery = { + _id: 'Accounts_OAuth_Drupal', + value: true, + }; + + this.add('Accounts_OAuth_Drupal', false, { type: 'boolean' }); + this.add('API_Drupal_URL', '', { type: 'string', public: true, enableQuery, i18nDescription: 'API_Drupal_URL_Description' }); + this.add('Accounts_OAuth_Drupal_id', '', { type: 'string', enableQuery }); + this.add('Accounts_OAuth_Drupal_secret', '', { type: 'string', enableQuery, secret: true }); + this.add('Accounts_OAuth_Drupal_callback_url', '_oauth/drupal', { type: 'relativeUrl', readonly: true, enableQuery }); + }); +}); diff --git a/app/e2e/client/SaveE2EPasswordModal.tsx b/app/e2e/client/SaveE2EPasswordModal.tsx deleted file mode 100644 index d397f04d8e15c..0000000000000 --- a/app/e2e/client/SaveE2EPasswordModal.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import React, { ReactElement } from 'react'; - -import GenericModal from '../../../client/components/GenericModal'; -import { useTranslation } from '../../../client/contexts/TranslationContext'; - -const SaveE2EPasswordModal = ({ - passwordRevealText, - onClose, - onCancel, - onConfirm, -}: { - passwordRevealText: string; - onClose: () => void; - onCancel: () => void; - onConfirm: () => void; -}): ReactElement => { - const t = useTranslation(); - - return ( - - ); -}; - -export default SaveE2EPasswordModal; diff --git a/app/e2e/client/logger.ts b/app/e2e/client/logger.ts index 1efd15d83475c..50e43b634304f 100644 --- a/app/e2e/client/logger.ts +++ b/app/e2e/client/logger.ts @@ -1,4 +1,4 @@ -import { getConfig } from '../../ui-utils/client/config'; +import { getConfig } from '../../../client/lib/utils/getConfig'; let debug: boolean | undefined = undefined; diff --git a/app/e2e/client/rocketchat.e2e.js b/app/e2e/client/rocketchat.e2e.js index 2c0541350ec59..c3a1162cfec95 100644 --- a/app/e2e/client/rocketchat.e2e.js +++ b/app/e2e/client/rocketchat.e2e.js @@ -20,15 +20,15 @@ import { deriveKey, } from './helper'; import * as banners from '../../../client/lib/banners'; -import { Rooms, Subscriptions, Messages } from '../../models'; -import { call } from '../../ui-utils'; +import { Rooms, Subscriptions, Messages } from '../../models/client'; import './events.js'; import './tabbar'; import { log, logError } from './logger'; -import { waitUntilFind } from '../../utils/client/lib/waitUntilFind'; +import { waitUntilFind } from '../../../client/lib/utils/waitUntilFind'; import { imperativeModal } from '../../../client/lib/imperativeModal'; -import SaveE2EPasswordModal from './SaveE2EPasswordModal'; -import EnterE2EPasswordModal from './EnterE2EPasswordModal'; +import SaveE2EPasswordModal from '../../../client/views/e2e/SaveE2EPasswordModal'; +import EnterE2EPasswordModal from '../../../client/views/e2e/EnterE2EPasswordModal'; +import { call } from '../../../client/lib/utils/call'; let failedToDecodeKey = false; diff --git a/app/e2e/client/rocketchat.e2e.room.js b/app/e2e/client/rocketchat.e2e.room.js index bfc10c8c8619b..85f492083e3b1 100644 --- a/app/e2e/client/rocketchat.e2e.room.js +++ b/app/e2e/client/rocketchat.e2e.room.js @@ -23,11 +23,11 @@ import { readFileAsArrayBuffer, } from './helper'; import { Notifications } from '../../notifications/client'; -import { Rooms, Subscriptions, Messages } from '../../models'; -import { call } from '../../ui-utils'; -import { roomTypes, RoomSettingsEnum } from '../../utils'; +import { Rooms, Subscriptions, Messages } from '../../models/client'; +import { roomTypes, RoomSettingsEnum } from '../../utils/client'; import { log, logError } from './logger'; import { E2ERoomState } from './E2ERoomState'; +import { call } from '../../../client/lib/utils/call'; const KEY_ID = Symbol('keyID'); const PAUSED = Symbol('PAUSED'); diff --git a/app/e2e/server/beforeCreateRoom.js b/app/e2e/server/beforeCreateRoom.js index ce3b21ad69355..a8c6a89335196 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/methods/getUsersOfRoomWithoutKey.js b/app/e2e/server/methods/getUsersOfRoomWithoutKey.js index a686af5e88c4c..2139ac8fde7e1 100644 --- a/app/e2e/server/methods/getUsersOfRoomWithoutKey.js +++ b/app/e2e/server/methods/getUsersOfRoomWithoutKey.js @@ -1,16 +1,23 @@ import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; -import { Subscriptions, Users } from '../../../models'; +import { canAccessRoom } from '../../../authorization/server'; +import { Subscriptions, Users } from '../../../models/server'; Meteor.methods({ 'e2e.getUsersOfRoomWithoutKey'(rid) { + check(rid, String); + const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'e2e.getUsersOfRoomWithoutKey' }); } - const room = Meteor.call('canAccessRoom', rid, userId); - if (!room) { + if (!rid) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.getUsersOfRoomWithoutKey' }); + } + + if (!canAccessRoom({ _id: rid }, { _id: userId })) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.getUsersOfRoomWithoutKey' }); } diff --git a/app/e2e/server/methods/setRoomKeyID.js b/app/e2e/server/methods/setRoomKeyID.js index a273e803b9340..e2f8aafa059bf 100644 --- a/app/e2e/server/methods/setRoomKeyID.js +++ b/app/e2e/server/methods/setRoomKeyID.js @@ -1,19 +1,29 @@ import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; -import { Rooms } from '../../../models'; +import { canAccessRoom } from '../../../authorization/server'; +import { Rooms } from '../../../models/server'; Meteor.methods({ 'e2e.setRoomKeyID'(rid, keyID) { + check(rid, String); + check(keyID, String); + const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'e2e.setRoomKeyID' }); } - const room = Meteor.call('canAccessRoom', rid, userId); - if (!room) { + if (!rid) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.setRoomKeyID' }); } + if (!canAccessRoom({ _id: rid }, { _id: userId })) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.setRoomKeyID' }); + } + + const room = Rooms.findOneById(rid, { fields: { e2eKeyId: 1 } }); + if (room.e2eKeyId) { throw new Meteor.Error('error-room-e2e-key-already-exists', 'E2E Key ID already exists', { method: 'e2e.setRoomKeyID' }); } diff --git a/app/e2e/server/settings.js b/app/e2e/server/settings.js deleted file mode 100644 index e15f5e4df1ab5..0000000000000 --- a/app/e2e/server/settings.js +++ /dev/null @@ -1,21 +0,0 @@ -import { settings } from '../../settings'; - -settings.addGroup('E2E Encryption', function() { - this.add('E2E_Enable', false, { - type: 'boolean', - i18nLabel: 'Enabled', - i18nDescription: 'E2E_Enable_description', - public: true, - alert: 'E2E_Enable_alert', - }); - - this.add('E2E_Enabled_Default_DirectRooms', false, { - type: 'boolean', - enableQuery: { _id: 'E2E_Enable', value: true }, - }); - - this.add('E2E_Enabled_Default_PrivateRooms', false, { - type: 'boolean', - enableQuery: { _id: 'E2E_Enable', value: true }, - }); -}); diff --git a/app/e2e/server/settings.ts b/app/e2e/server/settings.ts new file mode 100644 index 0000000000000..20c624fed9b54 --- /dev/null +++ b/app/e2e/server/settings.ts @@ -0,0 +1,23 @@ +import { settingsRegistry } from '../../settings/server'; + +settingsRegistry.addGroup('E2E Encryption', function() { + this.add('E2E_Enable', false, { + type: 'boolean', + i18nLabel: 'Enabled', + i18nDescription: 'E2E_Enable_description', + public: true, + alert: 'E2E_Enable_alert', + }); + + 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/client/lib/emojiCustom.js b/app/emoji-custom/client/lib/emojiCustom.js index f1e8b440cfdc5..177fdfba30ec3 100644 --- a/app/emoji-custom/client/lib/emojiCustom.js +++ b/app/emoji-custom/client/lib/emojiCustom.js @@ -1,5 +1,4 @@ import { Meteor } from 'meteor/meteor'; -import { Blaze } from 'meteor/blaze'; import { Session } from 'meteor/session'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -26,8 +25,6 @@ export const getEmojiUrlFromName = function(name, extension) { return `${ path }/emoji-custom/${ encodeURIComponent(name) }.${ extension }?_dc=${ random }`; }; -Blaze.registerHelper('emojiUrlFromName', getEmojiUrlFromName); - export const deleteEmojiCustom = function(emojiData) { delete emoji.list[`:${ emojiData.name }:`]; const arrayIndex = emoji.packages.emojiCustom.emojisByCategory.rocket.indexOf(emojiData.name); diff --git a/app/emoji-custom/server/methods/deleteEmojiCustom.js b/app/emoji-custom/server/methods/deleteEmojiCustom.js index 7393f245b459e..2964c5ff6cd66 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 b96b40b2fbd06..23843c81cec95 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 d06b382af85e6..d66aeee1a6add 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/emoji-custom/server/startup/emoji-custom.js b/app/emoji-custom/server/startup/emoji-custom.js index 8decb068ea1ed..c7a83a0cf3012 100644 --- a/app/emoji-custom/server/startup/emoji-custom.js +++ b/app/emoji-custom/server/startup/emoji-custom.js @@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; import _ from 'underscore'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { RocketChatFile } from '../../../file'; export let RocketChatFileEmojiCustomInstance; @@ -20,7 +21,7 @@ Meteor.startup(function() { throw new Error(`Invalid RocketChatStore type [${ storeType }]`); } - console.log(`Using ${ storeType } for custom emoji storage`.green); + SystemLogger.info(`Using ${ storeType } for custom emoji storage`); let path = '~/uploads'; if (settings.get('EmojiUpload_FileSystemPath') != null) { diff --git a/app/emoji-custom/server/startup/settings.js b/app/emoji-custom/server/startup/settings.js deleted file mode 100644 index 9b6d84a293102..0000000000000 --- a/app/emoji-custom/server/startup/settings.js +++ /dev/null @@ -1,24 +0,0 @@ -import { settings } from '../../../settings'; - -settings.addGroup('EmojiCustomFilesystem', function() { - this.add('EmojiUpload_Storage_Type', 'GridFS', { - type: 'select', - values: [{ - key: 'GridFS', - i18nLabel: 'GridFS', - }, { - key: 'FileSystem', - i18nLabel: 'FileSystem', - }], - i18nLabel: 'FileUpload_Storage_Type', - }); - - this.add('EmojiUpload_FileSystemPath', '', { - type: 'string', - enableQuery: { - _id: 'EmojiUpload_Storage_Type', - value: 'FileSystem', - }, - i18nLabel: 'FileUpload_FileSystemPath', - }); -}); diff --git a/app/emoji-custom/server/startup/settings.ts b/app/emoji-custom/server/startup/settings.ts new file mode 100644 index 0000000000000..b5ef6d29ac19b --- /dev/null +++ b/app/emoji-custom/server/startup/settings.ts @@ -0,0 +1,24 @@ +import { settingsRegistry } from '../../../settings/server'; + +settingsRegistry.addGroup('EmojiCustomFilesystem', function() { + this.add('EmojiUpload_Storage_Type', 'GridFS', { + type: 'select', + values: [{ + key: 'GridFS', + i18nLabel: 'GridFS', + }, { + key: 'FileSystem', + i18nLabel: 'FileSystem', + }], + i18nLabel: 'FileUpload_Storage_Type', + }); + + this.add('EmojiUpload_FileSystemPath', '', { + type: 'string', + enableQuery: { + _id: 'EmojiUpload_Storage_Type', + value: 'FileSystem', + }, + i18nLabel: 'FileUpload_FileSystemPath', + }); +}); diff --git a/app/emoji/client/emojiParser.js b/app/emoji/client/emojiParser.js index 71422578b13e2..b6d6d6950cf18 100644 --- a/app/emoji/client/emojiParser.js +++ b/app/emoji/client/emojiParser.js @@ -1,4 +1,4 @@ -import { isIE11 } from '../../ui-utils/client/lib/isIE11'; +import { isIE11 } from '../../../client/lib/utils/isIE11'; import { emoji } from '../lib/rocketchat'; /* @@ -29,7 +29,7 @@ const emojiParser = (message) => { let hasText = false; - if (!isIE11()) { + if (!isIE11) { const filter = (node) => { if (node.nodeType === Node.ELEMENT_NODE && ( node.classList.contains('emojione') diff --git a/app/emoji/client/index.js b/app/emoji/client/index.js index bf02d2633523a..c1678c647f116 100644 --- a/app/emoji/client/index.js +++ b/app/emoji/client/index.js @@ -1,4 +1,3 @@ export { EmojiPicker } from './lib/EmojiPicker'; -export { renderEmoji } from './lib/emojiRenderer'; export { emoji } from '../lib/rocketchat'; export { createEmojiMessageRenderer } from './emojiParser'; diff --git a/app/emoji/client/lib/emojiRenderer.js b/app/emoji/client/lib/emojiRenderer.js deleted file mode 100644 index d4b2ba2f49939..0000000000000 --- a/app/emoji/client/lib/emojiRenderer.js +++ /dev/null @@ -1,27 +0,0 @@ -import { Blaze } from 'meteor/blaze'; -import { Template } from 'meteor/templating'; -import { HTML } from 'meteor/htmljs'; - -import { emoji } from '../../lib/rocketchat'; -import { isSetNotNull } from '../function-isSet'; - -export const renderEmoji = function(_emoji) { - if (isSetNotNull(() => emoji.list[_emoji].emojiPackage)) { - const { emojiPackage } = emoji.list[_emoji]; - return emoji.packages[emojiPackage].render(_emoji); - } -}; - -Blaze.registerHelper('renderEmoji', renderEmoji); - -Template.registerHelper('renderEmoji', new Template('renderEmoji', function() { - const view = this; - const _emoji = Blaze.getData(view); - - if (isSetNotNull(() => emoji.list[_emoji].emojiPackage)) { - const { emojiPackage } = emoji.list[_emoji]; - return new HTML.Raw(emoji.packages[emojiPackage].render(_emoji)); - } - - return ''; -})); diff --git a/app/error-handler/server/lib/RocketChat.ErrorHandler.js b/app/error-handler/server/lib/RocketChat.ErrorHandler.js index a542d801fe72c..1552f6dfb6708 100644 --- a/app/error-handler/server/lib/RocketChat.ErrorHandler.js +++ b/app/error-handler/server/lib/RocketChat.ErrorHandler.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { Users, Rooms } from '../../../models'; import { sendMessage } from '../../../lib'; @@ -13,7 +13,7 @@ class ErrorHandler { Meteor.startup(() => { this.registerHandlers(); - settings.get('Log_Exceptions_to_Channel', (key, value) => { + settings.watch('Log_Exceptions_to_Channel', (value) => { this.rid = null; const roomName = value.trim(); if (roomName) { diff --git a/app/error-handler/server/startup/settings.js b/app/error-handler/server/startup/settings.js deleted file mode 100644 index 77421c3fb2a57..0000000000000 --- a/app/error-handler/server/startup/settings.js +++ /dev/null @@ -1,5 +0,0 @@ -import { settings } from '../../../settings'; - -settings.addGroup('Logs', function() { - this.add('Log_Exceptions_to_Channel', '', { type: 'string' }); -}); diff --git a/app/error-handler/server/startup/settings.ts b/app/error-handler/server/startup/settings.ts new file mode 100644 index 0000000000000..589240c4ba215 --- /dev/null +++ b/app/error-handler/server/startup/settings.ts @@ -0,0 +1,5 @@ +import { settingsRegistry } from '../../../settings/server'; + +settingsRegistry.addGroup('Logs', function() { + this.add('Log_Exceptions_to_Channel', '', { type: 'string' }); +}); diff --git a/app/federation/server/endpoints/dispatch.js b/app/federation/server/endpoints/dispatch.js index 7a1971ff82a01..333a30bbeebf3 100644 --- a/app/federation/server/endpoints/dispatch.js +++ b/app/federation/server/endpoints/dispatch.js @@ -1,16 +1,16 @@ -import { Meteor } from 'meteor/meteor'; import { EJSON } from 'meteor/ejson'; import { API } from '../../../api/server'; -import { logger } from '../lib/logger'; +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'; @@ -135,12 +135,12 @@ const eventHandlers = { federationAltered = true; } } catch (ex) { - logger.server.debug(`unable to create subscription for user ( ${ user._id } ) in room (${ roomId })`); + serverLogger.debug(`unable to create subscription for user ( ${ user._id } ) in room (${ roomId })`); } // 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 } }); @@ -164,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 } }); @@ -187,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 } }); @@ -227,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; @@ -240,7 +240,7 @@ const eventHandlers = { origin, }; - Meteor.runAsUser(upload.userId, () => Meteor.wrapAsync(fileStore.insert.bind(fileStore))(upload, buffer)); + fileStore.insertSync(upload, buffer); // Update the message's file denormalizedMessage.file._id = upload._id; @@ -268,7 +268,7 @@ const eventHandlers = { notifyUsersOnMessage(denormalizedMessage, room); sendAllNotifications(denormalizedMessage, room); } catch (err) { - logger.server.debug(`Error on creating message: ${ message._id }`); + serverLogger.debug(`Error on creating message: ${ message._id }`); } } } @@ -445,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'); } @@ -455,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'); } @@ -464,7 +464,7 @@ API.v1.addRoute('federation.events.dispatch', { authRequired: false, rateLimiter // Convert from EJSON const { events } = EJSON.fromJSONValue(payload); - logger.server.debug(`federation.events.dispatch => events=${ events.map((e) => JSON.stringify(e, null, 2)) }`); + serverLogger.debug({ msg: 'federation.events.dispatch', events }); // Loop over received events for (const event of events) { @@ -473,20 +473,20 @@ 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 if (!eventResult || !eventResult.success) { try { - logger.server.debug(`federation.events.dispatch => Event has missing parents -> event=${ JSON.stringify(event, null, 2) }`); + 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; } catch (err) { - logger.server.error(() => `dispatch => event=${ JSON.stringify(event, null, 2) } eventResult=${ JSON.stringify(eventResult, null, 2) } error=${ err.toString() } ${ err.stack }`); + serverLogger.error({ msg: 'dispatch', event, eventResult, err }); throw err; } diff --git a/app/federation/server/endpoints/requestFromLatest.js b/app/federation/server/endpoints/requestFromLatest.js index 87917880229e7..84fd69f88d3af 100644 --- a/app/federation/server/endpoints/requestFromLatest.js +++ b/app/federation/server/endpoints/requestFromLatest.js @@ -1,14 +1,14 @@ import { EJSON } from 'meteor/ejson'; import { API } from '../../../api/server'; -import { logger } from '../lib/logger'; +import { serverLogger } from '../lib/logger'; import { FederationRoomEvents } from '../../../models/server'; import { decryptIfNeeded } from '../lib/crypt'; 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,14 +18,14 @@ 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'); } const { fromDomain, contextType, contextQuery, latestEventIds } = EJSON.fromJSONValue(payload); - logger.server.debug(`federation.events.requestFromLatest => contextType=${ contextType } contextQuery=${ JSON.stringify(contextQuery, null, 2) } latestEventIds=${ latestEventIds.join(', ') }`); + serverLogger.debug({ msg: 'federation.events.requestFromLatest', contextType, contextQuery, latestEventIds }); let EventsModel; @@ -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 ca8b81a92089c..a997b2aff3073 100644 --- a/app/federation/server/endpoints/uploads.js +++ b/app/federation/server/endpoints/uploads.js @@ -1,7 +1,5 @@ -import { Meteor } from 'meteor/meteor'; - 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'; @@ -13,15 +11,13 @@ 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'); } - const getFileBuffer = Meteor.wrapAsync(FileUpload.getBuffer, FileUpload); - - const buffer = getFileBuffer(upload); + const buffer = FileUpload.getBufferSync(upload); return API.v1.success({ upload, buffer }); }, diff --git a/app/federation/server/endpoints/users.js b/app/federation/server/endpoints/users.js index d74cdae832489..7b92b0b5e5094 100644 --- a/app/federation/server/endpoints/users.js +++ b/app/federation/server/endpoints/users.js @@ -1,7 +1,7 @@ import { API } from '../../../api/server'; import { Users } from '../../../models/server'; import { normalizers } from '../normalizers'; -import { logger } from '../lib/logger'; +import { serverLogger } from '../lib/logger'; import { isFederationEnabled } from '../lib/isFederationEnabled'; const userFields = { _id: 1, username: 1, type: 1, emails: 1, name: 1 }; @@ -14,7 +14,7 @@ API.v1.addRoute('federation.users.search', { authRequired: false }, { const { username, domain } = this.requestParams(); - logger.server.debug(`federation.users.search => username=${ username } domain=${ domain }`); + serverLogger.debug(`federation.users.search => username=${ username } domain=${ domain }`); const query = { type: 'user', @@ -41,7 +41,7 @@ API.v1.addRoute('federation.users.getByUsername', { authRequired: false }, { const { username } = this.requestParams(); - logger.server.debug(`federation.users.getByUsername => username=${ username }`); + serverLogger.debug(`federation.users.getByUsername => username=${ username }`); const query = { type: 'user', diff --git a/app/federation/server/functions/addUser.js b/app/federation/server/functions/addUser.js index eebd1656260b2..314b7893fbc10 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 137ef802c5dc1..3f256bf3d88ab 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 4113b6014edd2..0000000000000 --- 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 0000000000000..e8cb5b3e5170c --- /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 8ccb510194a56..46aec0b7dcea1 100644 --- a/app/federation/server/handler/index.js +++ b/app/federation/server/handler/index.js @@ -1,77 +1,77 @@ import qs from 'querystring'; import { disabled } from '../functions/errors'; -import { logger } from '../lib/logger'; +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'); } - logger.client.debug(() => `searchUsers => query=${ query }`); + clientLogger.debug({ msg: 'searchUsers', query }); const [username, peerDomain] = query.split('@'); 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'); } - logger.client.debug(() => `getUserByUsername => query=${ query }`); + clientLogger.debug({ msg: 'getUserByUsername', query }); const [username, peerDomain] = query.split('@'); 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'); } - logger.client.debug(() => `requestEventsFromLatest => domain=${ domain } contextType=${ contextType } contextQuery=${ JSON.stringify(contextQuery, null, 2) } latestEventIds=${ latestEventIds.join(', ') }`); + clientLogger.debug({ msg: 'requestEventsFromLatest', domain, contextType, contextQuery, latestEventIds }); 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'); } domains = [...new Set(domains)]; - logger.client.debug(() => `dispatchEvents => domains=${ domains.join(', ') } events=${ events.map((e) => JSON.stringify(e, null, 2)) }`); + clientLogger.debug({ msg: '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/afterAddedToRoom.js b/app/federation/server/hooks/afterAddedToRoom.js index 1fd359504c5e3..02f76297fcfd8 100644 --- a/app/federation/server/hooks/afterAddedToRoom.js +++ b/app/federation/server/hooks/afterAddedToRoom.js @@ -1,4 +1,4 @@ -import { logger } from '../lib/logger'; +import { clientLogger } from '../lib/logger'; import { getFederatedRoomData, hasExternalDomain, isLocalUser, checkRoomType, checkRoomDomainsLength } from '../functions/helpers'; import { FederationRoomEvents, Subscriptions } from '../../../models/server'; import { normalizers } from '../normalizers'; @@ -16,7 +16,7 @@ async function afterAddedToRoom(involvedUsers, room) { return involvedUsers; } - logger.client.debug(() => `afterAddedToRoom => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + clientLogger.debug({ msg: 'afterAddedToRoom', involvedUsers, room }); // If there are not federated users on this room, ignore it const { users, subscriptions } = getFederatedRoomData(room); @@ -73,7 +73,7 @@ async function afterAddedToRoom(involvedUsers, room) { // Remove the user subscription from the room Subscriptions.remove({ _id: subscription._id }); - logger.client.error('afterAddedToRoom => Could not add user:', err); + clientLogger.error({ msg: 'afterAddedToRoom => Could not add user:', err }); } return involvedUsers; diff --git a/app/federation/server/hooks/afterCreateDirectRoom.js b/app/federation/server/hooks/afterCreateDirectRoom.js index a0048fcb24a6a..79e6fc992836e 100644 --- a/app/federation/server/hooks/afterCreateDirectRoom.js +++ b/app/federation/server/hooks/afterCreateDirectRoom.js @@ -1,4 +1,4 @@ -import { logger } from '../lib/logger'; +import { clientLogger } from '../lib/logger'; import { FederationRoomEvents, Subscriptions } from '../../../models/server'; import { normalizers } from '../normalizers'; import { deleteRoom } from '../../../lib/server/functions'; @@ -7,7 +7,7 @@ import { dispatchEvents } from '../handler'; import { isFullyQualified } from '../functions/helpers'; async function afterCreateDirectRoom(room, extras) { - logger.client.debug(() => `afterCreateDirectRoom => room=${ JSON.stringify(room, null, 2) } extras=${ JSON.stringify(extras, null, 2) }`); + clientLogger.debug({ msg: 'afterCreateDirectRoom', room, extras }); // If the room is federated, ignore if (room.federation) { return room; } @@ -41,11 +41,11 @@ 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); - logger.client.error('afterCreateDirectRoom => Could not create federated room:', err); + clientLogger.error({ msg: 'afterCreateDirectRoom => Could not create federated room:', err }); } return room; diff --git a/app/federation/server/hooks/afterCreateRoom.js b/app/federation/server/hooks/afterCreateRoom.js index a58d40446c179..905e108740cf8 100644 --- a/app/federation/server/hooks/afterCreateRoom.js +++ b/app/federation/server/hooks/afterCreateRoom.js @@ -1,4 +1,4 @@ -import { logger } from '../lib/logger'; +import { clientLogger } from '../lib/logger'; import { FederationRoomEvents, Subscriptions, Users } from '../../../models/server'; import { normalizers } from '../normalizers'; import { deleteRoom } from '../../../lib/server/functions'; @@ -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) { @@ -80,13 +80,13 @@ async function afterCreateRoom(roomOwner, room) { throw new Error('Channels cannot be federated'); } - logger.client.debug(() => `afterCreateRoom => roomOwner=${ JSON.stringify(roomOwner, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + clientLogger.debug({ msg: 'afterCreateRoom', roomOwner, room }); await doAfterCreateRoom(room, users, subscriptions); } catch (err) { deleteRoom(room._id); - logger.client.error('afterCreateRoom => Could not create federated room:', err); + clientLogger.error({ msg: 'afterCreateRoom => Could not create federated room:', err }); } return room; diff --git a/app/federation/server/hooks/afterDeleteMessage.js b/app/federation/server/hooks/afterDeleteMessage.js index 714de63a6e4da..6d070eafbac05 100644 --- a/app/federation/server/hooks/afterDeleteMessage.js +++ b/app/federation/server/hooks/afterDeleteMessage.js @@ -1,5 +1,5 @@ import { FederationRoomEvents, Rooms } from '../../../models/server'; -import { logger } from '../lib/logger'; +import { clientLogger } from '../lib/logger'; import { hasExternalDomain } from '../functions/helpers'; import { getFederationDomain } from '../lib/getFederationDomain'; import { dispatchEvent } from '../handler'; @@ -10,7 +10,7 @@ async function afterDeleteMessage(message) { // If there are not federated users on this room, ignore it if (!hasExternalDomain(room)) { return message; } - logger.client.debug(() => `afterDeleteMessage => message=${ JSON.stringify(message, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + clientLogger.debug({ msg: 'afterDeleteMessage', message, room }); // Create the delete message event const event = await FederationRoomEvents.createDeleteMessageEvent(getFederationDomain(), room._id, message._id); diff --git a/app/federation/server/hooks/afterLeaveRoom.js b/app/federation/server/hooks/afterLeaveRoom.js index 5f1227df0a69e..524d2078ae8ab 100644 --- a/app/federation/server/hooks/afterLeaveRoom.js +++ b/app/federation/server/hooks/afterLeaveRoom.js @@ -1,6 +1,6 @@ import { FederationRoomEvents } from '../../../models/server'; import { getFederatedRoomData, hasExternalDomain, isLocalUser } from '../functions/helpers'; -import { logger } from '../lib/logger'; +import { clientLogger } from '../lib/logger'; import { normalizers } from '../normalizers'; import { getFederationDomain } from '../lib/getFederationDomain'; import { dispatchEvent } from '../handler'; @@ -13,7 +13,7 @@ async function afterLeaveRoom(user, room) { return user; } - logger.client.debug(() => `afterLeaveRoom => user=${ JSON.stringify(user, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + clientLogger.debug({ msg: 'afterLeaveRoom', user, room }); const { users } = getFederatedRoomData(room); @@ -40,7 +40,7 @@ async function afterLeaveRoom(user, room) { // Dispatch the events dispatchEvent(domainsBeforeLeft, userLeftEvent); } catch (err) { - logger.client.error('afterLeaveRoom => Could not make user leave:', err); + clientLogger.error({ msg: 'afterLeaveRoom => Could not make user leave:', err }); } return user; diff --git a/app/federation/server/hooks/afterMuteUser.js b/app/federation/server/hooks/afterMuteUser.js index 4dba95ee4df7a..6a53d204d0a86 100644 --- a/app/federation/server/hooks/afterMuteUser.js +++ b/app/federation/server/hooks/afterMuteUser.js @@ -1,5 +1,5 @@ import { FederationRoomEvents } from '../../../models/server'; -import { logger } from '../lib/logger'; +import { clientLogger } from '../lib/logger'; import { normalizers } from '../normalizers'; import { hasExternalDomain } from '../functions/helpers'; import { getFederationDomain } from '../lib/getFederationDomain'; @@ -9,7 +9,7 @@ async function afterMuteUser(involvedUsers, room) { // If there are not federated users on this room, ignore it if (!hasExternalDomain(room)) { return involvedUsers; } - logger.client.debug(() => `afterMuteUser => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + clientLogger.debug({ msg: 'afterMuteUser', involvedUsers, room }); const { mutedUser } = involvedUsers; diff --git a/app/federation/server/hooks/afterRemoveFromRoom.js b/app/federation/server/hooks/afterRemoveFromRoom.js index c816f251982b1..4c7509ec961e5 100644 --- a/app/federation/server/hooks/afterRemoveFromRoom.js +++ b/app/federation/server/hooks/afterRemoveFromRoom.js @@ -1,6 +1,6 @@ import { FederationRoomEvents } from '../../../models/server'; import { getFederatedRoomData, hasExternalDomain, isLocalUser } from '../functions/helpers'; -import { logger } from '../lib/logger'; +import { clientLogger } from '../lib/logger'; import { normalizers } from '../normalizers'; import { getFederationDomain } from '../lib/getFederationDomain'; import { dispatchEvent } from '../handler'; @@ -15,7 +15,7 @@ async function afterRemoveFromRoom(involvedUsers, room) { return involvedUsers; } - logger.client.debug(() => `afterRemoveFromRoom => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + clientLogger.debug({ msg: 'afterRemoveFromRoom', involvedUsers, room }); const { users } = getFederatedRoomData(room); @@ -42,7 +42,7 @@ async function afterRemoveFromRoom(involvedUsers, room) { // Dispatch the events dispatchEvent(domainsBeforeRemoval, removeUserEvent); } catch (err) { - logger.client.error('afterRemoveFromRoom => Could not remove user:', err); + clientLogger.error({ msg: 'afterRemoveFromRoom => Could not remove user:', err }); } return involvedUsers; diff --git a/app/federation/server/hooks/afterSaveMessage.js b/app/federation/server/hooks/afterSaveMessage.js index 5f72868a23235..7fdc128977b5e 100644 --- a/app/federation/server/hooks/afterSaveMessage.js +++ b/app/federation/server/hooks/afterSaveMessage.js @@ -1,4 +1,4 @@ -import { logger } from '../lib/logger'; +import { clientLogger } from '../lib/logger'; import { FederationRoomEvents } from '../../../models/server'; import { normalizers } from '../normalizers'; import { hasExternalDomain } from '../functions/helpers'; @@ -9,7 +9,7 @@ async function afterSaveMessage(message, room) { // If there are not federated users on this room, ignore it if (!hasExternalDomain(room)) { return message; } - logger.client.debug(() => `afterSaveMessage => message=${ JSON.stringify(message, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + clientLogger.debug({ msg: 'afterSaveMessage', message, room }); let event; diff --git a/app/federation/server/hooks/afterSetReaction.js b/app/federation/server/hooks/afterSetReaction.js index fec108dd91da4..ce3d3d7263940 100644 --- a/app/federation/server/hooks/afterSetReaction.js +++ b/app/federation/server/hooks/afterSetReaction.js @@ -1,7 +1,5 @@ -import _ from 'underscore'; - import { FederationRoomEvents, Rooms } from '../../../models/server'; -import { logger } from '../lib/logger'; +import { clientLogger } from '../lib/logger'; import { hasExternalDomain } from '../functions/helpers'; import { getFederationDomain } from '../lib/getFederationDomain'; import { dispatchEvent } from '../handler'; @@ -12,7 +10,7 @@ async function afterSetReaction(message, { user, reaction }) { // If there are not federated users on this room, ignore it if (!hasExternalDomain(room)) { return message; } - logger.client.debug(() => `afterSetReaction => message=${ JSON.stringify(_.pick(message, '_id', 'msg'), null, 2) } room=${ JSON.stringify(_.pick(room, '_id'), null, 2) } user=${ JSON.stringify(_.pick(user, 'username'), null, 2) } reaction=${ reaction }`); + clientLogger.debug({ msg: 'afterSetReaction', message, room, user, reaction }); // Create the event const event = await FederationRoomEvents.createSetMessageReactionEvent(getFederationDomain(), room._id, message._id, user.username, reaction); diff --git a/app/federation/server/hooks/afterUnmuteUser.js b/app/federation/server/hooks/afterUnmuteUser.js index 3578e08c97d0b..e33ff80ac9564 100644 --- a/app/federation/server/hooks/afterUnmuteUser.js +++ b/app/federation/server/hooks/afterUnmuteUser.js @@ -1,5 +1,5 @@ import { FederationRoomEvents } from '../../../models/server'; -import { logger } from '../lib/logger'; +import { clientLogger } from '../lib/logger'; import { normalizers } from '../normalizers'; import { hasExternalDomain } from '../functions/helpers'; import { getFederationDomain } from '../lib/getFederationDomain'; @@ -9,7 +9,7 @@ async function afterUnmuteUser(involvedUsers, room) { // If there are not federated users on this room, ignore it if (!hasExternalDomain(room)) { return involvedUsers; } - logger.client.debug(() => `afterUnmuteUser => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + clientLogger.debug({ msg: 'afterUnmuteUser', involvedUsers, room }); const { unmutedUser } = involvedUsers; diff --git a/app/federation/server/hooks/afterUnsetReaction.js b/app/federation/server/hooks/afterUnsetReaction.js index 72c9259822c07..42d34091c80af 100644 --- a/app/federation/server/hooks/afterUnsetReaction.js +++ b/app/federation/server/hooks/afterUnsetReaction.js @@ -1,7 +1,5 @@ -import _ from 'underscore'; - import { FederationRoomEvents, Rooms } from '../../../models/server'; -import { logger } from '../lib/logger'; +import { clientLogger } from '../lib/logger'; import { hasExternalDomain } from '../functions/helpers'; import { getFederationDomain } from '../lib/getFederationDomain'; import { dispatchEvent } from '../handler'; @@ -12,7 +10,7 @@ async function afterUnsetReaction(message, { user, reaction }) { // If there are not federated users on this room, ignore it if (!hasExternalDomain(room)) { return message; } - logger.client.debug(() => `afterUnsetReaction => message=${ JSON.stringify(_.pick(message, '_id', 'msg'), null, 2) } room=${ JSON.stringify(_.pick(room, '_id'), null, 2) } user=${ JSON.stringify(_.pick(user, 'username'), null, 2) } reaction=${ reaction }`); + clientLogger.debug({ msg: 'afterUnsetReaction', message, room, user, reaction }); // Create the event const event = await FederationRoomEvents.createUnsetMessageReactionEvent(getFederationDomain(), room._id, message._id, user.username, reaction); diff --git a/app/federation/server/hooks/beforeDeleteRoom.js b/app/federation/server/hooks/beforeDeleteRoom.js index 0131e4692806d..7e55887263dfd 100644 --- a/app/federation/server/hooks/beforeDeleteRoom.js +++ b/app/federation/server/hooks/beforeDeleteRoom.js @@ -1,4 +1,4 @@ -import { logger } from '../lib/logger'; +import { clientLogger } from '../lib/logger'; import { FederationRoomEvents, Rooms } from '../../../models/server'; import { hasExternalDomain } from '../functions/helpers'; import { getFederationDomain } from '../lib/getFederationDomain'; @@ -13,7 +13,7 @@ async function beforeDeleteRoom(roomId) { // If there are not federated users on this room, ignore it if (!hasExternalDomain(room)) { return roomId; } - logger.client.debug(() => `beforeDeleteRoom => room=${ JSON.stringify(room, null, 2) }`); + clientLogger.debug({ msg: 'beforeDeleteRoom', room }); try { // Create the message event @@ -22,7 +22,7 @@ async function beforeDeleteRoom(roomId) { // Dispatch event (async) dispatchEvent(room.federation.domains, event); } catch (err) { - logger.client.error('beforeDeleteRoom => Could not remove room:', err); + clientLogger.error({ msg: 'beforeDeleteRoom => Could not remove room:', err }); throw err; } diff --git a/app/federation/server/lib/crypt.js b/app/federation/server/lib/crypt.js index ce92cfd840591..5a7685a2e9e09 100644 --- a/app/federation/server/lib/crypt.js +++ b/app/federation/server/lib/crypt.js @@ -1,21 +1,21 @@ -import { FederationKeys } from '../../../models/server'; +import { FederationKeys } from '../../../models/server/raw'; import { getFederationDomain } from './getFederationDomain'; import { search } from './dns'; -import { logger } from './logger'; +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) { - logger.crypt.error(err); + cryptLogger.error(err); throw new Error('Could not decrypt'); } @@ -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,19 +48,19 @@ 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) { - logger.crypt.error(err); + cryptLogger.error(err); throw new Error('Could not encrypt'); } diff --git a/app/federation/server/lib/dns.js b/app/federation/server/lib/dns.js index 189b8fc18692a..0080ddae625be 100644 --- a/app/federation/server/lib/dns.js +++ b/app/federation/server/lib/dns.js @@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor'; import mem from 'mem'; import * as federationErrors from '../functions/errors'; -import { logger } from './logger'; +import { dnsLogger } from './logger'; import { isFederationEnabled } from './isFederationEnabled'; import { federationRequest } from './http'; @@ -17,36 +17,36 @@ 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) { - logger.dns.error(err); + dnsLogger.error(err); throw federationErrors.peerCouldNotBeRegisteredWithHub('dns.registerWithHub'); } } -export function searchHub(peerDomain) { +export async function searchHub(peerDomain) { try { - logger.dns.debug(`searchHub: peerDomain=${ peerDomain }`); + 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) { - logger.dns.debug(`searchHub: could not find peerDomain=${ peerDomain }`); + dnsLogger.debug(`searchHub: could not find peerDomain=${ peerDomain }`); throw federationErrors.peerCouldNotBeRegisteredWithHub('dns.registerWithHub'); } const { url, public_key: publicKey } = peer; - logger.dns.debug(`searchHub: found peerDomain=${ peerDomain } url=${ url }`); + dnsLogger.debug(`searchHub: found peerDomain=${ peerDomain } url=${ url }`); return { url, @@ -54,7 +54,7 @@ export function searchHub(peerDomain) { publicKey, }; } catch (err) { - logger.dns.error(err); + dnsLogger.error(err); throw federationErrors.peerNotFoundUsingDNS('dns.searchHub'); } @@ -65,14 +65,14 @@ export function search(peerDomain) { throw federationErrors.disabled('dns.search'); } - logger.dns.debug(`search: peerDomain=${ peerDomain }`); + dnsLogger.debug(`search: peerDomain=${ peerDomain }`); let srvEntries = []; let protocol = ''; // Search by HTTPS first try { - logger.dns.debug(`search: peerDomain=${ peerDomain } srv=_rocketchat._https.${ peerDomain }`); + dnsLogger.debug(`search: peerDomain=${ peerDomain } srv=_rocketchat._https.${ peerDomain }`); srvEntries = memoizedDnsResolveSRV(`_rocketchat._https.${ peerDomain }`); protocol = 'https'; } catch (err) { @@ -82,7 +82,7 @@ export function search(peerDomain) { // If there is not entry, try with http if (!srvEntries.length) { try { - logger.dns.debug(`search: peerDomain=${ peerDomain } srv=_rocketchat._http.${ peerDomain }`); + dnsLogger.debug(`search: peerDomain=${ peerDomain } srv=_rocketchat._http.${ peerDomain }`); srvEntries = memoizedDnsResolveSRV(`_rocketchat._http.${ peerDomain }`); protocol = 'http'; } catch (err) { @@ -93,12 +93,12 @@ export function search(peerDomain) { // If there is not entry, try with tcp if (!srvEntries.length) { try { - logger.dns.debug(`search: peerDomain=${ peerDomain } srv=_rocketchat._tcp.${ peerDomain }`); + dnsLogger.debug(`search: peerDomain=${ peerDomain } srv=_rocketchat._tcp.${ peerDomain }`); srvEntries = memoizedDnsResolveSRV(`_rocketchat._tcp.${ peerDomain }`); protocol = 'https'; // https is the default // Then, also try to get the protocol - logger.dns.debug(`search: peerDomain=${ peerDomain } txt=rocketchat-tcp-protocol.${ peerDomain }`); + dnsLogger.debug(`search: peerDomain=${ peerDomain } txt=rocketchat-tcp-protocol.${ peerDomain }`); protocol = memoizedDnsResolveSRV(`rocketchat-tcp-protocol.${ peerDomain }`); protocol = protocol[0].join(''); @@ -115,7 +115,7 @@ export function search(peerDomain) { // If there is no entry, throw error if (!srvEntry || !protocol) { - logger.dns.debug(`search: could not find valid SRV entry peerDomain=${ peerDomain } srvEntry=${ JSON.stringify(srvEntry) } protocol=${ protocol }`); + dnsLogger.debug({ msg: 'search: could not find valid SRV entry', peerDomain, srvEntry, protocol }); return searchHub(peerDomain); } @@ -123,7 +123,7 @@ export function search(peerDomain) { // Get the public key from the TXT record try { - logger.dns.debug(`search: peerDomain=${ peerDomain } txt=rocketchat-public-key.${ peerDomain }`); + dnsLogger.debug(`search: peerDomain=${ peerDomain } txt=rocketchat-public-key.${ peerDomain }`); const publicKeyTxtRecords = memoizedDnsResolveTXT(`rocketchat-public-key.${ peerDomain }`); // Join the TXT record, that might be split @@ -134,11 +134,11 @@ export function search(peerDomain) { // If there is no entry, throw error if (!publicKey) { - logger.dns.debug(`search: could not find TXT entry for peerDomain=${ peerDomain } - SRV entry found`); + dnsLogger.debug(`search: could not find TXT entry for peerDomain=${ peerDomain } - SRV entry found`); return searchHub(peerDomain); } - logger.dns.debug(`search: found peerDomain=${ peerDomain } srvEntry=${ srvEntry.name }:${ srvEntry.port } protocol=${ protocol }`); + dnsLogger.debug({ msg: 'search: found', peerDomain, srvEntry, protocol }); return { url: `${ protocol }://${ srvEntry.name }:${ srvEntry.port }`, diff --git a/app/federation/server/lib/http.js b/app/federation/server/lib/http.js index 52cb314801636..e18d09b8e86d8 100644 --- a/app/federation/server/lib/http.js +++ b/app/federation/server/lib/http.js @@ -1,28 +1,28 @@ import { HTTP as MeteorHTTP } from 'meteor/http'; import { EJSON } from 'meteor/ejson'; -import { logger } from './logger'; +import { httpLogger } from './logger'; 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); } } - logger.http.debug(`[${ method }] ${ url }`); + httpLogger.debug(`[${ method }] ${ url }`); 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); @@ -37,11 +37,11 @@ export function federationRequestToPeer(method, peerDomain, uri, body, options = let result; try { - logger.http.debug(() => `federationRequestToPeer => url=${ baseUrl }${ uri }`); + 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) { - logger.http.error(`${ ignoreErrors ? '[IGNORED] ' : '' }Error ${ err }`); + httpLogger.error({ msg: `${ ignoreErrors ? '[IGNORED] ' : '' }Error`, err }); if (!ignoreErrors) { throw err; diff --git a/app/federation/server/lib/logger.js b/app/federation/server/lib/logger.js index f3e791a14bf82..9e66d33808b07 100644 --- a/app/federation/server/lib/logger.js +++ b/app/federation/server/lib/logger.js @@ -1,12 +1,10 @@ import { Logger } from '../../../logger/server'; -export const logger = new Logger('Federation', { - sections: { - client: 'client', - crypt: 'crypt', - dns: 'dns', - http: 'http', - server: 'server', - setup: 'Setup', - }, -}); +const logger = new Logger('Federation'); + +export const clientLogger = logger.section('client'); +export const cryptLogger = logger.section('crypt'); +export const dnsLogger = logger.section('dns'); +export const httpLogger = logger.section('http'); +export const serverLogger = logger.section('server'); +export const setupLogger = logger.section('Setup'); diff --git a/app/federation/server/normalizers/user.js b/app/federation/server/normalizers/user.js index e513ff5735972..8f0e64142b2b0 100644 --- a/app/federation/server/normalizers/user.js +++ b/app/federation/server/normalizers/user.js @@ -27,11 +27,10 @@ const normalizeUser = (originalResource) => { // Get only what we need, non-sensitive data const resource = _.pick(originalResource, '_id', 'username', 'type', 'emails', 'name', 'federation', 'isRemote', 'createdAt', '_updatedAt'); - const email = resource.emails[0].address; - resource.emails = [{ address: `${ resource._id }@${ getFederationDomain() }`, }]; + const email = resource.emails[0].address; resource.active = true; resource.roles = ['user']; diff --git a/app/federation/server/startup/generateKeys.js b/app/federation/server/startup/generateKeys.js index 012cdd0b48f47..32eaacc304184 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.js b/app/federation/server/startup/settings.js deleted file mode 100644 index 48213be788078..0000000000000 --- a/app/federation/server/startup/settings.js +++ /dev/null @@ -1,110 +0,0 @@ -import { debounce } from 'underscore'; -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../../settings/server'; -import { updateStatus, updateEnabled, isRegisteringOrEnabled } from '../functions/helpers'; -import { getFederationDomain } from '../lib/getFederationDomain'; -import { getFederationDiscoveryMethod } from '../lib/getFederationDiscoveryMethod'; -import { registerWithHub } from '../lib/dns'; -import { enableCallbacks, disableCallbacks } from '../lib/callbacks'; -import { logger } from '../lib/logger'; -import { FederationKeys } from '../../../models/server'; -import { STATUS_ENABLED, STATUS_REGISTERING, STATUS_ERROR_REGISTERING, STATUS_DISABLED } from '../constants'; - -Meteor.startup(function() { - const federationPublicKey = FederationKeys.getPublicKeyString(); - - settings.addGroup('Federation', function() { - this.add('FEDERATION_Enabled', false, { - type: 'boolean', - i18nLabel: 'Enabled', - i18nDescription: 'FEDERATION_Enabled', - alert: 'FEDERATION_Enabled_Alert', - public: true, - }); - - this.add('FEDERATION_Status', 'Disabled', { - readonly: true, - type: 'string', - i18nLabel: 'FEDERATION_Status', - }); - - this.add('FEDERATION_Domain', '', { - type: 'string', - i18nLabel: 'FEDERATION_Domain', - i18nDescription: 'FEDERATION_Domain_Description', - alert: 'FEDERATION_Domain_Alert', - disableReset: true, - }); - - this.add('FEDERATION_Public_Key', federationPublicKey, { - readonly: true, - type: 'string', - multiline: true, - i18nLabel: 'FEDERATION_Public_Key', - i18nDescription: 'FEDERATION_Public_Key_Description', - }); - - this.add('FEDERATION_Discovery_Method', 'dns', { - type: 'select', - values: [{ - key: 'dns', - i18nLabel: 'DNS', - }, { - key: 'hub', - i18nLabel: 'Hub', - }], - i18nLabel: 'FEDERATION_Discovery_Method', - i18nDescription: 'FEDERATION_Discovery_Method_Description', - public: true, - }); - - this.add('FEDERATION_Test_Setup', 'FEDERATION_Test_Setup', { - type: 'action', - actionText: 'FEDERATION_Test_Setup', - }); - }); -}); - -const updateSettings = debounce(Meteor.bindEnvironment(function() { - // Get the key pair - - if (getFederationDiscoveryMethod() === 'hub' && !isRegisteringOrEnabled()) { - // Register with hub - try { - updateStatus(STATUS_REGISTERING); - - registerWithHub(getFederationDomain(), settings.get('Site_Url'), FederationKeys.getPublicKeyString()); - - updateStatus(STATUS_ENABLED); - } catch (err) { - // Disable federation - updateEnabled(false); - - updateStatus(STATUS_ERROR_REGISTERING); - } - } else { - updateStatus(STATUS_ENABLED); - } -}), 150); - -function enableOrDisable(key, value) { - logger.setup.info(`Federation is ${ value ? 'enabled' : 'disabled' }`); - - if (value) { - updateSettings(); - - enableCallbacks(); - } else { - updateStatus(STATUS_DISABLED); - - disableCallbacks(); - } - - value && updateSettings(); -} - -// Add settings listeners -settings.get('FEDERATION_Enabled', enableOrDisable); -settings.get('FEDERATION_Domain', updateSettings); -settings.get('FEDERATION_Discovery_Method', updateSettings); diff --git a/app/federation/server/startup/settings.ts b/app/federation/server/startup/settings.ts new file mode 100644 index 0000000000000..36ade9e70eeb1 --- /dev/null +++ b/app/federation/server/startup/settings.ts @@ -0,0 +1,107 @@ +import { Meteor } from 'meteor/meteor'; + +import { settingsRegistry, settings } from '../../../settings/server'; +import { updateStatus, updateEnabled, isRegisteringOrEnabled } from '../functions/helpers'; +import { getFederationDomain } from '../lib/getFederationDomain'; +import { getFederationDiscoveryMethod } from '../lib/getFederationDiscoveryMethod'; +import { registerWithHub } from '../lib/dns'; +import { enableCallbacks, disableCallbacks } from '../lib/callbacks'; +import { setupLogger } from '../lib/logger'; +import { FederationKeys } from '../../../models/server/raw'; +import { STATUS_ENABLED, STATUS_REGISTERING, STATUS_ERROR_REGISTERING, STATUS_DISABLED } from '../constants'; + +Meteor.startup(async function() { + const federationPublicKey = await FederationKeys.getPublicKeyString(); + + settingsRegistry.addGroup('Federation', function() { + this.add('FEDERATION_Enabled', false, { + type: 'boolean', + i18nLabel: 'Enabled', + i18nDescription: 'FEDERATION_Enabled', + alert: 'FEDERATION_Enabled_Alert', + public: true, + }); + + this.add('FEDERATION_Status', 'Disabled', { + readonly: true, + type: 'string', + i18nLabel: 'FEDERATION_Status', + }); + + this.add('FEDERATION_Domain', '', { + type: 'string', + i18nLabel: 'FEDERATION_Domain', + i18nDescription: 'FEDERATION_Domain_Description', + alert: 'FEDERATION_Domain_Alert', + // disableReset: true, + }); + + this.add('FEDERATION_Public_Key', federationPublicKey || '', { + readonly: true, + type: 'string', + multiline: true, + i18nLabel: 'FEDERATION_Public_Key', + i18nDescription: 'FEDERATION_Public_Key_Description', + }); + + this.add('FEDERATION_Discovery_Method', 'dns', { + type: 'select', + values: [{ + key: 'dns', + i18nLabel: 'DNS', + }, { + key: 'hub', + i18nLabel: 'Hub', + }], + i18nLabel: 'FEDERATION_Discovery_Method', + i18nDescription: 'FEDERATION_Discovery_Method_Description', + public: true, + }); + + this.add('FEDERATION_Test_Setup', 'FEDERATION_Test_Setup', { + type: 'action', + actionText: 'FEDERATION_Test_Setup', + }); + }); +}); + +const updateSettings = async function(): Promise { + // Get the key pair + + if (getFederationDiscoveryMethod() === 'hub' && !Promise.await(isRegisteringOrEnabled())) { + // Register with hub + try { + await updateStatus(STATUS_REGISTERING); + + await registerWithHub(getFederationDomain(), settings.get('Site_Url'), await FederationKeys.getPublicKeyString()); + + await updateStatus(STATUS_ENABLED); + } catch (err) { + // Disable federation + await updateEnabled(false); + + await updateStatus(STATUS_ERROR_REGISTERING); + } + return; + } + await updateStatus(STATUS_ENABLED); +}; + +// Add settings listeners +settings.watch('FEDERATION_Enabled', function enableOrDisable(value) { + setupLogger.info(`Federation is ${ value ? 'enabled' : 'disabled' }`); + + if (value) { + Promise.await(updateSettings()); + + enableCallbacks(); + } else { + Promise.await(updateStatus(STATUS_DISABLED)); + + disableCallbacks(); + } + + value && updateSettings(); +}); + +settings.watchMultiple(['FEDERATION_Discovery_Method', 'FEDERATION_Domain'], updateSettings); diff --git a/app/file-upload/server/config/AmazonS3.js b/app/file-upload/server/config/AmazonS3.js index fbbb3724ce1aa..2218fdf7488fd 100644 --- a/app/file-upload/server/config/AmazonS3.js +++ b/app/file-upload/server/config/AmazonS3.js @@ -6,13 +6,14 @@ import _ from 'underscore'; import { settings } from '../../../settings'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import '../../ufs/AmazonS3/server.js'; +import { SystemLogger } from '../../../../server/lib/logger/system'; const get = function(file, req, res) { const forceDownload = typeof req.query.download !== 'undefined'; this.store.getRedirectURL(file, forceDownload, (err, fileUrl) => { if (err) { - return console.error(err); + return SystemLogger.error(err); } if (!fileUrl) { @@ -108,4 +109,4 @@ const configure = _.debounce(function() { AmazonS3UserDataFiles.store = FileUpload.configureUploadsStore('AmazonS3', AmazonS3UserDataFiles.name, config); }, 500); -settings.get(/^FileUpload_S3_/, configure); +settings.watchByRegex(/^FileUpload_S3_/, configure); diff --git a/app/file-upload/server/config/FileSystem.js b/app/file-upload/server/config/FileSystem.js index c0119eaca6f0b..9890ebceeb904 100644 --- a/app/file-upload/server/config/FileSystem.js +++ b/app/file-upload/server/config/FileSystem.js @@ -2,9 +2,8 @@ import fs from 'fs'; import { Meteor } from 'meteor/meteor'; import { UploadFS } from 'meteor/jalik:ufs'; -import _ from 'underscore'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import { getFileRange, setRangeHeaders } from '../lib/ranges'; @@ -120,7 +119,7 @@ const FileSystemUserDataFiles = new FileUploadClass({ }, }); -const createFileSystemStore = _.debounce(function() { +settings.watch('FileUpload_FileSystemPath', function() { const options = { path: settings.get('FileUpload_FileSystemPath'), // '/tmp/uploads/photos', }; @@ -131,6 +130,4 @@ const createFileSystemStore = _.debounce(function() { // DEPRECATED backwards compatibililty (remove) UploadFS.getStores().fileSystem = UploadFS.getStores()[FileSystemUploads.name]; -}, 500); - -settings.get('FileUpload_FileSystemPath', createFileSystemStore); +}); diff --git a/app/file-upload/server/config/GoogleStorage.js b/app/file-upload/server/config/GoogleStorage.js index c8e11bfe1fb55..1e2da55951e5a 100644 --- a/app/file-upload/server/config/GoogleStorage.js +++ b/app/file-upload/server/config/GoogleStorage.js @@ -6,13 +6,14 @@ import _ from 'underscore'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import { settings } from '../../../settings'; import '../../ufs/GoogleStorage/server.js'; +import { SystemLogger } from '../../../../server/lib/logger/system'; const get = function(file, req, res) { const forceDownload = typeof req.query.download !== 'undefined'; this.store.getRedirectURL(file, forceDownload, (err, fileUrl) => { if (err) { - return console.error(err); + return SystemLogger.error(err); } if (!fileUrl) { @@ -33,7 +34,7 @@ const get = function(file, req, res) { const copy = function(file, out) { this.store.getRedirectURL(file, false, (err, fileUrl) => { if (err) { - console.error(err); + SystemLogger.error(err); } if (fileUrl) { @@ -92,4 +93,4 @@ const configure = _.debounce(function() { GoogleCloudStorageUserDataFiles.store = FileUpload.configureUploadsStore('GoogleStorage', GoogleCloudStorageUserDataFiles.name, config); }, 500); -settings.get(/^FileUpload_GoogleStorage_/, configure); +settings.watchByRegex(/^FileUpload_GoogleStorage_/, configure); diff --git a/app/file-upload/server/config/Webdav.js b/app/file-upload/server/config/Webdav.js index 8c5382b6b77ea..ed6fc3dd77702 100644 --- a/app/file-upload/server/config/Webdav.js +++ b/app/file-upload/server/config/Webdav.js @@ -3,11 +3,12 @@ import _ from 'underscore'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import { settings } from '../../../settings'; import '../../ufs/Webdav/server.js'; +import { SystemLogger } from '../../../../server/lib/logger/system'; const get = function(file, req, res) { this.store.getReadStream(file._id, file) .on('error', () => { - console.error('An error ocurred when fetching the file'); + SystemLogger.error('An error ocurred when fetching the file'); res.writeHead(503); res.end(); }) @@ -68,4 +69,4 @@ const configure = _.debounce(function() { WebdavUserDataFiles.store = FileUpload.configureUploadsStore('Webdav', WebdavUserDataFiles.name, config); }, 500); -settings.get(/^FileUpload_Webdav_/, configure); +settings.watchByRegex(/^FileUpload_Webdav_/, configure); diff --git a/app/file-upload/server/config/_configUploadStorage.js b/app/file-upload/server/config/_configUploadStorage.js index 3fb54925db717..a21f873764b43 100644 --- a/app/file-upload/server/config/_configUploadStorage.js +++ b/app/file-upload/server/config/_configUploadStorage.js @@ -1,7 +1,8 @@ import { UploadFS } from 'meteor/jalik:ufs'; import _ from 'underscore'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import './AmazonS3.js'; import './FileSystem.js'; import './GoogleStorage.js'; @@ -12,11 +13,11 @@ const configStore = _.debounce(() => { const store = settings.get('FileUpload_Storage_Type'); if (store) { - console.log('Setting default file store to', store); + SystemLogger.info(`Setting default file store to ${ store }`); UploadFS.getStores().Avatars = UploadFS.getStore(`${ store }:Avatars`); UploadFS.getStores().Uploads = UploadFS.getStore(`${ store }:Uploads`); UploadFS.getStores().UserDataFiles = UploadFS.getStore(`${ store }:UserDataFiles`); } }, 1000); -settings.get(/^FileUpload_/, configStore); +settings.watchByRegex(/^FileUpload_/, configStore); diff --git a/app/file-upload/server/lib/FileUpload.js b/app/file-upload/server/lib/FileUpload.js index 23f13684b8447..70ecac27f2037 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'; @@ -28,11 +27,12 @@ import { isValidJWT, generateJWT } from '../../../utils/server/lib/JWTHelper'; import { Messages } from '../../../models/server'; import { AppEvents, Apps } from '../../../apps/server'; import { streamToBuffer } from './streamToBuffer'; +import { SystemLogger } from '../../../../server/lib/logger/system'; const cookie = new Cookies(); let maxFileSize = 0; -settings.get('FileUpload_MaxFileSize', function(key, value) { +settings.watch('FileUpload_MaxFileSize', function(value) { try { maxFileSize = parseInt(value); } catch (e) { @@ -40,6 +40,9 @@ settings.get('FileUpload_MaxFileSize', function(key, 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: {}, @@ -138,7 +141,7 @@ export const FileUpload = { defaultUploads() { return { - collection: Uploads.model, + collection: UploadsModel, filter: new UploadFS.Filter({ onCheck: FileUpload.validateFileUpload, }), @@ -160,7 +163,7 @@ export const FileUpload = { defaultAvatars() { return { - collection: Avatars.model, + collection: AvatarModel, filter: new UploadFS.Filter({ onCheck: FileUpload.validateAvatarUpload, }), @@ -175,7 +178,7 @@ export const FileUpload = { defaultUserDataFiles() { return { - collection: UserDataFiles.model, + collection: UserDataFilesModel, getPath(file) { return `${ settings.get('uniqueID') }/uploads/userData/${ file.userId }`; }, @@ -235,7 +238,7 @@ export const FileUpload = { .then(Meteor.bindEnvironment(({ data, info }) => { fs.writeFile(tempFilePath, data, Meteor.bindEnvironment((err) => { if (err != null) { - console.error(err); + SystemLogger.error(err); } this.getCollection().direct.update({ _id: file._id }, { @@ -253,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); @@ -278,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); @@ -317,7 +320,7 @@ export const FileUpload = { const s = sharp(tmpFile); s.metadata(Meteor.bindEnvironment((err, metadata) => { if (err != null) { - console.error(err); + SystemLogger.error(err); return fut.return(); } @@ -344,7 +347,7 @@ export const FileUpload = { })); })); })).catch((err) => { - console.error(err); + SystemLogger.error(err); fut.return(); }); }; @@ -377,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); }, @@ -431,7 +434,7 @@ export const FileUpload = { getStoreByName(handlerName) { if (this.handlers[handlerName] == null) { - console.error(`Upload handler "${ handlerName }" does not exists`); + SystemLogger.error(`Upload handler "${ handlerName }" does not exists`); } return this.handlers[handlerName]; }, @@ -461,6 +464,8 @@ export const FileUpload = { store.copy(file, buffer); }, + getBufferSync: Meteor.wrapAsync((file, cb) => FileUpload.getBuffer(file, cb)), + copy(file, targetFile) { const store = this.getStoreByName(file.store); const out = fs.createWriteStream(targetFile); @@ -564,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; @@ -584,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; @@ -597,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/proxy.js b/app/file-upload/server/lib/proxy.js index 0921956d111a3..2435df950b6d0 100644 --- a/app/file-upload/server/lib/proxy.js +++ b/app/file-upload/server/lib/proxy.js @@ -19,7 +19,7 @@ WebApp.connectHandlers.stack.unshift({ return next(); } - logger.debug('Upload URL:', req.url); + logger.debug({ msg: 'Upload URL:', url: req.url }); if (req.method !== 'POST') { return next(); @@ -75,7 +75,7 @@ WebApp.connectHandlers.stack.unshift({ instance.extraInformation.host = 'localhost'; } - logger.debug('Wrong instance, proxing to:', `${ instance.extraInformation.host }:${ instance.extraInformation.port }`); + logger.debug(`Wrong instance, proxing to ${ instance.extraInformation.host }:${ instance.extraInformation.port }`); const options = { hostname: instance.extraInformation.host, @@ -84,7 +84,7 @@ WebApp.connectHandlers.stack.unshift({ method: 'POST', }; - console.warn('UFS proxy middleware is deprecated as this upload method is not being used by Web/Mobile Clients. See this: https://docs.rocket.chat/api/rest-api/methods/rooms/upload'); + logger.warn('UFS proxy middleware is deprecated as this upload method is not being used by Web/Mobile Clients. See this: https://docs.rocket.chat/api/rest-api/methods/rooms/upload'); const proxy = http.request(options, function(proxy_res) { proxy_res.pipe(res, { end: true, diff --git a/app/file-upload/server/lib/requests.js b/app/file-upload/server/lib/requests.js index 80a3b4213b38d..3b2e8dad19d77 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/lib/streamToBuffer.ts b/app/file-upload/server/lib/streamToBuffer.ts index 34dc1c434a32a..7e0fa8b3cc1e6 100644 --- a/app/file-upload/server/lib/streamToBuffer.ts +++ b/app/file-upload/server/lib/streamToBuffer.ts @@ -1,11 +1,12 @@ import { Readable } from 'stream'; -export const streamToBuffer = (stream: Readable): Promise => new Promise((resolve) => { +export const streamToBuffer = (stream: Readable): Promise => new Promise((resolve, reject) => { const chunks: Array = []; stream .on('data', (data) => chunks.push(data)) .on('end', () => resolve(Buffer.concat(chunks))) + .on('error', (error) => reject(error)) // force stream to resume data flow in case it was explicitly paused before .resume(); }); diff --git a/app/file-upload/server/methods/getS3FileUrl.js b/app/file-upload/server/methods/getS3FileUrl.js index 9cd915c5e77c2..cfffdfcc032af 100644 --- a/app/file-upload/server/methods/getS3FileUrl.js +++ b/app/file-upload/server/methods/getS3FileUrl.js @@ -1,21 +1,21 @@ import { Meteor } from 'meteor/meteor'; import { UploadFS } from 'meteor/jalik:ufs'; -import { settings } from '../../../settings'; -import { Uploads } from '../../../models'; +import { settings } from '../../../settings/server'; +import { Uploads } from '../../../models/server/raw'; let protectedFiles; -settings.get('FileUpload_ProtectFiles', function(key, value) { +settings.watch('FileUpload_ProtectFiles', function(value) { protectedFiles = 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 6dfeebd29e518..886f9167e07ee 100644 --- a/app/file-upload/server/methods/sendFileMessage.ts +++ b/app/file-upload/server/methods/sendFileMessage.ts @@ -3,14 +3,14 @@ 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'; import { MessageAttachment } from '../../../../definition/IMessage/MessageAttachment/MessageAttachment'; import { FileAttachmentProps } from '../../../../definition/IMessage/MessageAttachment/Files/FileAttachmentProps'; import { IUser } from '../../../../definition/IUser'; +import { SystemLogger } from '../../../../server/lib/logger/system'; Meteor.methods({ async sendFileMessage(roomId, _store, file, msgData = {}) { @@ -34,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) }`); @@ -82,7 +82,7 @@ Meteor.methods({ }); } } catch (e) { - console.error(e); + SystemLogger.error(e); } attachments.push(attachment); } else if (/^audio\/.+/.test(file.type)) { diff --git a/app/file-upload/server/startup/settings.js b/app/file-upload/server/startup/settings.js deleted file mode 100644 index 4b228108e86b7..0000000000000 --- a/app/file-upload/server/startup/settings.js +++ /dev/null @@ -1,277 +0,0 @@ -import { settings } from '../../../settings'; - -settings.addGroup('FileUpload', function() { - this.add('FileUpload_Enabled', true, { - type: 'boolean', - public: true, - }); - - this.add('FileUpload_MaxFileSize', 104857600, { - type: 'int', - public: true, - i18nDescription: 'FileUpload_MaxFileSizeDescription', - }); - - this.add('FileUpload_MediaTypeWhiteList', '', { - type: 'string', - public: true, - i18nDescription: 'FileUpload_MediaTypeWhiteListDescription', - }); - - this.add('FileUpload_MediaTypeBlackList', 'image/svg+xml', { - type: 'string', - public: true, - i18nDescription: 'FileUpload_MediaTypeBlackListDescription', - }); - - this.add('FileUpload_ProtectFiles', true, { - type: 'boolean', - public: true, - i18nDescription: 'FileUpload_ProtectFilesDescription', - }); - - this.add('FileUpload_RotateImages', true, { - type: 'boolean', - }); - - this.add('FileUpload_Enable_json_web_token_for_files', true, { - type: 'boolean', - i18nLabel: 'FileUpload_Enable_json_web_token_for_files', - i18nDescription: 'FileUpload_Enable_json_web_token_for_files_description', - enableQuery: { - _id: 'FileUpload_ProtectFiles', - value: true, - }, - }); - - this.add('FileUpload_json_web_token_secret_for_files', '', { - type: 'string', - i18nLabel: 'FileUpload_json_web_token_secret_for_files', - i18nDescription: 'FileUpload_json_web_token_secret_for_files_description', - enableQuery: { - _id: 'FileUpload_Enable_json_web_token_for_files', - value: true, - }, - }); - - this.add('FileUpload_Storage_Type', 'GridFS', { - type: 'select', - values: [{ - key: 'GridFS', - i18nLabel: 'GridFS', - }, { - key: 'AmazonS3', - i18nLabel: 'AmazonS3', - }, { - key: 'GoogleCloudStorage', - i18nLabel: 'GoogleCloudStorage', - }, { - key: 'Webdav', - i18nLabel: 'WebDAV', - }, { - key: 'FileSystem', - i18nLabel: 'FileSystem', - }], - public: true, - }); - - this.section('Amazon S3', function() { - this.add('FileUpload_S3_Bucket', '', { - type: 'string', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'AmazonS3', - }, - }); - this.add('FileUpload_S3_Acl', '', { - type: 'string', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'AmazonS3', - }, - }); - this.add('FileUpload_S3_AWSAccessKeyId', '', { - type: 'string', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'AmazonS3', - }, - secret: true, - }); - this.add('FileUpload_S3_AWSSecretAccessKey', '', { - type: 'string', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'AmazonS3', - }, - secret: true, - }); - this.add('FileUpload_S3_CDN', '', { - type: 'string', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'AmazonS3', - }, - }); - this.add('FileUpload_S3_Region', '', { - type: 'string', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'AmazonS3', - }, - }); - this.add('FileUpload_S3_BucketURL', '', { - type: 'string', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'AmazonS3', - }, - i18nDescription: 'Override_URL_to_which_files_are_uploaded_This_url_also_used_for_downloads_unless_a_CDN_is_given.', - secret: true, - }); - this.add('FileUpload_S3_SignatureVersion', 'v4', { - type: 'string', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'AmazonS3', - }, - }); - this.add('FileUpload_S3_ForcePathStyle', false, { - type: 'boolean', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'AmazonS3', - }, - }); - this.add('FileUpload_S3_URLExpiryTimeSpan', 120, { - type: 'int', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'AmazonS3', - }, - i18nDescription: 'FileUpload_S3_URLExpiryTimeSpan_Description', - }); - this.add('FileUpload_S3_Proxy_Avatars', false, { - type: 'boolean', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'AmazonS3', - }, - }); - this.add('FileUpload_S3_Proxy_Uploads', false, { - type: 'boolean', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'AmazonS3', - }, - }); - }); - - this.section('Google Cloud Storage', function() { - this.add('FileUpload_GoogleStorage_Bucket', '', { - type: 'string', - private: true, - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'GoogleCloudStorage', - }, - secret: true, - }); - this.add('FileUpload_GoogleStorage_AccessId', '', { - type: 'string', - private: true, - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'GoogleCloudStorage', - }, - secret: true, - }); - this.add('FileUpload_GoogleStorage_Secret', '', { - type: 'string', - multiline: true, - private: true, - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'GoogleCloudStorage', - }, - secret: true, - }); - this.add('FileUpload_GoogleStorage_Proxy_Avatars', false, { - type: 'boolean', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'GoogleCloudStorage', - }, - }); - this.add('FileUpload_GoogleStorage_Proxy_Uploads', false, { - type: 'boolean', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'GoogleCloudStorage', - }, - }); - }); - - this.section('File System', function() { - this.add('FileUpload_FileSystemPath', '', { - type: 'string', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'FileSystem', - }, - }); - }); - - this.section('WebDAV', function() { - this.add('FileUpload_Webdav_Upload_Folder_Path', '', { - type: 'string', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'Webdav', - }, - }); - this.add('FileUpload_Webdav_Server_URL', '', { - type: 'string', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'Webdav', - }, - }); - this.add('FileUpload_Webdav_Username', '', { - type: 'string', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'Webdav', - }, - secret: true, - }); - this.add('FileUpload_Webdav_Password', '', { - type: 'password', - private: true, - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'Webdav', - }, - secret: true, - }); - this.add('FileUpload_Webdav_Proxy_Avatars', false, { - type: 'boolean', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'Webdav', - }, - }); - this.add('FileUpload_Webdav_Proxy_Uploads', false, { - type: 'boolean', - enableQuery: { - _id: 'FileUpload_Storage_Type', - value: 'Webdav', - }, - }); - }); - - this.add('FileUpload_Enabled_Direct', true, { - type: 'boolean', - public: true, - }); -}); diff --git a/app/file-upload/server/startup/settings.ts b/app/file-upload/server/startup/settings.ts new file mode 100644 index 0000000000000..f0fa109e3d7ab --- /dev/null +++ b/app/file-upload/server/startup/settings.ts @@ -0,0 +1,277 @@ +import { settingsRegistry } from '../../../settings/server'; + +settingsRegistry.addGroup('FileUpload', function() { + this.add('FileUpload_Enabled', true, { + type: 'boolean', + public: true, + }); + + this.add('FileUpload_MaxFileSize', 104857600, { + type: 'int', + public: true, + i18nDescription: 'FileUpload_MaxFileSizeDescription', + }); + + this.add('FileUpload_MediaTypeWhiteList', '', { + type: 'string', + public: true, + i18nDescription: 'FileUpload_MediaTypeWhiteListDescription', + }); + + this.add('FileUpload_MediaTypeBlackList', 'image/svg+xml', { + type: 'string', + public: true, + i18nDescription: 'FileUpload_MediaTypeBlackListDescription', + }); + + this.add('FileUpload_ProtectFiles', true, { + type: 'boolean', + public: true, + i18nDescription: 'FileUpload_ProtectFilesDescription', + }); + + this.add('FileUpload_RotateImages', true, { + type: 'boolean', + }); + + this.add('FileUpload_Enable_json_web_token_for_files', true, { + type: 'boolean', + i18nLabel: 'FileUpload_Enable_json_web_token_for_files', + i18nDescription: 'FileUpload_Enable_json_web_token_for_files_description', + enableQuery: { + _id: 'FileUpload_ProtectFiles', + value: true, + }, + }); + + this.add('FileUpload_json_web_token_secret_for_files', '', { + type: 'string', + i18nLabel: 'FileUpload_json_web_token_secret_for_files', + i18nDescription: 'FileUpload_json_web_token_secret_for_files_description', + enableQuery: { + _id: 'FileUpload_Enable_json_web_token_for_files', + value: true, + }, + }); + + this.add('FileUpload_Storage_Type', 'GridFS', { + type: 'select', + values: [{ + key: 'GridFS', + i18nLabel: 'GridFS', + }, { + key: 'AmazonS3', + i18nLabel: 'AmazonS3', + }, { + key: 'GoogleCloudStorage', + i18nLabel: 'GoogleCloudStorage', + }, { + key: 'Webdav', + i18nLabel: 'WebDAV', + }, { + key: 'FileSystem', + i18nLabel: 'FileSystem', + }], + public: true, + }); + + this.section('Amazon S3', function() { + this.add('FileUpload_S3_Bucket', '', { + type: 'string', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'AmazonS3', + }, + }); + this.add('FileUpload_S3_Acl', '', { + type: 'string', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'AmazonS3', + }, + }); + this.add('FileUpload_S3_AWSAccessKeyId', '', { + type: 'string', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'AmazonS3', + }, + secret: true, + }); + this.add('FileUpload_S3_AWSSecretAccessKey', '', { + type: 'string', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'AmazonS3', + }, + secret: true, + }); + this.add('FileUpload_S3_CDN', '', { + type: 'string', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'AmazonS3', + }, + }); + this.add('FileUpload_S3_Region', '', { + type: 'string', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'AmazonS3', + }, + }); + this.add('FileUpload_S3_BucketURL', '', { + type: 'string', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'AmazonS3', + }, + i18nDescription: 'Override_URL_to_which_files_are_uploaded_This_url_also_used_for_downloads_unless_a_CDN_is_given.', + secret: true, + }); + this.add('FileUpload_S3_SignatureVersion', 'v4', { + type: 'string', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'AmazonS3', + }, + }); + this.add('FileUpload_S3_ForcePathStyle', false, { + type: 'boolean', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'AmazonS3', + }, + }); + this.add('FileUpload_S3_URLExpiryTimeSpan', 120, { + type: 'int', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'AmazonS3', + }, + i18nDescription: 'FileUpload_S3_URLExpiryTimeSpan_Description', + }); + this.add('FileUpload_S3_Proxy_Avatars', false, { + type: 'boolean', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'AmazonS3', + }, + }); + this.add('FileUpload_S3_Proxy_Uploads', false, { + type: 'boolean', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'AmazonS3', + }, + }); + }); + + this.section('Google Cloud Storage', function() { + this.add('FileUpload_GoogleStorage_Bucket', '', { + type: 'string', + private: true, + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'GoogleCloudStorage', + }, + secret: true, + }); + this.add('FileUpload_GoogleStorage_AccessId', '', { + type: 'string', + private: true, + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'GoogleCloudStorage', + }, + secret: true, + }); + this.add('FileUpload_GoogleStorage_Secret', '', { + type: 'string', + multiline: true, + private: true, + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'GoogleCloudStorage', + }, + secret: true, + }); + this.add('FileUpload_GoogleStorage_Proxy_Avatars', false, { + type: 'boolean', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'GoogleCloudStorage', + }, + }); + this.add('FileUpload_GoogleStorage_Proxy_Uploads', false, { + type: 'boolean', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'GoogleCloudStorage', + }, + }); + }); + + this.section('File System', function() { + this.add('FileUpload_FileSystemPath', '', { + type: 'string', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'FileSystem', + }, + }); + }); + + this.section('WebDAV', function() { + this.add('FileUpload_Webdav_Upload_Folder_Path', '', { + type: 'string', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'Webdav', + }, + }); + this.add('FileUpload_Webdav_Server_URL', '', { + type: 'string', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'Webdav', + }, + }); + this.add('FileUpload_Webdav_Username', '', { + type: 'string', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'Webdav', + }, + secret: true, + }); + this.add('FileUpload_Webdav_Password', '', { + type: 'password', + private: true, + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'Webdav', + }, + secret: true, + }); + this.add('FileUpload_Webdav_Proxy_Avatars', false, { + type: 'boolean', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'Webdav', + }, + }); + this.add('FileUpload_Webdav_Proxy_Uploads', false, { + type: 'boolean', + enableQuery: { + _id: 'FileUpload_Storage_Type', + value: 'Webdav', + }, + }); + }); + + this.add('FileUpload_Enabled_Direct', true, { + type: 'boolean', + public: true, + }); +}); diff --git a/app/file-upload/ufs/AmazonS3/server.js b/app/file-upload/ufs/AmazonS3/server.js index fc6ab49ebd27a..e5fb5f87709f1 100644 --- a/app/file-upload/ufs/AmazonS3/server.js +++ b/app/file-upload/ufs/AmazonS3/server.js @@ -6,6 +6,8 @@ import { Random } from 'meteor/random'; import _ from 'underscore'; import S3 from 'aws-sdk/clients/s3'; +import { SystemLogger } from '../../../../server/lib/logger/system'; + /** * AmazonS3 store * @param options @@ -91,7 +93,7 @@ export class AmazonS3Store extends UploadFS.Store { s3.deleteObject(params, (err, data) => { if (err) { - console.error(err); + SystemLogger.error(err); } callback && callback(err, data); @@ -144,7 +146,7 @@ export class AmazonS3Store extends UploadFS.Store { }, (error) => { if (error) { - console.error(error); + SystemLogger.error(error); } writeStream.emit('real_finish'); diff --git a/app/file-upload/ufs/GoogleStorage/server.js b/app/file-upload/ufs/GoogleStorage/server.js index 47b384466549c..f0fe78265ef74 100644 --- a/app/file-upload/ufs/GoogleStorage/server.js +++ b/app/file-upload/ufs/GoogleStorage/server.js @@ -3,6 +3,8 @@ import { UploadFS } from 'meteor/jalik:ufs'; import { Random } from 'meteor/random'; import { Storage } from '@google-cloud/storage'; +import { SystemLogger } from '../../../../server/lib/logger/system'; + /** * GoogleStorage store * @param options @@ -70,7 +72,7 @@ export class GoogleStorageStore extends UploadFS.Store { const file = this.getCollection().findOne({ _id: fileId }); this.bucket.file(this.getPath(file)).delete(function(err, data) { if (err) { - console.error(err); + SystemLogger.error(err); } callback && callback(err, data); diff --git a/app/file-upload/ufs/Webdav/server.js b/app/file-upload/ufs/Webdav/server.js index 59567fdb4645c..3d5326e1f194f 100644 --- a/app/file-upload/ufs/Webdav/server.js +++ b/app/file-upload/ufs/Webdav/server.js @@ -5,6 +5,7 @@ import { UploadFS } from 'meteor/jalik:ufs'; import { Random } from 'meteor/random'; import { WebdavClientAdapter } from '../../../webdav/server/lib/webdavClientAdapter'; +import { SystemLogger } from '../../../../server/lib/logger/system'; /** * WebDAV store * @param options @@ -72,7 +73,7 @@ export class WebdavStore extends UploadFS.Store { const file = this.getCollection().findOne({ _id: fileId }); client.deleteFile(this.getPath(file)).then((data) => { callback && callback(null, data); - }).catch(console.error); + }).catch(SystemLogger.error); }; /** diff --git a/app/github-enterprise/server/startup.js b/app/github-enterprise/server/startup.js deleted file mode 100644 index c67ec14f81626..0000000000000 --- a/app/github-enterprise/server/startup.js +++ /dev/null @@ -1,16 +0,0 @@ -import { settings } from '../../settings'; - -settings.addGroup('OAuth', function() { - this.section('GitHub Enterprise', function() { - const enableQuery = { - _id: 'Accounts_OAuth_GitHub_Enterprise', - value: true, - }; - - this.add('Accounts_OAuth_GitHub_Enterprise', false, { type: 'boolean' }); - this.add('API_GitHub_Enterprise_URL', '', { type: 'string', public: true, enableQuery, i18nDescription: 'API_GitHub_Enterprise_URL_Description' }); - this.add('Accounts_OAuth_GitHub_Enterprise_id', '', { type: 'string', enableQuery, secret: true }); - this.add('Accounts_OAuth_GitHub_Enterprise_secret', '', { type: 'string', enableQuery, secret: true }); - this.add('Accounts_OAuth_GitHub_Enterprise_callback_url', '_oauth/github_enterprise', { type: 'relativeUrl', readonly: true, force: true, enableQuery }); - }); -}); diff --git a/app/github-enterprise/server/startup.ts b/app/github-enterprise/server/startup.ts new file mode 100644 index 0000000000000..de3d3b8af7057 --- /dev/null +++ b/app/github-enterprise/server/startup.ts @@ -0,0 +1,16 @@ +import { settingsRegistry } from '../../settings/server'; + +settingsRegistry.addGroup('OAuth', function() { + this.section('GitHub Enterprise', function() { + const enableQuery = { + _id: 'Accounts_OAuth_GitHub_Enterprise', + value: true, + }; + + this.add('Accounts_OAuth_GitHub_Enterprise', false, { type: 'boolean' }); + this.add('API_GitHub_Enterprise_URL', '', { type: 'string', public: true, enableQuery, i18nDescription: 'API_GitHub_Enterprise_URL_Description' }); + this.add('Accounts_OAuth_GitHub_Enterprise_id', '', { type: 'string', enableQuery, secret: true }); + this.add('Accounts_OAuth_GitHub_Enterprise_secret', '', { type: 'string', enableQuery, secret: true }); + this.add('Accounts_OAuth_GitHub_Enterprise_callback_url', '_oauth/github_enterprise', { type: 'relativeUrl', readonly: true, enableQuery }); + }); +}); diff --git a/app/gitlab/server/startup.js b/app/gitlab/server/startup.js deleted file mode 100644 index 2c98e63db7cb7..0000000000000 --- a/app/gitlab/server/startup.js +++ /dev/null @@ -1,18 +0,0 @@ -import { settings } from '../../settings'; - -settings.addGroup('OAuth', function() { - this.section('GitLab', function() { - const enableQuery = { - _id: 'Accounts_OAuth_Gitlab', - value: true, - }; - - this.add('Accounts_OAuth_Gitlab', false, { type: 'boolean', public: true }); - this.add('API_Gitlab_URL', '', { type: 'string', enableQuery, public: true, secret: true }); - this.add('Accounts_OAuth_Gitlab_id', '', { type: 'string', enableQuery }); - this.add('Accounts_OAuth_Gitlab_secret', '', { type: 'string', enableQuery, secret: true }); - this.add('Accounts_OAuth_Gitlab_identity_path', '/api/v4/user', { type: 'string', public: true, enableQuery }); - this.add('Accounts_OAuth_Gitlab_merge_users', false, { type: 'boolean', public: true, enableQuery }); - this.add('Accounts_OAuth_Gitlab_callback_url', '_oauth/gitlab', { type: 'relativeUrl', readonly: true, force: true, enableQuery }); - }); -}); diff --git a/app/gitlab/server/startup.ts b/app/gitlab/server/startup.ts new file mode 100644 index 0000000000000..81e4edb744251 --- /dev/null +++ b/app/gitlab/server/startup.ts @@ -0,0 +1,18 @@ +import { settingsRegistry } from '../../settings/server'; + +settingsRegistry.addGroup('OAuth', function() { + this.section('GitLab', function() { + const enableQuery = { + _id: 'Accounts_OAuth_Gitlab', + value: true, + }; + + this.add('Accounts_OAuth_Gitlab', false, { type: 'boolean', public: true }); + this.add('API_Gitlab_URL', '', { type: 'string', enableQuery, public: true, secret: true }); + this.add('Accounts_OAuth_Gitlab_id', '', { type: 'string', enableQuery }); + this.add('Accounts_OAuth_Gitlab_secret', '', { type: 'string', enableQuery, secret: true }); + this.add('Accounts_OAuth_Gitlab_identity_path', '/api/v4/user', { type: 'string', public: true, enableQuery }); + this.add('Accounts_OAuth_Gitlab_merge_users', false, { type: 'boolean', public: true, enableQuery }); + this.add('Accounts_OAuth_Gitlab_callback_url', '_oauth/gitlab', { type: 'relativeUrl', readonly: true, enableQuery }); + }); +}); diff --git a/app/google-oauth/server/index.js b/app/google-oauth/server/index.js index 81be9e680b0c8..6d49f091fb203 100644 --- a/app/google-oauth/server/index.js +++ b/app/google-oauth/server/index.js @@ -22,7 +22,7 @@ Meteor.startup(() => { credentialSecret: escape(options.credentialSecret), storagePrefix: escape(OAuth._storageTokenPrefix), redirectUrl: escape(options.redirectUrl), - isCordova: !! options.isCordova, + isCordova: Boolean(options.isCordova), }; let template; diff --git a/app/google-vision/README.md b/app/google-vision/README.md deleted file mode 100644 index 75ada0a6d01b8..0000000000000 --- a/app/google-vision/README.md +++ /dev/null @@ -1,7 +0,0 @@ -For this to properly work, you need to have a Google Service Account; -https://console.cloud.google.com/apis/credentials - -Then you have to authorize that service account access to your buckets; -https://console.cloud.google.com/storage/browser -To do that, click on the ellipsis by your bucket's row and Edit object default permissions -Add user and paste the service account e-mail with owner privileges diff --git a/app/google-vision/client/googlevision.js b/app/google-vision/client/googlevision.js deleted file mode 100644 index a9f977eba4ffd..0000000000000 --- a/app/google-vision/client/googlevision.js +++ /dev/null @@ -1,70 +0,0 @@ -const getVisionAttributes = (attachment) => { - const attributes = {}; - const labels = []; - if (attachment.labels && attachment.labels.length > 0) { - attachment.labels.forEach((label) => { - labels.push({ label }); - }); - } - if (attachment.safeSearch && attachment.safeSearch && attachment.safeSearch.adult === true) { - labels.push({ label: 'NSFW', bgColor: 'red', fontColor: 'white' }); - } - if (attachment.safeSearch && attachment.safeSearch.violence === true) { - labels.push({ label: 'Violence', bgColor: 'red', fontColor: 'white' }); - } - if (attachment.colors && attachment.colors.length > 0) { - attributes.color = `#${ attachment.colors[0] }`; - } - if (attachment.logos && attachment.logos.length > 0) { - labels.push({ label: `Logo: ${ attachment.logos[0] }` }); - } - if (attachment.faces && attachment.faces.length > 0) { - let faceCount = 0; - attachment.faces.forEach((face) => { - const faceAttributes = []; - if (face.joy) { - faceAttributes.push('Joy'); - } - if (face.sorrow) { - faceAttributes.push('Sorrow'); - } - if (face.anger) { - faceAttributes.push('Anger'); - } - if (face.surprise) { - faceAttributes.push('Surprise'); - } - if (faceAttributes.length > 0) { - labels.push({ label: `Face ${ ++faceCount }: ${ faceAttributes.join(', ') }` }); - } - }); - } - if (labels.length > 0) { - attributes.labels = labels; - } - return attributes; -}; - -export const createGoogleVisionMessageRenderer = () => - (message) => { - if (!message.attachments?.length) { - return message; - } - - message.attachments = message.attachments.map((attachment) => - Object.assign(attachment, getVisionAttributes(attachment))); - - return message; - }; - -export const createGoogleVisionMessageStreamHandler = () => - (message) => { - if (!message.attachments?.length) { - return message; - } - - message.attachments = message.attachments.map((attachment) => - Object.assign(attachment, getVisionAttributes(attachment))); - - return message; - }; diff --git a/app/google-vision/client/index.js b/app/google-vision/client/index.js deleted file mode 100644 index da4b03a0d29c4..0000000000000 --- a/app/google-vision/client/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export { - createGoogleVisionMessageRenderer, - createGoogleVisionMessageStreamHandler, -} from './googlevision'; diff --git a/app/google-vision/server/googlevision.js b/app/google-vision/server/googlevision.js deleted file mode 100644 index 69729c6c1ddb5..0000000000000 --- a/app/google-vision/server/googlevision.js +++ /dev/null @@ -1,159 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; - -import { settings } from '../../settings'; -import { callbacks } from '../../callbacks'; -import { Uploads, Settings, Users, Messages } from '../../models'; -import { FileUpload } from '../../file-upload'; -import { api } from '../../../server/sdk/api'; - -class GoogleVision { - constructor() { - this.storage = require('@google-cloud/storage'); - this.vision = require('@google-cloud/vision'); - this.storageClient = {}; - this.visionClient = {}; - this.enabled = settings.get('GoogleVision_Enable'); - this.serviceAccount = {}; - settings.get('GoogleVision_Enable', (key, value) => { - this.enabled = value; - }); - settings.get('GoogleVision_ServiceAccount', (key, value) => { - try { - this.serviceAccount = JSON.parse(value); - this.storageClient = this.storage({ credentials: this.serviceAccount }); - this.visionClient = this.vision({ credentials: this.serviceAccount }); - } catch (e) { - this.serviceAccount = {}; - } - }); - settings.get('GoogleVision_Block_Adult_Images', (key, value) => { - if (value) { - callbacks.add('beforeSaveMessage', this.blockUnsafeImages.bind(this), callbacks.priority.MEDIUM, 'googlevision-blockunsafe'); - } else { - callbacks.remove('beforeSaveMessage', 'googlevision-blockunsafe'); - } - }); - callbacks.add('afterFileUpload', this.annotate.bind(this), callbacks.priority.MEDIUM, 'GoogleVision'); - } - - incCallCount(count) { - const currentMonth = new Date().getMonth(); - const maxMonthlyCalls = settings.get('GoogleVision_Max_Monthly_Calls') || 0; - if (maxMonthlyCalls > 0) { - if (settings.get('GoogleVision_Current_Month') !== currentMonth) { - settings.set('GoogleVision_Current_Month', currentMonth); - if (count > maxMonthlyCalls) { - return false; - } - } else if (count + (settings.get('GoogleVision_Current_Month_Calls') || 0) > maxMonthlyCalls) { - return false; - } - } - Settings.update({ _id: 'GoogleVision_Current_Month_Calls' }, { $inc: { value: count } }); - return true; - } - - blockUnsafeImages(message) { - if (this.enabled && this.serviceAccount && message && message.file && message.file._id) { - const file = Uploads.findOne({ _id: message.file._id }); - if (file && file.type && file.type.indexOf('image') !== -1 && file.store === 'GoogleCloudStorage:Uploads' && file.GoogleStorage) { - if (this.incCallCount(1)) { - const bucket = this.storageClient.bucket(settings.get('FileUpload_GoogleStorage_Bucket')); - const bucketFile = bucket.file(file.GoogleStorage.path); - const results = Meteor.wrapAsync(this.visionClient.detectSafeSearch, this.visionClient)(bucketFile); - if (results && results.adult === true) { - FileUpload.getStore('Uploads').deleteById(file._id); - const user = Users.findOneById(message.u && message.u._id); - if (user) { - api.broadcast('notify.ephemeralMessage', user._id, message.rid, { - msg: TAPi18n.__('Adult_images_are_not_allowed', {}, user.language), - }); - } - throw new Meteor.Error('GoogleVisionError: Image blocked'); - } - } else { - console.error('Google Vision: Usage limit exceeded'); - } - return message; - } - } - } - - annotate({ message }) { - const visionTypes = []; - if (settings.get('GoogleVision_Type_Document')) { - visionTypes.push('document'); - } - if (settings.get('GoogleVision_Type_Faces')) { - visionTypes.push('faces'); - } - if (settings.get('GoogleVision_Type_Landmarks')) { - visionTypes.push('landmarks'); - } - if (settings.get('GoogleVision_Type_Labels')) { - visionTypes.push('labels'); - } - if (settings.get('GoogleVision_Type_Logos')) { - visionTypes.push('logos'); - } - if (settings.get('GoogleVision_Type_Properties')) { - visionTypes.push('properties'); - } - if (settings.get('GoogleVision_Type_SafeSearch')) { - visionTypes.push('safeSearch'); - } - if (settings.get('GoogleVision_Type_Similar')) { - visionTypes.push('similar'); - } - if (this.enabled && this.serviceAccount && visionTypes.length > 0 && message.file && message.file._id) { - const file = Uploads.findOne({ _id: message.file._id }); - if (file && file.type && file.type.indexOf('image') !== -1 && file.store === 'GoogleCloudStorage:Uploads' && file.GoogleStorage) { - if (this.incCallCount(visionTypes.length)) { - const bucket = this.storageClient.bucket(settings.get('FileUpload_GoogleStorage_Bucket')); - const bucketFile = bucket.file(file.GoogleStorage.path); - this.visionClient.detect(bucketFile, visionTypes, Meteor.bindEnvironment((error, results) => { - if (!error) { - Messages.setGoogleVisionData(message._id, this.getAnnotations(visionTypes, results)); - } else { - console.trace('GoogleVision error: ', error.stack); - } - })); - } else { - console.error('Google Vision: Usage limit exceeded'); - } - } - } - } - - getAnnotations(visionTypes, visionData) { - if (visionTypes.length === 1) { - const _visionData = {}; - _visionData[`${ visionTypes[0] }`] = visionData; - visionData = _visionData; - } - const results = {}; - for (const index in visionData) { - if (visionData.hasOwnProperty(index)) { - switch (index) { - case 'faces': - case 'landmarks': - case 'labels': - case 'similar': - case 'logos': - results[index] = (results[index] || []).concat(visionData[index] || []); - break; - case 'safeSearch': - results.safeSearch = visionData.safeSearch; - break; - case 'properties': - results.colors = visionData[index].colors; - break; - } - } - } - return results; - } -} - -export default new GoogleVision(); diff --git a/app/google-vision/server/index.js b/app/google-vision/server/index.js deleted file mode 100644 index 95ffc9e6e5081..0000000000000 --- a/app/google-vision/server/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import './settings'; -import './googlevision'; diff --git a/app/google-vision/server/settings.js b/app/google-vision/server/settings.js deleted file mode 100644 index efa0f446bc723..0000000000000 --- a/app/google-vision/server/settings.js +++ /dev/null @@ -1,93 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(function() { - settings.add('GoogleVision_Enable', false, { - type: 'boolean', - group: 'FileUpload', - section: 'Google Vision', - public: true, - enableQuery: { _id: 'FileUpload_Storage_Type', value: 'GoogleCloudStorage' }, - }); - settings.add('GoogleVision_ServiceAccount', '', { - type: 'string', - group: 'FileUpload', - section: 'Google Vision', - multiline: true, - enableQuery: { _id: 'GoogleVision_Enable', value: true }, - secret: true, - }); - settings.add('GoogleVision_Max_Monthly_Calls', 0, { - type: 'int', - group: 'FileUpload', - section: 'Google Vision', - enableQuery: { _id: 'GoogleVision_Enable', value: true }, - }); - settings.add('GoogleVision_Current_Month', 0, { - type: 'int', - group: 'FileUpload', - section: 'Google Vision', - hidden: true, - }); - settings.add('GoogleVision_Current_Month_Calls', 0, { - type: 'int', - group: 'FileUpload', - section: 'Google Vision', - blocked: true, - }); - settings.add('GoogleVision_Type_Document', false, { - type: 'boolean', - group: 'FileUpload', - section: 'Google Vision', - enableQuery: { _id: 'GoogleVision_Enable', value: true }, - }); - settings.add('GoogleVision_Type_Faces', false, { - type: 'boolean', - group: 'FileUpload', - section: 'Google Vision', - enableQuery: { _id: 'GoogleVision_Enable', value: true }, - }); - settings.add('GoogleVision_Type_Landmarks', false, { - type: 'boolean', - group: 'FileUpload', - section: 'Google Vision', - enableQuery: { _id: 'GoogleVision_Enable', value: true }, - }); - settings.add('GoogleVision_Type_Labels', false, { - type: 'boolean', - group: 'FileUpload', - section: 'Google Vision', - enableQuery: { _id: 'GoogleVision_Enable', value: true }, - }); - settings.add('GoogleVision_Type_Logos', false, { - type: 'boolean', - group: 'FileUpload', - section: 'Google Vision', - enableQuery: { _id: 'GoogleVision_Enable', value: true }, - }); - settings.add('GoogleVision_Type_Properties', false, { - type: 'boolean', - group: 'FileUpload', - section: 'Google Vision', - enableQuery: { _id: 'GoogleVision_Enable', value: true }, - }); - settings.add('GoogleVision_Type_SafeSearch', false, { - type: 'boolean', - group: 'FileUpload', - section: 'Google Vision', - enableQuery: { _id: 'GoogleVision_Enable', value: true }, - }); - settings.add('GoogleVision_Block_Adult_Images', false, { - type: 'boolean', - group: 'FileUpload', - section: 'Google Vision', - enableQuery: [{ _id: 'GoogleVision_Enable', value: true }, { _id: 'GoogleVision_Type_SafeSearch', value: true }], - }); - settings.add('GoogleVision_Type_Similar', false, { - type: 'boolean', - group: 'FileUpload', - section: 'Google Vision', - enableQuery: { _id: 'GoogleVision_Enable', value: true }, - }); -}); diff --git a/app/highlight-words/tests/helper.tests.js b/app/highlight-words/tests/helper.tests.js index 28c5fd075164e..2b4e895d0e652 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/iframe-login/server/iframe_rocketchat.js b/app/iframe-login/server/iframe_rocketchat.js deleted file mode 100644 index b23285be8e6f2..0000000000000 --- a/app/iframe-login/server/iframe_rocketchat.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(function() { - settings.addGroup('Accounts', function() { - this.section('Iframe', function() { - this.add('Accounts_iframe_enabled', false, { type: 'boolean', public: true }); - this.add('Accounts_iframe_url', '', { type: 'string', public: true }); - this.add('Accounts_Iframe_api_url', '', { type: 'string', public: true }); - this.add('Accounts_Iframe_api_method', 'POST', { type: 'string', public: true }); - }); - }); -}); diff --git a/app/iframe-login/server/iframe_rocketchat.ts b/app/iframe-login/server/iframe_rocketchat.ts new file mode 100644 index 0000000000000..675db0dfff518 --- /dev/null +++ b/app/iframe-login/server/iframe_rocketchat.ts @@ -0,0 +1,14 @@ +import { Meteor } from 'meteor/meteor'; + +import { settingsRegistry } from '../../settings/server'; + +Meteor.startup(function() { + settingsRegistry.addGroup('Accounts', function() { + this.section('Iframe', function() { + this.add('Accounts_iframe_enabled', false, { type: 'boolean', public: true }); + this.add('Accounts_iframe_url', '', { type: 'string', public: true }); + this.add('Accounts_Iframe_api_url', '', { type: 'string', public: true }); + this.add('Accounts_Iframe_api_method', 'POST', { type: 'string', public: true }); + }); + }); +}); diff --git a/app/iframe-login/server/iframe_server.js b/app/iframe-login/server/iframe_server.js index 586bc8ee8558f..95fd1d4cd50f6 100644 --- a/app/iframe-login/server/iframe_server.js +++ b/app/iframe-login/server/iframe_server.js @@ -10,8 +10,6 @@ Accounts.registerLoginHandler('iframe', function(result) { check(result.token, String); - console.log('[Method] registerLoginHandler'); - const user = Meteor.users.findOne({ 'services.iframe.token': result.token, }); diff --git a/app/importer-csv/server/importer.js b/app/importer-csv/server/importer.js index 66cc086c1c1ac..285f747e61939 100644 --- a/app/importer-csv/server/importer.js +++ b/app/importer-csv/server/importer.js @@ -35,7 +35,7 @@ export class CsvImporter extends Base { oldRate = rate; } } catch (e) { - console.error(e); + this.logger.error(e); } }; diff --git a/app/importer-hipchat-enterprise/server/importer.js b/app/importer-hipchat-enterprise/server/importer.js index 3f59b669e6323..d3482df088d96 100644 --- a/app/importer-hipchat-enterprise/server/importer.js +++ b/app/importer-hipchat-enterprise/server/importer.js @@ -3,28 +3,12 @@ import path from 'path'; import fs from 'fs'; import { Meteor } from 'meteor/meteor'; -import TurndownService from 'turndown'; import { Base, ProgressStep, } from '../../importer/server'; -const turndownService = new TurndownService({ - strongDelimiter: '*', - hr: '', - br: '\n', -}); - -turndownService.addRule('strikethrough', { - filter: 'img', - - replacement(content, node) { - const src = node.getAttribute('src') || ''; - const alt = node.alt || node.title || src; - return src ? `[${ alt }](${ src })` : ''; - }, -}); export class HipChatEnterpriseImporter extends Base { constructor(info, importRecord) { @@ -43,7 +27,7 @@ export class HipChatEnterpriseImporter extends Base { this.logger.debug('parsing file contents'); return JSON.parse(dataString); } catch (e) { - console.error(e); + this.logger.error(e); return false; } } @@ -151,6 +135,30 @@ export class HipChatEnterpriseImporter extends Base { return count; } + get turndownService() { + const TurndownService = Promise.await(import('turndown')).default; + + const turndownService = new TurndownService({ + strongDelimiter: '*', + hr: '', + br: '\n', + }); + + turndownService.addRule('strikethrough', { + filter: 'img', + + replacement(content, node) { + const src = node.getAttribute('src') || ''; + const alt = node.alt || node.title || src; + return src ? `[${ alt }](${ src })` : ''; + }, + }); + + this.turndownService = turndownService; + + return turndownService; + } + convertImportedMessage(importedMessage, rid, type) { const idType = type === 'private' ? type : `${ rid }-${ type }`; const newId = `hipchatenterprise-${ idType }-${ importedMessage.id }`; @@ -167,7 +175,7 @@ export class HipChatEnterpriseImporter extends Base { const text = importedMessage.message; if (importedMessage.message_format === 'html') { - newMessage.msg = turndownService.turndown(text); + newMessage.msg = this.turndownService.turndown(text); } else if (text.startsWith('/me ')) { newMessage.msg = `${ text.replace(/\/me /, '_') }_`; } else { diff --git a/app/importer-pending-avatars/server/importer.js b/app/importer-pending-avatars/server/importer.js index 7a8767ba582af..aa12266decf27 100644 --- a/app/importer-pending-avatars/server/importer.js +++ b/app/importer-pending-avatars/server/importer.js @@ -52,7 +52,6 @@ export class PendingAvatarImporter extends Base { Users.update({ _id }, { $unset: { _pendingAvatarUrl: '' } }); } catch (error) { this.logger.warn(`Failed to set ${ name }'s avatar from url ${ url }`); - console.log(`Failed to set ${ name }'s avatar from url ${ url }`); } }); } finally { @@ -65,7 +64,7 @@ export class PendingAvatarImporter extends Base { } catch (error) { // If the cursor expired, restart the method if (error && error.codeName === 'CursorNotFound') { - console.log('CursorNotFound'); + this.logger.info('CursorNotFound'); return this.startImport(); } diff --git a/app/importer-slack/server/importer.js b/app/importer-slack/server/importer.js index 7853b6d54e590..8c490192ac5d9 100644 --- a/app/importer-slack/server/importer.js +++ b/app/importer-slack/server/importer.js @@ -3,10 +3,9 @@ import _ from 'underscore'; import { Base, ProgressStep, - ImportData, ImporterWebsocket, } from '../../importer/server'; -import { Messages } from '../../models'; +import { Messages, ImportData } from '../../models/server'; import { settings } from '../../settings/server'; import { MentionsParser } from '../../mentions/lib/MentionsParser'; import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; @@ -18,7 +17,7 @@ export class SlackImporter extends Base { this.logger.debug('parsing file contents'); return JSON.parse(dataString); } catch (e) { - console.error(e); + this.logger.error(e); return false; } } @@ -197,7 +196,7 @@ export class SlackImporter extends Base { oldRate = rate; } } catch (e) { - console.error(e); + this.logger.error(e); } }; @@ -290,7 +289,7 @@ export class SlackImporter extends Base { }); if (!_.isEmpty(missedTypes)) { - console.log('Missed import types:', missedTypes); + this.logger.info('Missed import types:', missedTypes); } } catch (e) { this.logger.error(e); diff --git a/app/importer/server/classes/ImportDataConverter.ts b/app/importer/server/classes/ImportDataConverter.ts index b92fab40d1e06..906ead79effcc 100644 --- a/app/importer/server/classes/ImportDataConverter.ts +++ b/app/importer/server/classes/ImportDataConverter.ts @@ -2,18 +2,17 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; import _ from 'underscore'; -import { ImportData } from '../models/ImportData'; -import { IImportUser } from '../definitions/IImportUser'; -import { IImportMessage, IImportMessageReaction } from '../definitions/IImportMessage'; -import { IImportChannel } from '../definitions/IImportChannel'; -import { IImportUserRecord, IImportChannelRecord, IImportMessageRecord } from '../definitions/IImportRecord'; -import { Users, Rooms, Subscriptions } from '../../../models/server'; -import { generateUsernameSuggestion, insertMessage } from '../../../lib/server'; +import { ImportData as ImportDataRaw } from '../../../models/server/raw'; +import { IImportUser } from '../../../../definition/IImportUser'; +import { IImportMessage, IImportMessageReaction } from '../../../../definition/IImportMessage'; +import { IImportChannel } from '../../../../definition/IImportChannel'; +import { IConversionCallbacks } from '../definitions/IConversionCallbacks'; +import { IImportUserRecord, IImportChannelRecord, IImportMessageRecord } from '../../../../definition/IImportRecord'; +import { Users, Rooms, Subscriptions, ImportData } from '../../../models/server'; +import { generateUsernameSuggestion, insertMessage, saveUserIdentity, addUserToDefaultChannels } from '../../../lib/server'; import { setUserActiveStatus } from '../../../lib/server/functions/setUserActiveStatus'; -import { IUser } from '../../../../definition/IUser'; - -// @ts-ignore //@ToDo: Add the Logger class definitions. -type FakeLogger = Logger; +import { IUser, IUserEmail } from '../../../../definition/IUser'; +import type { Logger } from '../../../../server/lib/logger/Logger'; type IRoom = Record; type IMessage = Record; @@ -38,14 +37,9 @@ type IMessageReaction = { type IMessageReactions = Record; -interface IConversionCallbacks { - beforeImportFn?: { - (data: IImportUser | IImportChannel | IImportMessage, type: string): boolean; - }; - afterImportFn?: { - (data: IImportUser | IImportChannel | IImportMessage, type: string): void; - }; -} +export type IConverterOptions = { + flagEmailsAsVerified?: boolean; +}; const guessNameFromUsername = (username: string): string => username @@ -64,16 +58,25 @@ export class ImportDataConverter { private _roomNameCache: Map; - private _logger: FakeLogger; + private _logger: Logger; + + private _options: IConverterOptions; - constructor() { + public get options(): IConverterOptions { + return this._options; + } + + constructor(options?: IConverterOptions) { + this._options = options || { + flagEmailsAsVerified: false, + }; this._userCache = new Map(); this._userDisplayNameCache = new Map(); this._roomCache = new Map(); this._roomNameCache = new Map(); } - setLogger(logger: FakeLogger): void { + setLogger(logger: Logger): void { this._logger = logger; } @@ -113,7 +116,7 @@ export class ImportDataConverter { this.addUserToCache(userData.importIds[0], userData._id, userData.username); } - addObject(type: string, data: Record, options: Record = {}): void { + protected addObject(type: string, data: Record, options: Record = {}): void { ImportData.model.rawCollection().insert({ data, dataType: type, @@ -135,17 +138,7 @@ export class ImportDataConverter { }); } - updateUserId(_id: string, userData: IImportUser): void { - const updateData: Record = { - $set: { - statusText: userData.statusText || undefined, - roles: userData.roles || ['user'], - type: userData.type || 'user', - bio: userData.bio || undefined, - name: userData.name || undefined, - }, - }; - + addUserImportId(updateData: Record, userData: IImportUser): void { if (userData.importIds?.length) { updateData.$addToSet = { importIds: { @@ -153,26 +146,120 @@ export class ImportDataConverter { }, }; } + } - Users.update({ _id }, updateData); + addUserEmails(updateData: Record, userData: IImportUser, existingEmails: Array): void { + if (!userData.emails?.length) { + return; + } + + const verifyEmails = Boolean(this.options.flagEmailsAsVerified); + const newEmailList: Array = []; + + for (const email of userData.emails) { + const verified = verifyEmails || existingEmails.find((ee) => ee.address === email)?.verified || false; + + newEmailList.push({ + address: email, + verified, + }); + } + + updateData.$set.emails = newEmailList; } - updateUser(existingUser: IUser, userData: IImportUser): void { - userData._id = existingUser._id; + addUserServices(updateData: Record, userData: IImportUser): void { + if (!userData.services) { + return; + } - this.updateUserId(userData._id, userData); + for (const serviceKey in userData.services) { + if (!userData.services[serviceKey]) { + continue; + } - if (userData.importIds.length) { - this.addUserToCache(userData.importIds[0], existingUser._id, existingUser.username); + const service = userData.services[serviceKey]; + + for (const key in service) { + if (!service[key]) { + continue; + } + + updateData.$set[`services.${ serviceKey }.${ key }`] = service[key]; + } } + } - if (userData.avatarUrl) { - try { - Users.update({ _id: existingUser._id }, { $set: { _pendingAvatarUrl: userData.avatarUrl } }); - } catch (error) { - this._logger.warn(`Failed to set ${ existingUser._id }'s avatar from url ${ userData.avatarUrl }`); - this._logger.error(error); + addCustomFields(updateData: Record, userData: IImportUser): void { + if (!userData.customFields) { + return; + } + + const subset = (source: Record, currentPath: string): void => { + for (const key in source) { + if (!source.hasOwnProperty(key)) { + continue; + } + + const keyPath = `${ currentPath }.${ key }`; + if (typeof source[key] === 'object' && !Array.isArray(source[key])) { + subset(source[key], keyPath); + continue; + } + + updateData.$set[keyPath] = source[key]; } + }; + + subset(userData.customFields, 'customFields'); + } + + updateUser(existingUser: IUser, userData: IImportUser): void { + const { _id } = existingUser; + + userData._id = _id; + + if (!userData.roles && !existingUser.roles) { + userData.roles = ['user']; + } + if (!userData.type && !existingUser.type) { + userData.type = 'user'; + } + + // #ToDo: #TODO: Move this to the model class + const updateData: Record = { + $set: { + ...userData.roles && { roles: userData.roles }, + ...userData.type && { type: userData.type }, + ...userData.statusText && { statusText: userData.statusText }, + ...userData.bio && { bio: userData.bio }, + ...userData.services?.ldap && { ldap: true }, + ...userData.avatarUrl && { _pendingAvatarUrl: userData.avatarUrl }, + }, + }; + + this.addCustomFields(updateData, userData); + this.addUserServices(updateData, userData); + this.addUserImportId(updateData, userData); + this.addUserEmails(updateData, userData, existingUser.emails || []); + + if (Object.keys(updateData.$set).length === 0) { + delete updateData.$set; + } + if (Object.keys(updateData).length > 0) { + Users.update({ _id }, updateData); + } + + if (userData.utcOffset) { + Users.setUtcOffset(_id, userData.utcOffset); + } + + if (userData.name || userData.username) { + saveUserIdentity({ _id, name: userData.name, username: userData.username }); + } + + if (userData.importIds.length) { + this.addUserToCache(userData.importIds[0], existingUser._id, existingUser.username); } } @@ -188,41 +275,35 @@ export class ImportDataConverter { joinDefaultChannelsSilenced: true, }); - userData._id = userId; const user = Users.findOneById(userId, {}); + this.updateUser(user, userData); - if (user && userData.importIds.length) { - this.addUserToCache(userData.importIds[0], user._id, userData.username); - } + addUserToDefaultChannels(user, true); + return user; + } - Meteor.runAsUser(userId, () => { - Meteor.call('setUsername', userData.username, { joinDefaultChannelsSilenced: true }); - if (userData.name) { - Users.setName(userId, userData.name); - } + protected async getUsersToImport(): Promise> { + return ImportDataRaw.getAllUsers().toArray(); + } - this.updateUserId(userId, userData); + findExistingUser(data: IImportUser): IUser | undefined { + if (data.emails.length) { + const emailUser = Users.findOneByEmailAddress(data.emails[0], {}); - if (userData.utcOffset) { - Users.setUtcOffset(userId, userData.utcOffset); + if (emailUser) { + return emailUser; } + } - if (userData.avatarUrl) { - try { - Users.update({ _id: userId }, { $set: { _pendingAvatarUrl: userData.avatarUrl } }); - } catch (error) { - this._logger.warn(`Failed to set ${ userId }'s avatar from url ${ userData.avatarUrl }`); - this._logger.error(error); - } - } - }); - - return user; + // If we couldn't find one by their email address, try to find an existing user by their username + if (data.username) { + return Users.findOneByUsernameIgnoringCase(data.username, {}); + } } - convertUsers({ beforeImportFn, afterImportFn }: IConversionCallbacks = {}): void { - const users = ImportData.find({ dataType: 'user' }); - users.forEach(({ data, _id }: IImportUserRecord) => { + public convertUsers({ beforeImportFn, afterImportFn }: IConversionCallbacks = {}): void { + const users = Promise.await(this.getUsersToImport()); + users.forEach(({ data, _id }) => { try { if (beforeImportFn && !beforeImportFn(data, 'user')) { this.skipRecord(_id); @@ -236,23 +317,16 @@ export class ImportDataConverter { throw new Error('importer-user-missing-email-and-username'); } - let existingUser; - if (data.emails.length) { - existingUser = Users.findOneByEmailAddress(data.emails[0], {}); - } - - if (data.username) { - // If we couldn't find one by their email address, try to find an existing user by their username - if (!existingUser) { - existingUser = Users.findOneByUsernameIgnoringCase(data.username, {}); - } - } else { + let existingUser = this.findExistingUser(data); + if (!data.username) { data.username = generateUsernameSuggestion({ name: data.name, emails: data.emails, }); } + const isNewUser = !existingUser; + if (existingUser) { this.updateUser(existingUser, data); } else { @@ -266,28 +340,21 @@ export class ImportDataConverter { // Deleted users are 'inactive' users in Rocket.Chat if (data.deleted && existingUser?.active) { setUserActiveStatus(data._id, false, true); + } else if (data.deleted === false && existingUser?.active === false) { + setUserActiveStatus(data._id, true); } if (afterImportFn) { - afterImportFn(data, 'user'); + afterImportFn(data, 'user', isNewUser); } } catch (e) { + this._logger.error(e); this.saveError(_id, e); } }); } - saveNewId(importId: string, newId: string): void { - ImportData.update({ - _id: importId, - }, { - $set: { - id: newId, - }, - }); - } - - saveError(importId: string, error: Error): void { + protected saveError(importId: string, error: Error): void { this._logger.error(error); ImportData.update({ _id: importId, @@ -301,7 +368,7 @@ export class ImportDataConverter { }); } - skipRecord(_id: string): void { + protected skipRecord(_id: string): void { ImportData.update({ _id, }, { @@ -424,9 +491,13 @@ export class ImportDataConverter { return result; } + protected async getMessagesToImport(): Promise> { + return ImportDataRaw.getAllMessages().toArray(); + } + convertMessages({ beforeImportFn, afterImportFn }: IConversionCallbacks = {}): void { const rids: Array = []; - const messages = ImportData.find({ dataType: 'message' }); + const messages = Promise.await(this.getMessagesToImport()); messages.forEach(({ data: m, _id }: IImportMessageRecord) => { try { if (beforeImportFn && !beforeImportFn(m, 'message')) { @@ -498,7 +569,7 @@ export class ImportDataConverter { } if (afterImportFn) { - afterImportFn(m, 'message'); + afterImportFn(m, 'message', true); } } catch (e) { this.saveError(_id, e); @@ -528,7 +599,7 @@ export class ImportDataConverter { this.updateRoomId(room._id, roomData); } - findDMForImportedUsers(...users: Array): IImportChannel | undefined { + public findDMForImportedUsers(...users: Array): IImportChannel | undefined { const record = ImportData.findDMForImportedUsers(...users); if (record) { return record.data; @@ -709,7 +780,7 @@ export class ImportDataConverter { roomData._id = roomInfo.rid; }); } catch (e) { - this._logger.warn(roomData.name, members); + this._logger.warn({ msg: 'Failed to create new room', name: roomData.name, members }); this._logger.error(e); throw e; } @@ -768,8 +839,12 @@ export class ImportDataConverter { return Rooms.findOneByNonValidatedName(data.name, {}); } + protected async getChannelsToImport(): Promise> { + return ImportDataRaw.getAllChannels().toArray(); + } + convertChannels(startedByUserId: string, { beforeImportFn, afterImportFn }: IConversionCallbacks = {}): void { - const channels = ImportData.find({ dataType: 'channel' }); + const channels = Promise.await(this.getChannelsToImport()); channels.forEach(({ data: c, _id }: IImportChannelRecord) => { try { if (beforeImportFn && !beforeImportFn(c, 'channel')) { @@ -801,7 +876,7 @@ export class ImportDataConverter { } if (afterImportFn) { - afterImportFn(c, 'channel'); + afterImportFn(c, 'channel', !existingRoom); } } catch (e) { this.saveError(_id, e); @@ -824,11 +899,9 @@ export class ImportDataConverter { }); } - clearImportData(): void { - const rawCollection = ImportData.model.rawCollection(); - const remove = Meteor.wrapAsync(rawCollection.remove, rawCollection); - - remove({}); + public clearImportData(): void { + // Using raw collection since its faster + Promise.await(ImportData.model.rawCollection().remove({})); } clearSuccessfullyImportedData(): void { diff --git a/app/importer/server/classes/ImporterBase.js b/app/importer/server/classes/ImporterBase.js index decfd093543f6..29767fdafeadd 100644 --- a/app/importer/server/classes/ImporterBase.js +++ b/app/importer/server/classes/ImporterBase.js @@ -14,7 +14,7 @@ import { RawImports } from '../models/RawImports'; import { Settings, Imports } from '../../../models'; import { Logger } from '../../../logger'; import { ImportDataConverter } from './ImportDataConverter'; -import { ImportData } from '../models/ImportData'; +import { ImportData } from '../../../models/server'; import { t } from '../../../utils/server'; import { Selection, @@ -54,7 +54,7 @@ export class Base { this.info = info; - this.logger = new Logger(`${ this.info.name } Importer`, {}); + this.logger = new Logger(`${ this.info.name } Importer`); this.converter.setLogger(this.logger); this.progress = new Progress(this.info.key, this.info.name); diff --git a/app/importer/server/classes/VirtualDataConverter.ts b/app/importer/server/classes/VirtualDataConverter.ts new file mode 100644 index 0000000000000..5baf334e394bb --- /dev/null +++ b/app/importer/server/classes/VirtualDataConverter.ts @@ -0,0 +1,149 @@ +import { Random } from 'meteor/random'; + +import type { IImportUserRecord, IImportChannelRecord, IImportMessageRecord, IImportRecord, IImportRecordType, IImportData } from '../../../../definition/IImportRecord'; +import { IImportChannel } from '../../../../definition/IImportChannel'; +import { ImportDataConverter } from './ImportDataConverter'; +import type { IConverterOptions } from './ImportDataConverter'; + +export class VirtualDataConverter extends ImportDataConverter { + protected _userRecords: Array; + + protected _channelRecords: Array; + + protected _messageRecords: Array; + + protected useVirtual: boolean; + + constructor(virtual = true, options?: IConverterOptions) { + super(options); + + this.useVirtual = virtual; + if (virtual) { + this.clearVirtualData(); + } + } + + public clearImportData(): void { + if (!this.useVirtual) { + return super.clearImportData(); + } + + this.clearVirtualData(); + } + + public clearSuccessfullyImportedData(): void { + if (!this.useVirtual) { + return super.clearSuccessfullyImportedData(); + } + + this.clearVirtualData(); + } + + public findDMForImportedUsers(...users: Array): IImportChannel | undefined { + if (!this.useVirtual) { + return super.findDMForImportedUsers(...users); + } + + // The original method is only used by the hipchat importer so we probably don't need to implement this on the virtual converter. + return undefined; + } + + protected addObject(type: IImportRecordType, data: IImportData, options: Record = {}): void { + if (!this.useVirtual) { + return super.addObject(type, data, options); + } + + const list = this.getObjectList(type); + + list.push({ + _id: Random.id(), + data, + dataType: type, + ...options, + }); + } + + protected async getUsersToImport(): Promise> { + if (!this.useVirtual) { + return super.getUsersToImport(); + } + + return this._userRecords; + } + + protected saveError(importId: string, error: Error): void { + if (!this.useVirtual) { + return super.saveError(importId, error); + } + + const record = this.getVirtualRecordById(importId); + + if (!record) { + return; + } + + if (!record.errors) { + record.errors = []; + } + + record.errors.push({ + message: error.message, + stack: error.stack, + }); + } + + protected skipRecord(_id: string): void { + if (!this.useVirtual) { + return super.skipRecord(_id); + } + + const record = this.getVirtualRecordById(_id); + + if (record) { + record.skipped = true; + } + } + + protected async getMessagesToImport(): Promise { + if (!this.useVirtual) { + return super.getMessagesToImport(); + } + + return this._messageRecords; + } + + protected async getChannelsToImport(): Promise { + if (!this.useVirtual) { + return super.getChannelsToImport(); + } + + return this._channelRecords; + } + + private clearVirtualData(): void { + this._userRecords = []; + this._channelRecords = []; + this._messageRecords = []; + } + + private getObjectList(type: IImportRecordType): Array { + switch (type) { + case 'user': + return this._userRecords; + case 'channel': + return this._channelRecords; + case 'message': + return this._messageRecords; + } + } + + private getVirtualRecordById(id: string): IImportRecord | undefined { + for (const store of [this._userRecords, this._channelRecords, this._messageRecords]) { + for (const record of store) { + if (record._id === id) { + return record; + } + } + } + } +} diff --git a/app/importer/server/definitions/IConversionCallbacks.ts b/app/importer/server/definitions/IConversionCallbacks.ts new file mode 100644 index 0000000000000..80df62fc6f9ea --- /dev/null +++ b/app/importer/server/definitions/IConversionCallbacks.ts @@ -0,0 +1,11 @@ +import { IImportUser } from '../../../../definition/IImportUser'; +import { IImportMessage } from '../../../../definition/IImportMessage'; +import { IImportChannel } from '../../../../definition/IImportChannel'; + +export type ImporterBeforeImportCallback = {(data: IImportUser | IImportChannel | IImportMessage, type: string): boolean} +export type ImporterAfterImportCallback = {(data: IImportUser | IImportChannel | IImportMessage, type: string, isNewRecord: boolean): void}; + +export interface IConversionCallbacks { + beforeImportFn?: ImporterBeforeImportCallback; + afterImportFn?: ImporterAfterImportCallback; +} diff --git a/app/importer/server/definitions/IImportRecord.ts b/app/importer/server/definitions/IImportRecord.ts deleted file mode 100644 index 09d3a9418ef46..0000000000000 --- a/app/importer/server/definitions/IImportRecord.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { IImportUser } from './IImportUser'; -import { IImportChannel } from './IImportChannel'; -import { IImportMessage } from './IImportMessage'; - -export interface IImportRecord { - data: IImportUser | IImportChannel | IImportMessage; - dataType: 'user' | 'channel' | 'message'; - _id: string; - options?: {}; -} - -export interface IImportUserRecord extends IImportRecord { - data: IImportUser; - dataType: 'user'; -} - -export interface IImportChannelRecord extends IImportRecord { - data: IImportChannel; - dataType: 'channel'; -} - -export interface IImportMessageRecord extends IImportRecord { - data: IImportMessage; - dataType: 'message'; - options: { - useQuickInsert?: boolean; - }; -} diff --git a/app/importer/server/index.js b/app/importer/server/index.js index dd9e89ba02096..0fed91e5b6608 100644 --- a/app/importer/server/index.js +++ b/app/importer/server/index.js @@ -2,7 +2,6 @@ import { Base } from './classes/ImporterBase'; import { ImporterWebsocket } from './classes/ImporterWebsocket'; import { Progress } from './classes/ImporterProgress'; import { RawImports } from './models/RawImports'; -import { ImportData } from './models/ImportData'; import { Selection } from './classes/ImporterSelection'; import { SelectionChannel } from './classes/ImporterSelectionChannel'; import { SelectionUser } from './classes/ImporterSelectionUser'; @@ -26,7 +25,6 @@ export { Progress, ProgressStep, RawImports, - ImportData, Selection, SelectionChannel, SelectionUser, diff --git a/app/importer/server/methods/getLatestImportOperations.js b/app/importer/server/methods/getLatestImportOperations.js index 14980ace21b60..b7e97cc8a40ea 100644 --- a/app/importer/server/methods/getLatestImportOperations.js +++ b/app/importer/server/methods/getLatestImportOperations.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; -import { Imports } from '../../../models'; -import { hasRole } from '../../../authorization'; +import { Imports } from '../../../models/server'; +import { hasRole } from '../../../authorization/server'; Meteor.methods({ getLatestImportOperations() { diff --git a/app/importer/server/models/ImportData.ts b/app/importer/server/models/ImportData.ts deleted file mode 100644 index a6afb291e19c1..0000000000000 --- a/app/importer/server/models/ImportData.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { Base } from '../../../models/server'; -import { IImportUserRecord, IImportChannelRecord } from '../definitions/IImportRecord'; - -class ImportDataModel extends Base { - constructor() { - super('import_data'); - } - - getAllUsersForSelection(): Array { - return this.find({ - dataType: 'user', - }, { - fields: { - 'data.importIds': 1, - 'data.username': 1, - 'data.emails': 1, - 'data.deleted': 1, - 'data.type': 1, - }, - }).fetch(); - } - - getAllChannelsForSelection(): Array { - return this.find({ - dataType: 'channel', - 'data.t': { - $ne: 'd', - }, - }, { - fields: { - 'data.importIds': 1, - 'data.name': 1, - 'data.archived': 1, - 'data.t': 1, - }, - }).fetch(); - } - - checkIfDirectMessagesExists(): boolean { - return this.find({ - dataType: 'channel', - 'data.t': 'd', - }, { - fields: { - _id: 1, - }, - }).count() > 0; - } - - countMessages(): number { - return this.find({ - dataType: 'message', - }).count(); - } - - findChannelImportIdByNameOrImportId(channelIdentifier: string): string | undefined { - const channel = this.findOne({ - dataType: 'channel', - $or: [ - { - 'data.name': channelIdentifier, - }, - { - 'data.importIds': channelIdentifier, - }, - ], - }, { - fields: { - 'data.importIds': 1, - }, - }); - - return channel?.data?.importIds?.shift(); - } - - findDMForImportedUsers(...users: Array): IImportChannelRecord | undefined { - const query = { - dataType: 'channel', - 'data.users': { - $all: users, - }, - }; - - return this.findOne(query); - } -} - -export const ImportData = new ImportDataModel(); diff --git a/app/importer/server/startup/setImportsToInvalid.js b/app/importer/server/startup/setImportsToInvalid.js index a7ec863732225..431d4040ab79a 100644 --- a/app/importer/server/startup/setImportsToInvalid.js +++ b/app/importer/server/startup/setImportsToInvalid.js @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; -import { Imports } from '../../../models'; +import { Imports } from '../../../models/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { RawImports } from '../models/RawImports'; import { ProgressStep } from '../../lib/ImporterProgressStep'; @@ -8,7 +9,7 @@ function runDrop(fn) { try { fn(); } catch (e) { - console.log('error', e); // TODO: Remove + SystemLogger.error('error', e); // TODO: Remove // ignored } } diff --git a/app/integrations/server/api/api.js b/app/integrations/server/api/api.js index 63d32c9be3386..eb223c67c9ad6 100644 --- a/app/integrations/server/api/api.js +++ b/app/integrations/server/api/api.js @@ -10,10 +10,11 @@ import _ from 'underscore'; import s from 'underscore.string'; import moment from 'moment'; -import { logger } from '../logger'; -import { processWebhookMessage } from '../../../lib'; +import { incomingLogger } from '../logger'; +import { processWebhookMessage } from '../../../lib/server'; import { API, APIClass, defaultRateLimiterOptions } from '../../../api/server'; -import * as Models from '../../../models'; +import * as Models from '../../../models/server'; +import { Integrations } from '../../../models/server/raw'; import { settings } from '../../../settings/server'; const compiledScripts = {}; @@ -63,8 +64,8 @@ function getIntegrationScript(integration) { const script = integration.scriptCompiled; const { sandbox, store } = buildSandbox(); try { - logger.incoming.info('Will evaluate script of Trigger', integration.name); - logger.incoming.debug(script); + incomingLogger.info({ msg: 'Will evaluate script of Trigger', name: integration.name }); + incomingLogger.debug(script); const vmScript = vm.createScript(script, 'script.js'); vmScript.runInNewContext(sandbox); @@ -77,23 +78,20 @@ function getIntegrationScript(integration) { return compiledScripts[integration._id].script; } - } catch ({ stack }) { - logger.incoming.error('[Error evaluating Script in Trigger', integration.name, ':]'); - logger.incoming.error(script.replace(/^/gm, ' ')); - logger.incoming.error('[Stack:]'); - logger.incoming.error(stack.replace(/^/gm, ' ')); + } catch (err) { + incomingLogger.error({ msg: 'Error evaluating Script in Trigger', name: integration.name, script, err }); throw API.v1.failure('error-evaluating-script'); } if (!sandbox.Script) { - logger.incoming.error('[Class "Script" not in Trigger', integration.name, ']'); + incomingLogger.error({ msg: 'Class "Script" not in Trigger', name: integration.name }); throw API.v1.failure('class-script-not-found'); } } function createIntegration(options, user) { - logger.incoming.info('Add integration', options.name); - logger.incoming.debug(options); + incomingLogger.info({ msg: 'Add integration', name: options.name }); + incomingLogger.debug(options); Meteor.runAsUser(user._id, function() { switch (options.event) { @@ -129,12 +127,13 @@ function createIntegration(options, user) { } function removeIntegration(options, user) { - logger.incoming.info('Remove integration'); - logger.incoming.debug(options); + 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)); @@ -142,9 +141,8 @@ function removeIntegration(options, user) { } function executeIntegrationRest() { - logger.incoming.info('Post integration:', this.integration.name); - logger.incoming.debug('@urlParams:', this.urlParams); - logger.incoming.debug('@bodyParams:', this.bodyParams); + incomingLogger.info({ msg: 'Post integration:', name: this.integration.name }); + incomingLogger.debug({ urlParams: this.urlParams, bodyParams: this.bodyParams }); if (this.integration.enabled !== true) { return { @@ -165,7 +163,7 @@ function executeIntegrationRest() { try { script = getIntegrationScript(this.integration); } catch (e) { - logger.incoming.warn(e); + incomingLogger.error(e); return API.v1.failure(e.message); } @@ -214,7 +212,7 @@ function executeIntegrationRest() { })).wait(); if (!result) { - logger.incoming.debug('[Process Incoming Request result of Trigger', this.integration.name, ':] No data'); + incomingLogger.debug({ msg: 'Process Incoming Request result of Trigger has no data', name: this.integration.name }); return API.v1.success(); } if (result && result.error) { return API.v1.failure(result.error); @@ -226,13 +224,9 @@ function executeIntegrationRest() { this.user = result.user; } - logger.incoming.debug('[Process Incoming Request result of Trigger', this.integration.name, ':]'); - logger.incoming.debug('result', this.bodyParams); - } catch ({ stack }) { - logger.incoming.error('[Error running Script in Trigger', this.integration.name, ':]'); - logger.incoming.error(this.integration.scriptCompiled.replace(/^/gm, ' ')); - logger.incoming.error('[Stack:]'); - logger.incoming.error(stack.replace(/^/gm, ' ')); + incomingLogger.debug({ msg: 'Process Incoming Request result of Trigger', name: this.integration.name, result: this.bodyParams }); + } catch (err) { + incomingLogger.error({ msg: 'Error running Script in Trigger', name: this.integration.name, script: this.integration.scriptCompiled, err }); return API.v1.failure('error-running-script'); } } @@ -247,13 +241,13 @@ function executeIntegrationRest() { this.bodyParams.bot = { i: this.integration._id }; try { - const message = processWebhookMessage(this.bodyParams, this.user, defaultValues, this.integration); + const message = processWebhookMessage(this.bodyParams, this.user, defaultValues); if (_.isEmpty(message)) { return API.v1.failure('unknown-error'); } if (this.scriptResponse) { - logger.incoming.debug('response', this.scriptResponse); + incomingLogger.debug({ msg: 'response', response: this.scriptResponse }); } return API.v1.success(this.scriptResponse); @@ -271,7 +265,7 @@ function removeIntegrationRest() { } function integrationSampleRest() { - logger.incoming.info('Sample Integration'); + incomingLogger.info('Sample Integration'); return { statusCode: 200, body: [ @@ -308,7 +302,7 @@ function integrationSampleRest() { } function integrationInfoRest() { - logger.incoming.info('Info integration'); + incomingLogger.info('Info integration'); return { statusCode: 200, body: { @@ -381,13 +375,13 @@ 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) { - logger.incoming.info('Invalid integration id', this.request.params.integrationId, 'or token', this.request.params.token); + incomingLogger.info(`Invalid integration id ${ this.request.params.integrationId } or token ${ this.request.params.token }`); return { error: { diff --git a/app/integrations/server/lib/triggerHandler.js b/app/integrations/server/lib/triggerHandler.js index a7cd0a682c4ac..27f71a4e5729d 100644 --- a/app/integrations/server/lib/triggerHandler.js +++ b/app/integrations/server/lib/triggerHandler.js @@ -9,10 +9,11 @@ import moment from 'moment'; import Fiber from 'fibers'; import Future from 'fibers/future'; -import * as Models from '../../../models'; -import { settings } from '../../../settings'; -import { getRoomByNameOrIdWithOptionToJoin, processWebhookMessage } from '../../../lib'; -import { logger } from '../logger'; +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'; import { integrations } from '../../lib/rocketchat'; export class RocketChatIntegrationHandler { @@ -22,21 +23,21 @@ 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) { - logger.outgoing.debug(`Adding the integration ${ record.name } of the event ${ record.event }!`); + outgoingLogger.debug(`Adding the integration ${ record.name } of the event ${ record.event }!`); let channels; if (record.event && !integrations.outgoingEvents[record.event].use.channel) { - logger.outgoing.debug('The integration doesnt rely on channels.'); + outgoingLogger.debug('The integration doesnt rely on channels.'); // We don't use any channels, so it's special ;) channels = ['__any']; } else if (_.isEmpty(record.channel)) { - logger.outgoing.debug('The integration had an empty channel property, so it is going on all the public channels.'); + outgoingLogger.debug('The integration had an empty channel property, so it is going on all the public channels.'); channels = ['all_public_channels']; } else { - logger.outgoing.debug('The integration is going on these channels:', record.channel); + outgoingLogger.debug('The integration is going on these channels:', record.channel); channels = [].concat(record.channel); } @@ -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. @@ -172,11 +173,11 @@ export class RocketChatIntegrationHandler { // If no room could be found, we won't be sending any messages but we'll warn in the logs if (!tmpRoom) { - logger.outgoing.warn(`The Integration "${ trigger.name }" doesn't have a room configured nor did it provide a room to send the message to.`); + outgoingLogger.warn(`The Integration "${ trigger.name }" doesn't have a room configured nor did it provide a room to send the message to.`); return; } - logger.outgoing.debug(`Found a room for ${ trigger.name } which is: ${ tmpRoom.name } with a type of ${ tmpRoom.t }`); + outgoingLogger.debug(`Found a room for ${ trigger.name } which is: ${ tmpRoom.name } with a type of ${ tmpRoom.t }`); message.bot = { i: trigger._id }; @@ -192,7 +193,7 @@ export class RocketChatIntegrationHandler { message.channel = `#${ tmpRoom._id }`; } - message = processWebhookMessage(message, user, defaultValues, trigger); + message = processWebhookMessage(message, user, defaultValues); return message; } @@ -240,8 +241,8 @@ export class RocketChatIntegrationHandler { let vmScript; try { - logger.outgoing.info('Will evaluate script of Trigger', integration.name); - logger.outgoing.debug(script); + outgoingLogger.info({ msg: 'Will evaluate script of Trigger', name: integration.name }); + outgoingLogger.debug(script); vmScript = this.vm.createScript(script, 'script.js'); @@ -256,16 +257,13 @@ export class RocketChatIntegrationHandler { return this.compiledScripts[integration._id].script; } - } catch (e) { - logger.outgoing.error(`Error evaluating Script in Trigger ${ integration.name }:`); - logger.outgoing.error(script.replace(/^/gm, ' ')); - logger.outgoing.error('Stack Trace:'); - logger.outgoing.error(e.stack.replace(/^/gm, ' ')); + } catch (err) { + outgoingLogger.error({ msg: 'Error evaluating Script in Trigger', name: integration.name, script, err }); throw new Meteor.Error('error-evaluating-script'); } if (!sandbox.Script) { - logger.outgoing.error(`Class "Script" not in Trigger ${ integration.name }:`); + outgoingLogger.error(`Class "Script" not in Trigger ${ integration.name }:`); throw new Meteor.Error('class-script-not-found'); } } @@ -295,7 +293,7 @@ export class RocketChatIntegrationHandler { } if (!script[method]) { - logger.outgoing.error(`Method "${ method }" no found in the Integration "${ integration.name }"`); + outgoingLogger.error(`Method "${ method }" no found in the Integration "${ integration.name }"`); this.updateHistory({ historyId, step: `execute-script-no-method-${ method }` }); return; } @@ -323,16 +321,13 @@ export class RocketChatIntegrationHandler { timeout: 3000, })).wait(); - logger.outgoing.debug(`Script method "${ method }" result of the Integration "${ integration.name }" is:`); - logger.outgoing.debug(result); + outgoingLogger.debug({ msg: `Script method "${ method }" result of the Integration "${ integration.name }" is:`, result }); return result; - } catch (e) { - this.updateHistory({ historyId, step: `execute-script-error-running-${ method }`, error: true, errorStack: e.stack.replace(/^/gm, ' ') }); - logger.outgoing.error(`Error running Script in the Integration ${ integration.name }:`); - logger.outgoing.debug(integration.scriptCompiled.replace(/^/gm, ' ')); // Only output the compiled script if debugging is enabled, so the logs don't get spammed. - logger.outgoing.error('Stack:'); - logger.outgoing.error(e.stack.replace(/^/gm, ' ')); + } catch (err) { + this.updateHistory({ historyId, step: `execute-script-error-running-${ method }`, error: true, errorStack: err.stack.replace(/^/gm, ' ') }); + outgoingLogger.error({ msg: 'Error running Script in the Integration', name: integration.name, err }); + outgoingLogger.debug({ msg: 'Error running Script in the Integration', name: integration.name, script: integration.scriptCompiled }); // Only output the compiled script if debugging is enabled, so the logs don't get spammed. } } @@ -381,12 +376,12 @@ export class RocketChatIntegrationHandler { } break; default: - logger.outgoing.warn(`An Unhandled Trigger Event was called: ${ argObject.event }`); + outgoingLogger.warn(`An Unhandled Trigger Event was called: ${ argObject.event }`); argObject.event = undefined; break; } - logger.outgoing.debug(`Got the event arguments for the event: ${ argObject.event }`, argObject); + outgoingLogger.debug({ msg: `Got the event arguments for the event: ${ argObject.event }`, argObject }); return argObject; } @@ -546,7 +541,7 @@ export class RocketChatIntegrationHandler { } executeTriggers(...args) { - logger.outgoing.debug('Execute Trigger:', args[0]); + outgoingLogger.debug({ msg: 'Execute Trigger:', arg: args[0] }); const argObject = this.eventNameArgumentsToObject(...args); const { event, message, room } = argObject; @@ -558,7 +553,7 @@ export class RocketChatIntegrationHandler { return; } - logger.outgoing.debug('Starting search for triggers for the room:', room ? room._id : '__any'); + outgoingLogger.debug(`Starting search for triggers for the room: ${ room ? room._id : '__any' }`); const triggersToExecute = this.getTriggersToExecute(room, message); @@ -569,10 +564,10 @@ export class RocketChatIntegrationHandler { } } - logger.outgoing.debug(`Found ${ triggersToExecute.length } to iterate over and see if the match the event.`); + outgoingLogger.debug(`Found ${ triggersToExecute.length } to iterate over and see if the match the event.`); for (const triggerToExecute of triggersToExecute) { - logger.outgoing.debug(`Is "${ triggerToExecute.name }" enabled, ${ triggerToExecute.enabled }, and what is the event? ${ triggerToExecute.event }`); + outgoingLogger.debug(`Is "${ triggerToExecute.name }" enabled, ${ triggerToExecute.enabled }, and what is the event? ${ triggerToExecute.event }`); if (triggerToExecute.enabled === true && triggerToExecute.event === event) { this.executeTrigger(triggerToExecute, argObject); } @@ -587,11 +582,11 @@ export class RocketChatIntegrationHandler { executeTriggerUrl(url, trigger, { event, message, room, owner, user }, theHistoryId, tries = 0) { if (!this.isTriggerEnabled(trigger)) { - logger.outgoing.warn(`The trigger "${ trigger.name }" is no longer enabled, stopping execution of it at try: ${ tries }`); + outgoingLogger.warn(`The trigger "${ trigger.name }" is no longer enabled, stopping execution of it at try: ${ tries }`); return; } - logger.outgoing.debug(`Starting to execute trigger: ${ trigger.name } (${ trigger._id })`); + outgoingLogger.debug(`Starting to execute trigger: ${ trigger.name } (${ trigger._id })`); let word; // Not all triggers/events support triggerWords @@ -609,14 +604,14 @@ export class RocketChatIntegrationHandler { // Stop if there are triggerWords but none match if (!word) { - logger.outgoing.debug(`The trigger word which "${ trigger.name }" was expecting could not be found, not executing.`); + outgoingLogger.debug(`The trigger word which "${ trigger.name }" was expecting could not be found, not executing.`); return; } } } if (message && message.editedAt && !trigger.runOnEdits) { - logger.outgoing.debug(`The trigger "${ trigger.name }"'s run on edits is disabled and the message was edited.`); + outgoingLogger.debug(`The trigger "${ trigger.name }"'s run on edits is disabled and the message was edited.`); return; } @@ -634,8 +629,8 @@ export class RocketChatIntegrationHandler { this.mapEventArgsToData(data, { trigger, event, message, room, owner, user }); this.updateHistory({ historyId, step: 'mapped-args-to-data', data, triggerWord: word }); - logger.outgoing.info(`Will be executing the Integration "${ trigger.name }" to the url: ${ url }`); - logger.outgoing.debug(data); + outgoingLogger.info(`Will be executing the Integration "${ trigger.name }" to the url: ${ url }`); + outgoingLogger.debug(data); let opts = { params: {}, @@ -676,9 +671,9 @@ export class RocketChatIntegrationHandler { this.updateHistory({ historyId, step: 'pre-http-call', url: opts.url, httpCallData: opts.data }); HTTP.call(opts.method, opts.url, opts, (error, result) => { if (!result) { - logger.outgoing.warn(`Result for the Integration ${ trigger.name } to ${ url } is empty`); + outgoingLogger.warn(`Result for the Integration ${ trigger.name } to ${ url } is empty`); } else { - logger.outgoing.info(`Status code for the Integration ${ trigger.name } to ${ url } is ${ result.statusCode }`); + outgoingLogger.info(`Status code for the Integration ${ trigger.name } to ${ url } is ${ result.statusCode }`); } this.updateHistory({ historyId, step: 'after-http-call', httpError: error, httpResult: result }); @@ -712,25 +707,22 @@ export class RocketChatIntegrationHandler { // if the result contained nothing or wasn't a successful statusCode if (!result || !this.successResults.includes(result.statusCode)) { if (error) { - logger.outgoing.error(`Error for the Integration "${ trigger.name }" to ${ url } is:`); - logger.outgoing.error(error); + outgoingLogger.error({ msg: `Error for the Integration "${ trigger.name }" to ${ url }`, err: error }); } if (result) { - logger.outgoing.error(`Error for the Integration "${ trigger.name }" to ${ url } is:`); - logger.outgoing.error(result); + outgoingLogger.error({ msg: `Error for the Integration "${ trigger.name }" to ${ url }`, result }); if (result.statusCode === 410) { this.updateHistory({ historyId, step: 'after-process-http-status-410', error: true }); - logger.outgoing.error(`Disabling the Integration "${ trigger.name }" because the status code was 401 (Gone).`); - Models.Integrations.update({ _id: trigger._id }, { $set: { enabled: false } }); + outgoingLogger.error(`Disabling the Integration "${ trigger.name }" because the status code was 401 (Gone).`); + Promise.await(Integrations.updateOne({ _id: trigger._id }, { $set: { enabled: false } })); return; } if (result.statusCode === 500) { this.updateHistory({ historyId, step: 'after-process-http-status-500', error: true }); - logger.outgoing.error(`Error "500" for the Integration "${ trigger.name }" to ${ url }.`); - logger.outgoing.error(result.content); + outgoingLogger.error({ msg: `Error "500" for the Integration "${ trigger.name }" to ${ url }.`, content: result.content }); return; } } @@ -760,7 +752,7 @@ export class RocketChatIntegrationHandler { return; } - logger.outgoing.info(`Trying the Integration ${ trigger.name } to ${ url } again in ${ waitTime } milliseconds.`); + outgoingLogger.info(`Trying the Integration ${ trigger.name } to ${ url } again in ${ waitTime } milliseconds.`); Meteor.setTimeout(() => { this.executeTriggerUrl(url, trigger, { event, message, room, owner, user }, historyId, tries + 1); }, waitTime); diff --git a/app/integrations/server/logger.js b/app/integrations/server/logger.js index 4a293574b9841..dd00762798839 100644 --- a/app/integrations/server/logger.js +++ b/app/integrations/server/logger.js @@ -1,8 +1,6 @@ import { Logger } from '../../logger'; -export const logger = new Logger('Integrations', { - sections: { - incoming: 'Incoming WebHook', - outgoing: 'Outgoing WebHook', - }, -}); +const logger = new Logger('Integrations'); + +export const incomingLogger = logger.section('Incoming WebHook'); +export const outgoingLogger = logger.section('Outgoing WebHook'); diff --git a/app/integrations/server/methods/clearIntegrationHistory.js b/app/integrations/server/methods/clearIntegrationHistory.js deleted file mode 100644 index 87eec581e37aa..0000000000000 --- a/app/integrations/server/methods/clearIntegrationHistory.js +++ /dev/null @@ -1,29 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; -import { IntegrationHistory, Integrations } from '../../../models'; -import notifications from '../../../notifications/server/lib/Notifications'; - -Meteor.methods({ - clearIntegrationHistory(integrationId) { - let integration; - - if (hasPermission(this.userId, 'manage-outgoing-integrations') || hasPermission(this.userId, 'manage-outgoing-integrations', 'bot')) { - integration = Integrations.findOne(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 } }); - } else { - throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'clearIntegrationHistory' }); - } - - if (!integration) { - throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'clearIntegrationHistory' }); - } - - IntegrationHistory.removeByIntegrationId(integrationId); - - notifications.streamIntegrationHistory.emit(integrationId, { type: 'removed' }); - - return true; - }, -}); diff --git a/app/integrations/server/methods/clearIntegrationHistory.ts b/app/integrations/server/methods/clearIntegrationHistory.ts new file mode 100644 index 0000000000000..f4ef3e974b961 --- /dev/null +++ b/app/integrations/server/methods/clearIntegrationHistory.ts @@ -0,0 +1,32 @@ +import { Meteor } from 'meteor/meteor'; + +import { hasPermission } from '../../../authorization/server'; +import { IntegrationHistory, Integrations } from '../../../models/server/raw'; +import notifications from '../../../notifications/server/lib/Notifications'; + +Meteor.methods({ + async clearIntegrationHistory(integrationId) { + let integration; + + if (hasPermission(this.userId, 'manage-outgoing-integrations') || hasPermission(this.userId, 'manage-outgoing-integrations', 'bot')) { + integration = await Integrations.findOneById(integrationId); + } else if (hasPermission(this.userId, 'manage-own-outgoing-integrations') || hasPermission(this.userId, 'manage-own-outgoing-integrations', 'bot')) { + integration = await Integrations.findOne({ + _id: integrationId, + '_createdBy._id': this.userId, + }); + } else { + throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'clearIntegrationHistory' }); + } + + if (!integration) { + throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'clearIntegrationHistory' }); + } + + await IntegrationHistory.removeByIntegrationId(integrationId); + + notifications.streamIntegrationHistory.emit(integrationId, { type: 'removed' }); + + return true; + }, +}); diff --git a/app/integrations/server/methods/incoming/addIncomingIntegration.js b/app/integrations/server/methods/incoming/addIncomingIntegration.js index 6e86dacd5700e..23b339ed48fb4 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.js deleted file mode 100644 index 96c25116a10d2..0000000000000 --- a/app/integrations/server/methods/incoming/deleteIncomingIntegration.js +++ /dev/null @@ -1,26 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../../authorization'; -import { Integrations } from '../../../../models'; - -Meteor.methods({ - deleteIncomingIntegration(integrationId) { - let integration; - - if (hasPermission(this.userId, 'manage-incoming-integrations')) { - integration = Integrations.findOne(integrationId); - } else if (hasPermission(this.userId, 'manage-own-incoming-integrations')) { - integration = Integrations.findOne(integrationId, { fields: { '_createdBy._id': this.userId } }); - } else { - throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'deleteIncomingIntegration' }); - } - - if (!integration) { - throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'deleteIncomingIntegration' }); - } - - Integrations.remove({ _id: integrationId }); - - return true; - }, -}); diff --git a/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts b/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts new file mode 100644 index 0000000000000..bbd158f20ae0f --- /dev/null +++ b/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts @@ -0,0 +1,29 @@ +import { Meteor } from 'meteor/meteor'; + +import { hasPermission } from '../../../../authorization/server'; +import { Integrations } from '../../../../models/server/raw'; + +Meteor.methods({ + async deleteIncomingIntegration(integrationId) { + let integration; + + if (hasPermission(this.userId, 'manage-incoming-integrations')) { + integration = Integrations.findOneById(integrationId); + } else if (hasPermission(this.userId, 'manage-own-incoming-integrations')) { + integration = Integrations.findOne({ + _id: integrationId, + '_createdBy._id': this.userId, + }); + } else { + throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'deleteIncomingIntegration' }); + } + + if (!integration) { + throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'deleteIncomingIntegration' }); + } + + 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 5e7b3517ba0ed..fc5a6d384b951 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 5baf6e88cda45..ae6f1aa6933db 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.js deleted file mode 100644 index 07823b22bb2cb..0000000000000 --- a/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.js +++ /dev/null @@ -1,27 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../../authorization'; -import { IntegrationHistory, Integrations } from '../../../../models'; - -Meteor.methods({ - deleteOutgoingIntegration(integrationId) { - let integration; - - if (hasPermission(this.userId, 'manage-outgoing-integrations') || hasPermission(this.userId, 'manage-outgoing-integrations', 'bot')) { - integration = Integrations.findOne(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 } }); - } else { - throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'deleteOutgoingIntegration' }); - } - - if (!integration) { - throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'deleteOutgoingIntegration' }); - } - - Integrations.remove({ _id: integrationId }); - IntegrationHistory.removeByIntegrationId(integrationId); - - return true; - }, -}); diff --git a/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts b/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts new file mode 100644 index 0000000000000..a63e845eaa77a --- /dev/null +++ b/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts @@ -0,0 +1,30 @@ +import { Meteor } from 'meteor/meteor'; + +import { hasPermission } from '../../../../authorization/server'; +import { IntegrationHistory, Integrations } from '../../../../models/server/raw'; + +Meteor.methods({ + async deleteOutgoingIntegration(integrationId) { + let integration; + + if (hasPermission(this.userId, 'manage-outgoing-integrations') || hasPermission(this.userId, 'manage-outgoing-integrations', 'bot')) { + integration = Integrations.findOneById(integrationId); + } else if (hasPermission(this.userId, 'manage-own-outgoing-integrations') || hasPermission(this.userId, 'manage-own-outgoing-integrations', 'bot')) { + integration = Integrations.findOne({ + _id: integrationId, + '_createdBy._id': this.userId, + }); + } else { + throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'deleteOutgoingIntegration' }); + } + + if (!integration) { + throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'deleteOutgoingIntegration' }); + } + + 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.js deleted file mode 100644 index 8d88cde3ea28d..0000000000000 --- a/app/integrations/server/methods/outgoing/replayOutgoingIntegration.js +++ /dev/null @@ -1,33 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../../authorization'; -import { Integrations, IntegrationHistory } from '../../../../models'; -import { triggerHandler } from '../../lib/triggerHandler'; - -Meteor.methods({ - replayOutgoingIntegration({ integrationId, historyId }) { - let integration; - - if (hasPermission(this.userId, 'manage-outgoing-integrations') || hasPermission(this.userId, 'manage-outgoing-integrations', 'bot')) { - integration = Integrations.findOne(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 } }); - } else { - throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'replayOutgoingIntegration' }); - } - - if (!integration) { - throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'replayOutgoingIntegration' }); - } - - const history = IntegrationHistory.findOneByIntegrationIdAndHistoryId(integration._id, historyId); - - if (!history) { - throw new Meteor.Error('error-invalid-integration-history', 'Invalid Integration History', { method: 'replayOutgoingIntegration' }); - } - - triggerHandler.replay(integration, history); - - return true; - }, -}); diff --git a/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts b/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts new file mode 100644 index 0000000000000..bf3136525bc99 --- /dev/null +++ b/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts @@ -0,0 +1,36 @@ +import { Meteor } from 'meteor/meteor'; + +import { hasPermission } from '../../../../authorization/server'; +import { Integrations, IntegrationHistory } from '../../../../models/server/raw'; +import { triggerHandler } from '../../lib/triggerHandler'; + +Meteor.methods({ + async replayOutgoingIntegration({ integrationId, historyId }) { + let integration; + + if (hasPermission(this.userId, 'manage-outgoing-integrations') || hasPermission(this.userId, 'manage-outgoing-integrations', 'bot')) { + integration = await Integrations.findOneById(integrationId); + } else if (hasPermission(this.userId, 'manage-own-outgoing-integrations') || hasPermission(this.userId, 'manage-own-outgoing-integrations', 'bot')) { + integration = await Integrations.findOne({ + _id: integrationId, + '_createdBy._id': this.userId, + }); + } else { + throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'replayOutgoingIntegration' }); + } + + if (!integration) { + throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'replayOutgoingIntegration' }); + } + + const history = await IntegrationHistory.findOneByIntegrationIdAndHistoryId(integration._id, historyId); + + if (!history) { + throw new Meteor.Error('error-invalid-integration-history', 'Invalid Integration History', { method: 'replayOutgoingIntegration' }); + } + + triggerHandler.replay(integration, history); + + return true; + }, +}); diff --git a/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js b/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js index 981a7890bc290..e9e4bc1ba9682 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 c875a53906d0a..3b608e7121e05 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 476a5f729e092..10d67435237dc 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 eadbe67966b3b..0ea066a8e2879 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 3cf638fd3e942..6fcef0a407883 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 dda8add8b6123..81febb4394423 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/irc/server/irc-bridge/index.js b/app/irc/server/irc-bridge/index.js index c796720c471db..8f296f847e958 100644 --- a/app/irc/server/irc-bridge/index.js +++ b/app/irc/server/irc-bridge/index.js @@ -5,9 +5,13 @@ import _ from 'underscore'; import * as peerCommandHandlers from './peerHandlers'; import * as localCommandHandlers from './localHandlers'; -import { callbacks } from '../../../callbacks'; +import { callbacks } from '../../../callbacks/server'; import * as servers from '../servers'; import { Settings } from '../../../models/server'; +import { Logger } from '../../../logger/server'; + +const logger = new Logger('IRC Bridge'); +const queueLogger = logger.section('Queue'); let removed = false; const updateLastPing = _.throttle(Meteor.bindEnvironment(() => { @@ -82,11 +86,13 @@ class Bridge { * Log helper */ log(message) { - console.log(`[irc][bridge] ${ message }`); + // TODO logger: debug? + logger.info(message); } logQueue(message) { - console.log(`[irc][bridge][queue] ${ message }`); + // TODO logger: debug? + queueLogger.info(message); } /** diff --git a/app/irc/server/irc-bridge/localHandlers/onSaveMessage.js b/app/irc/server/irc-bridge/localHandlers/onSaveMessage.js index a5ebf1ddfb82e..aed761ff127b1 100644 --- a/app/irc/server/irc-bridge/localHandlers/onSaveMessage.js +++ b/app/irc/server/irc-bridge/localHandlers/onSaveMessage.js @@ -1,3 +1,4 @@ +import { SystemLogger } from '../../../../../server/lib/logger/system'; import { Subscriptions, Users } from '../../../../models'; export default function handleOnSaveMessage(message, to) { @@ -21,7 +22,7 @@ export default function handleOnSaveMessage(message, to) { }); if (!toIdentification) { - console.error('[irc][server] Target user not found'); + SystemLogger.error('[irc][server] Target user not found'); return; } } else { diff --git a/app/irc/server/irc-settings.js b/app/irc/server/irc-settings.js deleted file mode 100644 index 9ad6ad0ca5fb0..0000000000000 --- a/app/irc/server/irc-settings.js +++ /dev/null @@ -1,68 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(function() { - settings.addGroup('IRC_Federation', function() { - this.add('IRC_Enabled', false, { - type: 'boolean', - i18nLabel: 'Enabled', - i18nDescription: 'IRC_Enabled', - alert: 'IRC_Enabled_Alert', - }); - - this.add('IRC_Protocol', 'RFC2813', { - type: 'select', - i18nLabel: 'Protocol', - i18nDescription: 'IRC_Protocol', - values: [ - { - key: 'RFC2813', - i18nLabel: 'RFC2813', - }, - ], - }); - - this.add('IRC_Host', 'localhost', { - type: 'string', - i18nLabel: 'Host', - i18nDescription: 'IRC_Host', - }); - - this.add('IRC_Port', 6667, { - type: 'int', - i18nLabel: 'Port', - i18nDescription: 'IRC_Port', - }); - - this.add('IRC_Name', 'irc.rocket.chat', { - type: 'string', - i18nLabel: 'Name', - i18nDescription: 'IRC_Name', - }); - - this.add('IRC_Description', 'Rocket.Chat IRC Bridge', { - type: 'string', - i18nLabel: 'Description', - i18nDescription: 'IRC_Description', - }); - - this.add('IRC_Local_Password', 'password', { - type: 'string', - i18nLabel: 'Local_Password', - i18nDescription: 'IRC_Local_Password', - }); - - this.add('IRC_Peer_Password', 'password', { - type: 'string', - i18nLabel: 'Peer_Password', - i18nDescription: 'IRC_Peer_Password', - }); - - this.add('IRC_Reset_Connection', 'resetIrcConnection', { - type: 'action', - actionText: 'Reset_Connection', - i18nLabel: 'Reset_Connection', - }); - }); -}); diff --git a/app/irc/server/irc-settings.ts b/app/irc/server/irc-settings.ts new file mode 100644 index 0000000000000..9e1bb613e6903 --- /dev/null +++ b/app/irc/server/irc-settings.ts @@ -0,0 +1,68 @@ +import { Meteor } from 'meteor/meteor'; + +import { settingsRegistry } from '../../settings/server'; + +Meteor.startup(function() { + settingsRegistry.addGroup('IRC_Federation', function() { + this.add('IRC_Enabled', false, { + type: 'boolean', + i18nLabel: 'Enabled', + i18nDescription: 'IRC_Enabled', + alert: 'IRC_Enabled_Alert', + }); + + this.add('IRC_Protocol', 'RFC2813', { + type: 'select', + i18nLabel: 'Protocol', + i18nDescription: 'IRC_Protocol', + values: [ + { + key: 'RFC2813', + i18nLabel: 'RFC2813', + }, + ], + }); + + this.add('IRC_Host', 'localhost', { + type: 'string', + i18nLabel: 'Host', + i18nDescription: 'IRC_Host', + }); + + this.add('IRC_Port', 6667, { + type: 'int', + i18nLabel: 'Port', + i18nDescription: 'IRC_Port', + }); + + this.add('IRC_Name', 'irc.rocket.chat', { + type: 'string', + i18nLabel: 'Name', + i18nDescription: 'IRC_Name', + }); + + this.add('IRC_Description', 'Rocket.Chat IRC Bridge', { + type: 'string', + i18nLabel: 'Description', + i18nDescription: 'IRC_Description', + }); + + this.add('IRC_Local_Password', 'password', { + type: 'string', + i18nLabel: 'Local_Password', + i18nDescription: 'IRC_Local_Password', + }); + + this.add('IRC_Peer_Password', 'password', { + type: 'string', + i18nLabel: 'Peer_Password', + i18nDescription: 'IRC_Peer_Password', + }); + + this.add('IRC_Reset_Connection', 'resetIrcConnection', { + type: 'action', + actionText: 'Reset_Connection', + i18nLabel: 'Reset_Connection', + }); + }); +}); diff --git a/app/irc/server/servers/RFC2813/index.js b/app/irc/server/servers/RFC2813/index.js index 6d6335c4017ba..77750b9d10591 100644 --- a/app/irc/server/servers/RFC2813/index.js +++ b/app/irc/server/servers/RFC2813/index.js @@ -5,6 +5,9 @@ import { EventEmitter } from 'events'; import parseMessage from './parseMessage'; import peerCommandHandlers from './peerCommandHandlers'; import localCommandHandlers from './localCommandHandlers'; +import { Logger } from '../../../../logger/server'; + +const logger = new Logger('IRC Server'); class RFC2813 { constructor(config) { @@ -35,7 +38,7 @@ class RFC2813 { this.socket.on('data', this.onReceiveFromPeer.bind(this)); this.socket.on('connect', this.onConnect.bind(this)); - this.socket.on('error', (err) => console.log('[irc][server][err]', err)); + this.socket.on('error', (err) => logger.error(err)); this.socket.on('timeout', () => this.log('Timeout')); this.socket.on('close', () => this.log('Connection Closed')); // Setup local @@ -46,7 +49,8 @@ class RFC2813 { * Log helper */ log(message) { - console.log(`[irc][server] ${ message }`); + // TODO logger: debug? + logger.info(message); } /** @@ -148,11 +152,11 @@ class RFC2813 { const command = peerCommandHandlers[parsedMessage.command].call(this, parsedMessage); if (command) { - this.log(`Emitting peer command to local: ${ JSON.stringify(command) }`); + this.log({ msg: 'Emitting peer command to local', command }); this.emit('peerCommand', command); } } else { - this.log(`Unhandled peer message: ${ JSON.stringify(parsedMessage) }`); + this.log({ msg: 'Unhandled peer message', parsedMessage }); } } }); @@ -171,7 +175,7 @@ class RFC2813 { localCommandHandlers[command].call(this, parameters, this); } else { - this.log(`Unhandled local command: ${ JSON.stringify(command) }`); + this.log({ msg: 'Unhandled local command', command }); } } } diff --git a/app/issuelinks/server/settings.js b/app/issuelinks/server/settings.js deleted file mode 100644 index 94db4a32aa9d7..0000000000000 --- a/app/issuelinks/server/settings.js +++ /dev/null @@ -1,19 +0,0 @@ -import { settings } from '../../settings'; - -settings.add('IssueLinks_Enabled', false, { - type: 'boolean', - i18nLabel: 'Enabled', - i18nDescription: 'IssueLinks_Incompatible', - group: 'Message', - section: 'Issue_Links', - public: true, -}); - -settings.add('IssueLinks_Template', '', { - type: 'string', - i18nLabel: 'IssueLinks_LinkTemplate', - i18nDescription: 'IssueLinks_LinkTemplate_Description', - group: 'Message', - section: 'Issue_Links', - public: true, -}); diff --git a/app/issuelinks/server/settings.ts b/app/issuelinks/server/settings.ts new file mode 100644 index 0000000000000..ad38e55fd6b5c --- /dev/null +++ b/app/issuelinks/server/settings.ts @@ -0,0 +1,19 @@ +import { settingsRegistry } from '../../settings/server'; + +settingsRegistry.add('IssueLinks_Enabled', false, { + type: 'boolean', + i18nLabel: 'Enabled', + i18nDescription: 'IssueLinks_Incompatible', + group: 'Message', + section: 'Issue_Links', + public: true, +}); + +settingsRegistry.add('IssueLinks_Template', '', { + type: 'string', + i18nLabel: 'IssueLinks_LinkTemplate', + i18nDescription: 'IssueLinks_LinkTemplate_Description', + group: 'Message', + section: 'Issue_Links', + public: true, +}); diff --git a/app/katex/server/settings.js b/app/katex/server/settings.js deleted file mode 100644 index 43f1fbbd0b1a4..0000000000000 --- a/app/katex/server/settings.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(function() { - const enableQuery = { - _id: 'Katex_Enabled', - value: true, - }; - settings.add('Katex_Enabled', true, { - type: 'boolean', - group: 'Message', - section: 'Katex', - public: true, - i18n: 'Katex_Enabled_Description', - }); - settings.add('Katex_Parenthesis_Syntax', true, { - type: 'boolean', - group: 'Message', - section: 'Katex', - public: true, - enableQuery, - i18nDescription: 'Katex_Parenthesis_Syntax_Description', - }); - return settings.add('Katex_Dollar_Syntax', false, { - type: 'boolean', - group: 'Message', - section: 'Katex', - public: true, - enableQuery, - i18nDescription: 'Katex_Dollar_Syntax_Description', - }); -}); - -// --- -// generated by coffee-script 1.9.2 diff --git a/app/katex/server/settings.ts b/app/katex/server/settings.ts new file mode 100644 index 0000000000000..1c5cd9381a167 --- /dev/null +++ b/app/katex/server/settings.ts @@ -0,0 +1,36 @@ +import { Meteor } from 'meteor/meteor'; + +import { settingsRegistry } from '../../settings/server'; + +Meteor.startup(function() { + const enableQuery = { + _id: 'Katex_Enabled', + value: true, + }; + settingsRegistry.add('Katex_Enabled', true, { + type: 'boolean', + group: 'Message', + section: 'Katex', + public: true, + i18nDescription: 'Katex_Enabled_Description', + }); + settingsRegistry.add('Katex_Parenthesis_Syntax', true, { + type: 'boolean', + group: 'Message', + section: 'Katex', + public: true, + enableQuery, + i18nDescription: 'Katex_Parenthesis_Syntax_Description', + }); + return settingsRegistry.add('Katex_Dollar_Syntax', false, { + type: 'boolean', + group: 'Message', + section: 'Katex', + public: true, + enableQuery, + i18nDescription: 'Katex_Dollar_Syntax_Description', + }); +}); + +// --- +// generated by coffee-script 1.9.2 diff --git a/app/ldap/client/index.js b/app/ldap/client/index.js deleted file mode 100644 index fecf898e1ae44..0000000000000 --- a/app/ldap/client/index.js +++ /dev/null @@ -1 +0,0 @@ -import './loginHelper'; diff --git a/app/ldap/client/loginHelper.js b/app/ldap/client/loginHelper.js deleted file mode 100644 index b2d6cf5d71ff4..0000000000000 --- a/app/ldap/client/loginHelper.js +++ /dev/null @@ -1,34 +0,0 @@ -// Pass in username, password as normal -// customLdapOptions should be passed in if you want to override LDAP_DEFAULTS -// on any particular call (if you have multiple ldap servers you'd like to connect to) -// You'll likely want to set the dn value here {dn: "..."} -import { Meteor } from 'meteor/meteor'; -import { Accounts } from 'meteor/accounts-base'; - -Meteor.loginWithLDAP = function(...args) { - // Pull username and password - const username = args.shift(); - const password = args.shift(); - - // Check if last argument is a function - // if it is, pop it off and set callback to it - const callback = typeof args[args.length - 1] === 'function' ? args.pop() : null; - - // if args still holds options item, grab it - const customLdapOptions = args.length > 0 ? args.shift() : {}; - - // Set up loginRequest object - const loginRequest = { - ldap: true, - username, - ldapPass: password, - ldapOptions: customLdapOptions, - }; - - Accounts.callLoginMethod({ - // Call login method with ldap = true - // This will hook into our login handler for ldap - methodArguments: [loginRequest], - userCallback: callback, - }); -}; diff --git a/app/ldap/server/index.js b/app/ldap/server/index.js deleted file mode 100644 index acdd21c00c48f..0000000000000 --- a/app/ldap/server/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import './loginHandler'; -import './settings'; -import './testConnection'; -import './syncUsers'; -import './sync'; diff --git a/app/ldap/server/ldap.js b/app/ldap/server/ldap.js deleted file mode 100644 index eafdd3161796b..0000000000000 --- a/app/ldap/server/ldap.js +++ /dev/null @@ -1,525 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import ldapjs from 'ldapjs'; -import Bunyan from 'bunyan'; - -import { callbacks } from '../../callbacks/server'; -import { settings } from '../../settings'; -import { Logger } from '../../logger'; - -const logger = new Logger('LDAP', { - sections: { - connection: 'Connection', - bind: 'Bind', - search: 'Search', - auth: 'Auth', - }, -}); - -export default class LDAP { - constructor() { - this.ldapjs = ldapjs; - - this.connected = false; - - this.options = { - host: settings.get('LDAP_Host'), - port: settings.get('LDAP_Port'), - Reconnect: settings.get('LDAP_Reconnect'), - Internal_Log_Level: settings.get('LDAP_Internal_Log_Level'), - timeout: settings.get('LDAP_Timeout'), - connect_timeout: settings.get('LDAP_Connect_Timeout'), - idle_timeout: settings.get('LDAP_Idle_Timeout'), - encryption: settings.get('LDAP_Encryption'), - ca_cert: settings.get('LDAP_CA_Cert'), - reject_unauthorized: settings.get('LDAP_Reject_Unauthorized') || false, - Authentication: settings.get('LDAP_Authentication'), - Authentication_UserDN: settings.get('LDAP_Authentication_UserDN'), - Authentication_Password: settings.get('LDAP_Authentication_Password'), - BaseDN: settings.get('LDAP_BaseDN'), - User_Search_Filter: settings.get('LDAP_User_Search_Filter'), - User_Search_Scope: settings.get('LDAP_User_Search_Scope'), - User_Search_Field: settings.get('LDAP_User_Search_Field'), - Search_Page_Size: settings.get('LDAP_Search_Page_Size'), - Search_Size_Limit: settings.get('LDAP_Search_Size_Limit'), - group_filter_enabled: settings.get('LDAP_Group_Filter_Enable'), - group_filter_object_class: settings.get('LDAP_Group_Filter_ObjectClass'), - group_filter_group_id_attribute: settings.get('LDAP_Group_Filter_Group_Id_Attribute'), - group_filter_group_member_attribute: settings.get('LDAP_Group_Filter_Group_Member_Attribute'), - group_filter_group_member_format: settings.get('LDAP_Group_Filter_Group_Member_Format'), - group_filter_group_name: settings.get('LDAP_Group_Filter_Group_Name'), - find_user_after_login: settings.get('LDAP_Find_User_After_Login'), - }; - } - - connectSync(...args) { - if (!this._connectSync) { - this._connectSync = Meteor.wrapAsync(this.connectAsync, this); - } - return this._connectSync(...args); - } - - searchAllSync(...args) { - if (!this._searchAllSync) { - this._searchAllSync = Meteor.wrapAsync(this.searchAllAsync, this); - } - return this._searchAllSync(...args); - } - - connectAsync(callback) { - logger.connection.info('Init setup'); - - let replied = false; - - const connectionOptions = { - url: `${ this.options.host }:${ this.options.port }`, - timeout: this.options.timeout, - connectTimeout: this.options.connect_timeout, - idleTimeout: this.options.idle_timeout, - reconnect: this.options.Reconnect, - }; - - if (this.options.Internal_Log_Level !== 'disabled') { - connectionOptions.log = new Bunyan({ - name: 'ldapjs', - component: 'client', - stream: process.stderr, - level: this.options.Internal_Log_Level, - }); - } - - const tlsOptions = { - rejectUnauthorized: this.options.reject_unauthorized, - }; - - if (this.options.ca_cert && this.options.ca_cert !== '') { - // Split CA cert into array of strings - const chainLines = settings.get('LDAP_CA_Cert').split('\n'); - let cert = []; - const ca = []; - chainLines.forEach((line) => { - cert.push(line); - if (line.match(/-END CERTIFICATE-/)) { - ca.push(cert.join('\n')); - cert = []; - } - }); - tlsOptions.ca = ca; - } - - if (this.options.encryption === 'ssl') { - connectionOptions.url = `ldaps://${ connectionOptions.url }`; - connectionOptions.tlsOptions = tlsOptions; - } else { - connectionOptions.url = `ldap://${ connectionOptions.url }`; - } - - logger.connection.info('Connecting', connectionOptions.url); - logger.connection.debug('connectionOptions', connectionOptions); - - this.client = ldapjs.createClient(connectionOptions); - - this.bindSync = Meteor.wrapAsync(this.client.bind, this.client); - - this.client.on('error', (error) => { - logger.connection.error('connection', error); - if (replied === false) { - replied = true; - callback(error, null); - } - }); - - this.client.on('idle', () => { - logger.search.info('Idle'); - this.disconnect(); - }); - - this.client.on('close', () => { - logger.search.info('Closed'); - }); - - if (this.options.encryption === 'tls') { - // Set host parameter for tls.connect which is used by ldapjs starttls. This shouldn't be needed in newer nodejs versions (e.g v5.6.0). - // https://github.com/RocketChat/Rocket.Chat/issues/2035 - // https://github.com/mcavage/node-ldapjs/issues/349 - tlsOptions.host = this.options.host; - - logger.connection.info('Starting TLS'); - logger.connection.debug('tlsOptions', tlsOptions); - - this.client.starttls(tlsOptions, null, (error, response) => { - if (error) { - logger.connection.error('TLS connection', error); - if (replied === false) { - replied = true; - callback(error, null); - } - return; - } - - logger.connection.info('TLS connected'); - this.connected = true; - if (replied === false) { - replied = true; - callback(null, response); - } - }); - } else { - this.client.on('connect', (response) => { - logger.connection.info('LDAP connected'); - this.connected = true; - if (replied === false) { - replied = true; - callback(null, response); - } - }); - } - - setTimeout(() => { - if (replied === false) { - logger.connection.error('connection time out', connectionOptions.connectTimeout); - replied = true; - callback(new Error('Timeout')); - } - }, connectionOptions.connectTimeout); - } - - getUserFilter(username) { - const filter = []; - - if (this.options.User_Search_Filter !== '') { - if (this.options.User_Search_Filter[0] === '(') { - filter.push(`${ this.options.User_Search_Filter }`); - } else { - filter.push(`(${ this.options.User_Search_Filter })`); - } - } - - const usernameFilter = this.options.User_Search_Field.split(',').map((item) => `(${ item }=${ username })`); - - if (usernameFilter.length === 0) { - logger.error('LDAP_LDAP_User_Search_Field not defined'); - } else if (usernameFilter.length === 1) { - filter.push(`${ usernameFilter[0] }`); - } else { - filter.push(`(|${ usernameFilter.join('') })`); - } - - return `(&${ filter.join('') })`; - } - - bindIfNecessary() { - if (this.domainBinded === true) { - return; - } - - if (this.options.Authentication !== true) { - return; - } - - logger.bind.info('Binding UserDN', this.options.Authentication_UserDN); - this.bindSync(this.options.Authentication_UserDN, this.options.Authentication_Password); - this.domainBinded = true; - } - - searchUsersSync(username, page) { - this.bindIfNecessary(); - - const searchOptions = { - filter: this.getUserFilter(username), - scope: this.options.User_Search_Scope || 'sub', - sizeLimit: this.options.Search_Size_Limit, - }; - - if (this.options.Search_Page_Size > 0) { - searchOptions.paged = { - pageSize: this.options.Search_Page_Size, - pagePause: !!page, - }; - } - - logger.search.info('Searching user', username); - logger.search.debug('searchOptions', searchOptions); - logger.search.debug('BaseDN', this.options.BaseDN); - - if (page) { - return this.searchAllPaged(this.options.BaseDN, searchOptions, page); - } - - return this.searchAllSync(this.options.BaseDN, searchOptions); - } - - getUserByIdSync(id, attribute) { - this.bindIfNecessary(); - - const Unique_Identifier_Field = settings.get('LDAP_Unique_Identifier_Field').split(','); - - let filter; - - if (attribute) { - filter = new this.ldapjs.filters.EqualityFilter({ - attribute, - value: Buffer.from(id, 'hex'), - }); - } else { - const filters = []; - Unique_Identifier_Field.forEach((item) => { - filters.push(new this.ldapjs.filters.EqualityFilter({ - attribute: item, - value: Buffer.from(id, 'hex'), - })); - }); - - filter = new this.ldapjs.filters.OrFilter({ filters }); - } - - const searchOptions = { - filter, - scope: 'sub', - attributes: ['*', '+'], - }; - - logger.search.info('Searching by id', id); - logger.search.debug('search filter', searchOptions.filter.toString()); - logger.search.debug('BaseDN', this.options.BaseDN); - - const result = this.searchAllSync(this.options.BaseDN, searchOptions); - - if (!Array.isArray(result) || result.length === 0) { - return; - } - - if (result.length > 1) { - logger.search.error('Search by id', id, 'returned', result.length, 'records'); - } - - return result[0]; - } - - getUserByUsernameSync(username) { - this.bindIfNecessary(); - - const searchOptions = { - filter: this.getUserFilter(username), - scope: this.options.User_Search_Scope || 'sub', - }; - - logger.search.info('Searching user', username); - logger.search.debug('searchOptions', searchOptions); - logger.search.debug('BaseDN', this.options.BaseDN); - - const result = this.searchAllSync(this.options.BaseDN, searchOptions); - - if (!Array.isArray(result) || result.length === 0) { - return; - } - - if (result.length > 1) { - logger.search.error('Search by username', username, 'returned', result.length, 'records'); - } - - return result[0]; - } - - isUserInGroup(username, userdn) { - if (!this.options.group_filter_enabled) { - return true; - } - - const filter = ['(&']; - - if (this.options.group_filter_object_class !== '') { - filter.push(`(objectclass=${ this.options.group_filter_object_class })`); - } - - if (this.options.group_filter_group_member_attribute !== '') { - filter.push(`(${ this.options.group_filter_group_member_attribute }=${ this.options.group_filter_group_member_format })`); - } - - if (this.options.group_filter_group_id_attribute !== '') { - filter.push(`(${ this.options.group_filter_group_id_attribute }=${ this.options.group_filter_group_name })`); - } - filter.push(')'); - - const searchOptions = { - filter: filter.join('').replace(/#{username}/g, username).replace(/#{userdn}/g, userdn), - scope: 'sub', - }; - - logger.search.debug('Group filter LDAP:', searchOptions.filter); - - const result = this.searchAllSync(this.options.BaseDN, searchOptions); - - if (!Array.isArray(result) || result.length === 0) { - return false; - } - return true; - } - - extractLdapEntryData(entry) { - const values = { - _raw: entry.raw, - }; - - Object.keys(values._raw).forEach((key) => { - const value = values._raw[key]; - - if (!['thumbnailPhoto', 'jpegPhoto'].includes(key)) { - if (value instanceof Buffer) { - values[key] = value.toString(); - } else { - values[key] = value; - } - } - - if (key === 'ou' && Array.isArray(value)) { - value.forEach((item, index) => { - if (item instanceof Buffer) { - value[index] = item.toString(); - } - }); - } - }); - - return values; - } - - searchAllPaged(BaseDN, options, page) { - this.bindIfNecessary(); - - ({ BaseDN, options } = callbacks.run('ldap.beforeSearchAll', { BaseDN, options })); - - const processPage = ({ entries, title, end, next }) => { - logger.search.info(title); - // Force LDAP idle to wait the record processing - this.client._updateIdle(true); - page(null, entries, { end, - next: () => { - // Reset idle timer - this.client._updateIdle(); - next && next(); - } }); - }; - - this.client.search(BaseDN, options, (error, res) => { - if (error) { - logger.search.error(error); - page(error); - return; - } - - res.on('error', (error) => { - logger.search.error(error); - page(error); - }); - - let entries = []; - - const internalPageSize = options.paged && options.paged.pageSize > 0 ? options.paged.pageSize * 2 : 500; - - res.on('searchEntry', (entry) => { - entries.push(this.extractLdapEntryData(entry)); - - if (entries.length >= internalPageSize) { - processPage({ - entries, - title: 'Internal Page', - end: false, - }); - entries = []; - } - }); - - res.on('page', (result, next) => { - if (!next) { - this.client._updateIdle(true); - processPage({ - entries, - title: 'Final Page', - end: true, - }); - entries = []; - } else if (entries.length) { - processPage({ - entries, - title: 'Page', - end: false, - next, - }); - entries = []; - } - }); - - res.on('end', () => { - if (entries.length) { - processPage({ - entries, - title: 'Final Page', - end: true, - }); - entries = []; - } - }); - }); - } - - searchAllAsync(BaseDN, options, callback) { - this.bindIfNecessary(); - - ({ BaseDN, options } = callbacks.run('ldap.beforeSearchAll', { BaseDN, options })); - - this.client.search(BaseDN, options, (error, res) => { - if (error) { - logger.search.error(error); - callback(error); - return; - } - - res.on('error', (error) => { - logger.search.error(error); - callback(error); - }); - - const entries = []; - - res.on('searchEntry', (entry) => { - entries.push(this.extractLdapEntryData(entry)); - }); - - res.on('end', () => { - logger.search.info('Search result count', entries.length); - callback(null, entries); - }); - }); - } - - authSync(dn, password) { - logger.auth.info('Authenticating', dn); - - try { - this.bindSync(dn, password); - if (this.options.find_user_after_login) { - const searchOptions = { - scope: this.options.User_Search_Scope || 'sub', - }; - const result = this.searchAllSync(dn, searchOptions); - if (result.length === 0) { - logger.auth.info('Bind successful but user was not found via search', dn, searchOptions); - return false; - } - } - logger.auth.info('Authenticated', dn); - return true; - } catch (error) { - logger.auth.info('Not authenticated', dn); - logger.auth.debug('error', error); - return false; - } - } - - disconnect() { - this.connected = false; - this.domainBinded = false; - logger.connection.info('Disconecting'); - this.client.unbind(); - } -} diff --git a/app/ldap/server/loginHandler.js b/app/ldap/server/loginHandler.js deleted file mode 100644 index 79425c9dfe741..0000000000000 --- a/app/ldap/server/loginHandler.js +++ /dev/null @@ -1,184 +0,0 @@ -import { SHA256 } from 'meteor/sha'; -import { Meteor } from 'meteor/meteor'; -import { Accounts } from 'meteor/accounts-base'; -import ldapEscape from 'ldap-escape'; - -import { slug, getLdapUsername, getLdapUserUniqueID, syncUserData, addLdapUser } from './sync'; -import LDAP from './ldap'; -import { settings } from '../../settings'; -import { callbacks } from '../../callbacks'; -import { Logger } from '../../logger'; - - -const logger = new Logger('LDAPHandler', {}); - -function fallbackDefaultAccountSystem(bind, username, password) { - if (typeof username === 'string') { - if (username.indexOf('@') === -1) { - username = { username }; - } else { - username = { email: username }; - } - } - - logger.info('Fallback to default account system', username); - - const loginRequest = { - user: username, - password: { - digest: SHA256(password), - algorithm: 'sha-256', - }, - }; - - return Accounts._runLoginHandlers(bind, loginRequest); -} - -Accounts.registerLoginHandler('ldap', function(loginRequest) { - if (!loginRequest.ldap || !loginRequest.ldapOptions) { - return undefined; - } - - logger.info('Init LDAP login', loginRequest.username); - - if (settings.get('LDAP_Enable') !== true) { - return fallbackDefaultAccountSystem(this, loginRequest.username, loginRequest.ldapPass); - } - - const self = this; - const ldap = new LDAP(); - let ldapUser; - - const escapedUsername = ldapEscape.filter`${ loginRequest.username }`; - - try { - ldap.connectSync(); - const users = ldap.searchUsersSync(escapedUsername); - - if (users.length !== 1) { - logger.info('Search returned', users.length, 'record(s) for', escapedUsername); - throw new Error('User not Found'); - } - - if (ldap.authSync(users[0].dn, loginRequest.ldapPass) === true) { - if (ldap.isUserInGroup(escapedUsername, users[0].dn)) { - ldapUser = users[0]; - } else { - throw new Error('User not in a valid group'); - } - } else { - logger.info('Wrong password for', escapedUsername); - } - } catch (error) { - logger.error(error); - } - - if (ldapUser === undefined) { - return fallbackDefaultAccountSystem(self, loginRequest.username, loginRequest.ldapPass); - } - - // Look to see if user already exists - let userQuery; - - const Unique_Identifier_Field = getLdapUserUniqueID(ldapUser); - let user; - - if (Unique_Identifier_Field) { - userQuery = { - 'services.ldap.id': Unique_Identifier_Field.value, - }; - - logger.info('Querying user'); - logger.debug('userQuery', userQuery); - - user = Meteor.users.findOne(userQuery); - } - - let username; - - if (settings.get('LDAP_Username_Field') !== '') { - username = slug(getLdapUsername(ldapUser)); - } else { - username = slug(loginRequest.username); - } - - if (!user) { - userQuery = { - username, - }; - - logger.debug('userQuery', userQuery); - - user = Meteor.users.findOne(userQuery); - } - - // Login user if they exist - if (user) { - if (user.ldap !== true && settings.get('LDAP_Merge_Existing_Users') !== true) { - logger.info('User exists without "ldap: true"'); - throw new Meteor.Error('LDAP-login-error', `LDAP Authentication succeeded, but there's already an existing user with provided username [${ username }] in Mongo.`); - } - - logger.info('Logging user'); - - syncUserData(user, ldapUser, ldap); - - if (settings.get('LDAP_Login_Fallback') === true && typeof loginRequest.ldapPass === 'string' && loginRequest.ldapPass.trim() !== '') { - Accounts.setPassword(user._id, loginRequest.ldapPass, { logout: false }); - } - logger.info('running afterLDAPLogin'); - callbacks.run('afterLDAPLogin', { user, ldapUser, ldap }); - return { - userId: user._id, - }; - } - - logger.info('User does not exist, creating', username); - - if (settings.get('LDAP_Username_Field') === '') { - username = undefined; - } - - if (settings.get('LDAP_Login_Fallback') !== true) { - loginRequest.ldapPass = undefined; - } - - // Create new user - const result = addLdapUser(ldapUser, username, loginRequest.ldapPass, ldap); - - if (result instanceof Error) { - throw result; - } - callbacks.run('afterLDAPLogin', { user: result, ldapUser, ldap }); - - return result; -}); - -let LDAP_Enable; -settings.get('LDAP_Enable', (key, value) => { - if (LDAP_Enable === value) { - return; - } - LDAP_Enable = value; - - if (!value) { - return callbacks.remove('beforeValidateLogin', 'validateLdapLoginFallback'); - } - - callbacks.add('beforeValidateLogin', (login) => { - if (!login.allowed) { - return login; - } - - // The fallback setting should only block password logins, so users that have other login services can continue using them - if (login.type !== 'password') { - return login; - } - - if (login.user.services && login.user.services.ldap && login.user.services.ldap.id) { - login.allowed = !!settings.get('LDAP_Login_Fallback'); - } - - return login; - }, callbacks.priority.MEDIUM, 'validateLdapLoginFallback'); -}); diff --git a/app/ldap/server/settings.js b/app/ldap/server/settings.js deleted file mode 100644 index 10e8d90bfe34f..0000000000000 --- a/app/ldap/server/settings.js +++ /dev/null @@ -1,134 +0,0 @@ -import { settings } from '../../settings'; - -settings.addGroup('LDAP', function() { - const enableQuery = { _id: 'LDAP_Enable', value: true }; - const enableAuthentication = [ - enableQuery, - { _id: 'LDAP_Authentication', value: true }, - ]; - const enableTLSQuery = [ - enableQuery, - { _id: 'LDAP_Encryption', value: { $in: ['tls', 'ssl'] } }, - ]; - const syncDataQuery = [ - enableQuery, - { _id: 'LDAP_Sync_User_Data', value: true }, - ]; - const syncGroupsQuery = [ - enableQuery, - { _id: 'LDAP_Sync_User_Data_Groups', value: true }, - ]; - const syncGroupsChannelsQuery = [ - enableQuery, - { _id: 'LDAP_Sync_User_Data_Groups', value: true }, - { _id: 'LDAP_Sync_User_Data_Groups_AutoChannels', value: true }, - ]; - const groupFilterQuery = [ - enableQuery, - { _id: 'LDAP_Group_Filter_Enable', value: true }, - ]; - const backgroundSyncQuery = [ - enableQuery, - { _id: 'LDAP_Background_Sync', value: true }, - ]; - - this.add('LDAP_Enable', false, { type: 'boolean', public: true }); - this.add('LDAP_Login_Fallback', false, { type: 'boolean', enableQuery: null }); - this.add('LDAP_Find_User_After_Login', true, { type: 'boolean', enableQuery }); - this.add('LDAP_Host', '', { type: 'string', enableQuery }); - this.add('LDAP_Port', '389', { type: 'int', enableQuery }); - this.add('LDAP_Reconnect', false, { type: 'boolean', enableQuery }); - this.add('LDAP_Encryption', 'plain', { type: 'select', values: [{ key: 'plain', i18nLabel: 'No_Encryption' }, { key: 'tls', i18nLabel: 'StartTLS' }, { key: 'ssl', i18nLabel: 'SSL/LDAPS' }], enableQuery }); - this.add('LDAP_CA_Cert', '', { type: 'string', multiline: true, enableQuery: enableTLSQuery, secret: true }); - this.add('LDAP_Reject_Unauthorized', true, { type: 'boolean', enableQuery: enableTLSQuery }); - this.add('LDAP_BaseDN', '', { type: 'string', enableQuery }); - this.add('LDAP_Internal_Log_Level', 'disabled', { - type: 'select', - values: [ - { key: 'disabled', i18nLabel: 'Disabled' }, - { key: 'error', i18nLabel: 'Error' }, - { key: 'warn', i18nLabel: 'Warn' }, - { key: 'info', i18nLabel: 'Info' }, - { key: 'debug', i18nLabel: 'Debug' }, - { key: 'trace', i18nLabel: 'Trace' }, - ], - enableQuery, - }); - this.add('LDAP_Test_Connection', 'ldap_test_connection', { type: 'action', actionText: 'Test_Connection' }); - - this.section('Authentication', function() { - this.add('LDAP_Authentication', false, { type: 'boolean', enableQuery }); - this.add('LDAP_Authentication_UserDN', '', { type: 'string', enableQuery: enableAuthentication, secret: true }); - this.add('LDAP_Authentication_Password', '', { type: 'password', enableQuery: enableAuthentication, secret: true }); - }); - - this.section('Timeouts', function() { - this.add('LDAP_Timeout', 60000, { type: 'int', enableQuery }); - this.add('LDAP_Connect_Timeout', 1000, { type: 'int', enableQuery }); - this.add('LDAP_Idle_Timeout', 1000, { type: 'int', enableQuery }); - }); - - this.section('User Search', function() { - this.add('LDAP_User_Search_Filter', '(objectclass=*)', { type: 'string', enableQuery }); - this.add('LDAP_User_Search_Scope', 'sub', { type: 'string', enableQuery }); - this.add('LDAP_User_Search_Field', 'sAMAccountName', { type: 'string', enableQuery }); - this.add('LDAP_Search_Page_Size', 250, { type: 'int', enableQuery }); - this.add('LDAP_Search_Size_Limit', 1000, { type: 'int', enableQuery }); - }); - - this.section('User Search (Group Validation)', function() { - this.add('LDAP_Group_Filter_Enable', false, { type: 'boolean', enableQuery }); - this.add('LDAP_Group_Filter_ObjectClass', 'groupOfUniqueNames', { type: 'string', enableQuery: groupFilterQuery }); - this.add('LDAP_Group_Filter_Group_Id_Attribute', 'cn', { type: 'string', enableQuery: groupFilterQuery }); - this.add('LDAP_Group_Filter_Group_Member_Attribute', 'uniqueMember', { type: 'string', enableQuery: groupFilterQuery }); - this.add('LDAP_Group_Filter_Group_Member_Format', 'uniqueMember', { type: 'string', enableQuery: groupFilterQuery }); - this.add('LDAP_Group_Filter_Group_Name', 'ROCKET_CHAT', { type: 'string', enableQuery: groupFilterQuery }); - }); - - this.section('Sync / Import', function() { - this.add('LDAP_Username_Field', 'sAMAccountName', { - type: 'string', - enableQuery, - // public so that it's visible to AccountProfilePage: - public: true, - }); - this.add('LDAP_Unique_Identifier_Field', 'objectGUID,ibm-entryUUID,GUID,dominoUNID,nsuniqueId,uidNumber', { type: 'string', enableQuery }); - this.add('LDAP_Default_Domain', '', { type: 'string', enableQuery }); - this.add('LDAP_Merge_Existing_Users', false, { type: 'boolean', enableQuery }); - - this.add('LDAP_Sync_User_Data', false, { type: 'boolean', enableQuery }); - this.add('LDAP_Sync_User_Data_FieldMap', '{"cn":"name", "mail":"email"}', { type: 'string', enableQuery: syncDataQuery }); - - this.add('LDAP_Sync_User_Data_Groups', false, { type: 'boolean', enableQuery }); - this.add('LDAP_Sync_User_Data_Groups_AutoRemove', false, { type: 'boolean', enableQuery: syncGroupsQuery }); - this.add('LDAP_Sync_User_Data_Groups_Filter', '(&(cn=#{groupName})(memberUid=#{username}))', { type: 'string', enableQuery: syncGroupsQuery }); - this.add('LDAP_Sync_User_Data_Groups_BaseDN', '', { type: 'string', enableQuery: syncGroupsQuery }); - this.add('LDAP_Sync_User_Data_GroupsMap', '{\n\t"rocket-admin": "admin",\n\t"tech-support": "support"\n}', { - type: 'code', - multiline: true, - public: false, - code: 'application/json', - enableQuery: syncGroupsQuery, - }); - this.add('LDAP_Sync_User_Data_Groups_AutoChannels', false, { type: 'boolean', enableQuery: syncGroupsQuery }); - this.add('LDAP_Sync_User_Data_Groups_AutoChannels_Admin', 'rocket.cat', { type: 'string', enableQuery: syncGroupsChannelsQuery }); - this.add('LDAP_Sync_User_Data_Groups_AutoChannelsMap', '{\n\t"employee": "general",\n\t"techsupport": [\n\t\t"helpdesk",\n\t\t"support"\n\t]\n}', { - type: 'code', - multiline: true, - public: false, - code: 'application/json', - enableQuery: syncGroupsChannelsQuery, - }); - this.add('LDAP_Sync_User_Data_Groups_Enforce_AutoChannels', false, { type: 'boolean', enableQuery: syncGroupsChannelsQuery }); - - this.add('LDAP_Sync_User_Avatar', true, { type: 'boolean', enableQuery }); - this.add('LDAP_Avatar_Field', '', { type: 'string', enableQuery }); - - this.add('LDAP_Background_Sync', false, { type: 'boolean', enableQuery }); - this.add('LDAP_Background_Sync_Interval', 'Every 24 hours', { type: 'string', enableQuery: backgroundSyncQuery }); - this.add('LDAP_Background_Sync_Import_New_Users', true, { type: 'boolean', enableQuery: backgroundSyncQuery }); - this.add('LDAP_Background_Sync_Keep_Existant_Users_Updated', true, { type: 'boolean', enableQuery: backgroundSyncQuery }); - - this.add('LDAP_Sync_Now', 'ldap_sync_now', { type: 'action', actionText: 'Execute_Synchronization_Now' }); - }); -}); diff --git a/app/ldap/server/sync.js b/app/ldap/server/sync.js deleted file mode 100644 index f4f0c65f993b5..0000000000000 --- a/app/ldap/server/sync.js +++ /dev/null @@ -1,629 +0,0 @@ -import limax from 'limax'; -import { Meteor } from 'meteor/meteor'; -import { Accounts } from 'meteor/accounts-base'; -import { SyncedCron } from 'meteor/littledata:synced-cron'; -import _ from 'underscore'; - -import LDAP from './ldap'; -import { callbacks } from '../../callbacks/server'; -import { RocketChatFile } from '../../file'; -import { settings } from '../../settings'; -import { Users, Roles, Rooms, Subscriptions } from '../../models'; -import { Logger } from '../../logger'; -import { _setRealName } from '../../lib'; -import { templateVarHandler } from '../../utils'; -import { FileUpload } from '../../file-upload'; -import { addUserToRoom, removeUserFromRoom, createRoom, saveUserIdentity } from '../../lib/server/functions'; -import { api } from '../../../server/sdk/api'; - -export const logger = new Logger('LDAPSync', {}); - -export function isUserInLDAPGroup(ldap, ldapUser, user, ldapGroup) { - const syncUserRolesFilter = settings.get('LDAP_Sync_User_Data_Groups_Filter').trim(); - const syncUserRolesBaseDN = settings.get('LDAP_Sync_User_Data_Groups_BaseDN').trim(); - - if (!syncUserRolesFilter || !syncUserRolesBaseDN) { - logger.error('Please setup LDAP Group Filter and LDAP Group BaseDN in LDAP Settings.'); - return false; - } - const searchOptions = { - filter: syncUserRolesFilter.replace(/#{username}/g, user.username).replace(/#{groupName}/g, ldapGroup).replace(/#{userdn}/g, ldapUser.dn), - scope: 'sub', - }; - - const result = ldap.searchAllSync(syncUserRolesBaseDN, searchOptions); - if (!Array.isArray(result) || result.length === 0) { - logger.debug(`${ user.username } is not in ${ ldapGroup } group!!!`); - } else { - logger.debug(`${ user.username } is in ${ ldapGroup } group.`); - return true; - } - - return false; -} - -export function slug(text) { - if (settings.get('UTF8_Names_Slugify') !== true) { - return text; - } - text = limax(text, { replacement: '.' }); - return text.replace(/[^0-9a-z-_.]/g, ''); -} - - -export function getPropertyValue(obj, key) { - try { - return _.reduce(key.split('.'), (acc, el) => acc[el], obj); - } catch (err) { - return undefined; - } -} - - -export function getLdapUsername(ldapUser) { - const usernameField = settings.get('LDAP_Username_Field'); - - if (usernameField.indexOf('#{') > -1) { - return usernameField.replace(/#{(.+?)}/g, function(match, field) { - return ldapUser[field]; - }); - } - - return ldapUser[usernameField]; -} - - -export function getLdapUserUniqueID(ldapUser) { - let Unique_Identifier_Field = settings.get('LDAP_Unique_Identifier_Field'); - - if (Unique_Identifier_Field !== '') { - Unique_Identifier_Field = Unique_Identifier_Field.replace(/\s/g, '').split(','); - } else { - Unique_Identifier_Field = []; - } - - let User_Search_Field = settings.get('LDAP_User_Search_Field'); - - if (User_Search_Field !== '') { - User_Search_Field = User_Search_Field.replace(/\s/g, '').split(','); - } else { - User_Search_Field = []; - } - - Unique_Identifier_Field = Unique_Identifier_Field.concat(User_Search_Field); - - if (Unique_Identifier_Field.length > 0) { - Unique_Identifier_Field = Unique_Identifier_Field.find((field) => !_.isEmpty(ldapUser._raw[field])); - if (Unique_Identifier_Field) { - Unique_Identifier_Field = { - attribute: Unique_Identifier_Field, - value: ldapUser._raw[Unique_Identifier_Field].toString('hex'), - }; - } - return Unique_Identifier_Field; - } -} - -export function getDataToSyncUserData(ldapUser, user) { - const syncUserData = settings.get('LDAP_Sync_User_Data'); - const syncUserDataFieldMap = settings.get('LDAP_Sync_User_Data_FieldMap').trim(); - - const userData = {}; - - if (syncUserData && syncUserDataFieldMap) { - const whitelistedUserFields = ['email', 'name', 'customFields']; - const fieldMap = JSON.parse(syncUserDataFieldMap); - const emailList = []; - _.map(fieldMap, function(userField, ldapField) { - switch (userField) { - case 'email': - if (!ldapUser.hasOwnProperty(ldapField)) { - logger.debug(`user does not have attribute: ${ ldapField }`); - return; - } - - const verified = settings.get('Accounts_Verify_Email_For_External_Accounts'); - - if (_.isObject(ldapUser[ldapField])) { - _.map(ldapUser[ldapField], function(item) { - emailList.push({ address: item, verified }); - }); - } else { - emailList.push({ address: ldapUser[ldapField], verified }); - } - break; - - default: - const [outerKey, innerKeys] = userField.split(/\.(.+)/); - - if (!_.find(whitelistedUserFields, (el) => el === outerKey)) { - logger.debug(`user attribute not whitelisted: ${ userField }`); - return; - } - - if (outerKey === 'customFields') { - let customFieldsMeta; - - try { - customFieldsMeta = JSON.parse(settings.get('Accounts_CustomFields')); - } catch (e) { - logger.debug('Invalid JSON for Custom Fields'); - return; - } - - if (!getPropertyValue(customFieldsMeta, innerKeys)) { - logger.debug(`user attribute does not exist: ${ userField }`); - return; - } - } - - const tmpUserField = getPropertyValue(user, userField); - const tmpLdapField = templateVarHandler(ldapField, ldapUser); - - if (tmpLdapField && tmpUserField !== tmpLdapField) { - // creates the object structure instead of just assigning 'tmpLdapField' to - // 'userData[userField]' in order to avoid the "cannot use the part (...) - // to traverse the element" (MongoDB) error that can happen. Do not handle - // arrays. - // TODO: Find a better solution. - const dKeys = userField.split('.'); - const lastKey = _.last(dKeys); - _.reduce(dKeys, (obj, currKey) => { - if (currKey === lastKey) { - obj[currKey] = tmpLdapField; - } else { - obj[currKey] = obj[currKey] || {}; - } - return obj[currKey]; - }, userData); - logger.debug(`user.${ userField } changed to: ${ tmpLdapField }`); - } - } - }); - - if (emailList.length > 0) { - if (JSON.stringify(user.emails) !== JSON.stringify(emailList)) { - userData.emails = emailList; - } - } - } - - const uniqueId = getLdapUserUniqueID(ldapUser); - - if (uniqueId && (!user.services || !user.services.ldap || user.services.ldap.id !== uniqueId.value || user.services.ldap.idAttribute !== uniqueId.attribute)) { - userData['services.ldap.id'] = uniqueId.value; - userData['services.ldap.idAttribute'] = uniqueId.attribute; - } - - if (user.ldap !== true) { - userData.ldap = true; - } - - if (_.size(userData)) { - return userData; - } -} -export function mapLdapGroupsToUserRoles(ldap, ldapUser, user) { - const syncUserRoles = settings.get('LDAP_Sync_User_Data_Groups'); - const syncUserRolesAutoRemove = settings.get('LDAP_Sync_User_Data_Groups_AutoRemove'); - const syncUserRolesFieldMap = settings.get('LDAP_Sync_User_Data_GroupsMap').trim(); - - if (!syncUserRoles || !syncUserRolesFieldMap) { - logger.debug('not syncing user roles'); - return []; - } - - const roles = Roles.find({}, { - fields: { - _updatedAt: 0, - }, - }).fetch(); - - if (!roles) { - return []; - } - - let fieldMap; - - try { - fieldMap = JSON.parse(syncUserRolesFieldMap); - } catch (err) { - logger.error(`Unexpected error : ${ err.message }`); - return []; - } - if (!fieldMap) { - return []; - } - - const userRoles = []; - - for (const ldapField in fieldMap) { - if (!fieldMap.hasOwnProperty(ldapField)) { - continue; - } - - const userField = fieldMap[ldapField]; - - const [roleName] = userField.split(/\.(.+)/); - if (!_.find(roles, (el) => el._id === roleName)) { - logger.debug(`User Role doesn't exist: ${ roleName }`); - continue; - } - - logger.debug(`User role exists for mapping ${ ldapField } -> ${ roleName }`); - - if (isUserInLDAPGroup(ldap, ldapUser, user, ldapField)) { - userRoles.push(roleName); - continue; - } - - if (!syncUserRolesAutoRemove) { - continue; - } - - const del = Roles.removeUserRoles(user._id, roleName); - if (settings.get('UI_DisplayRoles') && del) { - api.broadcast('user.roleUpdate', { - type: 'removed', - _id: roleName, - u: { - _id: user._id, - username: user.username, - }, - }); - } - } - - return userRoles; -} -export function createRoomForSync(channel) { - logger.info(`Channel '${ channel }' doesn't exist, creating it.`); - - const room = createRoom('c', channel, settings.get('LDAP_Sync_User_Data_Groups_AutoChannels_Admin'), [], false, { customFields: { ldap: true } }); - if (!room || !room.rid) { - logger.error(`Unable to auto-create channel '${ channel }' during ldap sync.`); - return; - } - room._id = room.rid; - return room; -} - -export function mapLDAPGroupsToChannels(ldap, ldapUser, user) { - const syncUserRoles = settings.get('LDAP_Sync_User_Data_Groups'); - const syncUserRolesAutoChannels = settings.get('LDAP_Sync_User_Data_Groups_AutoChannels'); - const syncUserRolesEnforceAutoChannels = settings.get('LDAP_Sync_User_Data_Groups_Enforce_AutoChannels'); - const syncUserRolesChannelFieldMap = settings.get('LDAP_Sync_User_Data_Groups_AutoChannelsMap').trim(); - - const userChannels = []; - if (!syncUserRoles || !syncUserRolesAutoChannels || !syncUserRolesChannelFieldMap) { - logger.debug('not syncing groups to channels'); - return []; - } - - let fieldMap; - try { - fieldMap = JSON.parse(syncUserRolesChannelFieldMap); - } catch (err) { - logger.error(`Unexpected error : ${ err.message }`); - return []; - } - - if (!fieldMap) { - return []; - } - - _.map(fieldMap, function(channels, ldapField) { - if (!Array.isArray(channels)) { - channels = [channels]; - } - - for (const channel of channels) { - let room = Rooms.findOneByNonValidatedName(channel); - - if (!room) { - room = createRoomForSync(channel); - } - if (isUserInLDAPGroup(ldap, ldapUser, user, ldapField)) { - if (room.teamMain) { - logger.error(`Can't add user to channel ${ channel } because it is a team.`); - } else { - userChannels.push(room._id); - } - } else if (syncUserRolesEnforceAutoChannels && !room.teamMain) { - const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, user._id); - if (subscription) { - removeUserFromRoom(room._id, user); - } - } - } - }); - - return userChannels; -} - -function syncUserAvatar(user, ldapUser) { - if (!user?._id || settings.get('LDAP_Sync_User_Avatar') !== true) { - return; - } - - const avatarField = (settings.get('LDAP_Avatar_Field') || 'thumbnailPhoto').trim(); - const avatar = ldapUser._raw[avatarField] || ldapUser._raw.thumbnailPhoto || ldapUser._raw.jpegPhoto; - if (!avatar) { - return; - } - - logger.info('Syncing user avatar'); - - Meteor.defer(() => { - const rs = RocketChatFile.bufferToStream(avatar); - const fileStore = FileUpload.getStore('Avatars'); - fileStore.deleteByName(user.username); - - const file = { - userId: user._id, - type: 'image/jpeg', - size: avatar.length, - }; - - Meteor.runAsUser(user._id, () => { - fileStore.insert(file, rs, (err, result) => { - Meteor.setTimeout(function() { - Users.setAvatarData(user._id, 'ldap', result.etag); - api.broadcast('user.avatarUpdate', { username: user.username, avatarETag: result.etag }); - }, 500); - }); - }); - }); -} - -export function syncUserData(user, ldapUser, ldap) { - logger.info('Syncing user data'); - logger.debug('user', { email: user.email, _id: user._id }); - logger.debug('ldapUser', ldapUser.object); - - const userData = getDataToSyncUserData(ldapUser, user); - - // Returns a list of Rocket.Chat Groups a user should belong - // to if their LDAP group matches the LDAP_Sync_User_Data_GroupsMap - const userRoles = mapLdapGroupsToUserRoles(ldap, ldapUser, user); - - // Returns a list of Rocket.Chat Channels a user should belong - // to if their LDAP group matches the LDAP_Sync_User_Data_Groups_AutoChannelsMap - const userChannels = mapLDAPGroupsToChannels(ldap, ldapUser, user); - - if (user && user._id && userData) { - logger.debug('setting', JSON.stringify(userData, null, 2)); - if (userData.name) { - _setRealName(user._id, userData.name); - delete userData.name; - } - userData.customFields = { - ...user.customFields, ...userData.customFields, - }; - Meteor.users.update(user._id, { $set: userData }); - user = Meteor.users.findOne({ _id: user._id }); - } - - if (settings.get('LDAP_Username_Field') !== '') { - const username = slug(getLdapUsername(ldapUser)); - if (user && user._id && username !== user.username) { - logger.info('Syncing user username', user.username, '->', username); - saveUserIdentity({ _id: user._id, username }); - } - } - - if (settings.get('LDAP_Sync_User_Data_Groups') === true) { - for (const roleName of userRoles) { - const add = Roles.addUserRoles(user._id, roleName); - if (settings.get('UI_DisplayRoles') && add) { - api.broadcast('user.roleUpdate', { - type: 'added', - _id: roleName, - u: { - _id: user._id, - username: user.username, - }, - }); - } - logger.info('Synced user group', roleName, 'from LDAP for', user.username); - } - } - - if (settings.get('LDAP_Sync_User_Data_Groups_AutoChannels') === true) { - for (const userChannel of userChannels) { - addUserToRoom(userChannel, user); - logger.info('Synced user channel', userChannel, 'from LDAP for', user.username); - } - } - - syncUserAvatar(user, ldapUser); -} - -export function addLdapUser(ldapUser, username, password, ldap) { - const uniqueId = getLdapUserUniqueID(ldapUser); - - const userObject = {}; - - if (username) { - userObject.username = username; - } - - const userData = getDataToSyncUserData(ldapUser, {}); - - if (userData && userData.emails && userData.emails[0] && userData.emails[0].address) { - if (Array.isArray(userData.emails[0].address)) { - userObject.email = userData.emails[0].address[0]; - } else { - userObject.email = userData.emails[0].address; - } - } else if (ldapUser.mail && ldapUser.mail.indexOf('@') > -1) { - userObject.email = ldapUser.mail; - } else if (settings.get('LDAP_Default_Domain') !== '') { - userObject.email = `${ username || uniqueId.value }@${ settings.get('LDAP_Default_Domain') }`; - } else { - const error = new Meteor.Error('LDAP-login-error', 'LDAP Authentication succeeded, there is no email to create an account. Have you tried setting your Default Domain in LDAP Settings?'); - logger.error(error); - throw error; - } - - logger.debug('New user data', userObject); - - if (password) { - userObject.password = password; - } - - try { - userObject._id = Accounts.createUser(userObject); - } catch (error) { - logger.error('Error creating user', error); - return error; - } - - syncUserData(userObject, ldapUser, ldap); - - return { - userId: userObject._id, - }; -} - -export function importNewUsers(ldap) { - if (settings.get('LDAP_Enable') !== true) { - logger.error('Can\'t run LDAP Import, LDAP is disabled'); - return; - } - - if (!ldap) { - ldap = new LDAP(); - } - - if (!ldap.connected) { - ldap.connectSync(); - } - - let count = 0; - ldap.searchUsersSync('*', Meteor.bindEnvironment((error, ldapUsers, { next, end } = {}) => { - if (error) { - throw error; - } - - ldapUsers.forEach((ldapUser) => { - count++; - - const uniqueId = getLdapUserUniqueID(ldapUser); - // Look to see if user already exists - const userQuery = { - 'services.ldap.id': uniqueId.value, - }; - - logger.debug('userQuery', userQuery); - - let username; - if (settings.get('LDAP_Username_Field') !== '') { - username = slug(getLdapUsername(ldapUser)); - } - - // Add user if it was not added before - let user = Meteor.users.findOne(userQuery); - - if (!user && username && settings.get('LDAP_Merge_Existing_Users') === true) { - const userQuery = { - username, - }; - - logger.debug('userQuery merge', userQuery); - - user = Meteor.users.findOne(userQuery); - if (user) { - syncUserData(user, ldapUser, ldap); - } - } - - if (!user) { - addLdapUser(ldapUser, username, undefined, ldap); - } - - if (count % 100 === 0) { - logger.info('Import running. Users imported until now:', count); - } - }); - - if (end) { - logger.info('Import finished. Users imported:', count); - } - - next(count); - })); -} - -export function sync() { - if (settings.get('LDAP_Enable') !== true) { - return; - } - - const ldap = new LDAP(); - - try { - ldap.connectSync(); - - let users; - if (settings.get('LDAP_Background_Sync_Keep_Existant_Users_Updated') === true) { - users = Users.findLDAPUsers(); - } - - if (settings.get('LDAP_Background_Sync_Import_New_Users') === true) { - importNewUsers(ldap); - } - - if (settings.get('LDAP_Background_Sync_Keep_Existant_Users_Updated') === true) { - users.forEach(function(user) { - let ldapUser; - - if (user.services && user.services.ldap && user.services.ldap.id) { - ldapUser = ldap.getUserByIdSync(user.services.ldap.id, user.services.ldap.idAttribute); - } else { - ldapUser = ldap.getUserByUsernameSync(user.username); - } - - if (ldapUser) { - syncUserData(user, ldapUser, ldap); - } - - callbacks.run('ldap.afterSyncExistentUser', { ldapUser, user }); - }); - } - } catch (error) { - logger.error(error); - return error; - } - return true; -} - -const jobName = 'LDAP_Sync'; - -const addCronJob = _.debounce(Meteor.bindEnvironment(function addCronJobDebounced() { - if (settings.get('LDAP_Background_Sync') !== true) { - logger.info('Disabling LDAP Background Sync'); - if (SyncedCron.nextScheduledAtDate(jobName)) { - SyncedCron.remove(jobName); - } - return; - } - - if (settings.get('LDAP_Background_Sync_Interval')) { - logger.info('Enabling LDAP Background Sync'); - SyncedCron.add({ - name: jobName, - schedule: (parser) => parser.text(settings.get('LDAP_Background_Sync_Interval')), - job() { - sync(); - }, - }); - } -}), 500); - -Meteor.startup(() => { - Meteor.defer(() => { - settings.get('LDAP_Background_Sync', addCronJob); - settings.get('LDAP_Background_Sync_Interval', addCronJob); - }); -}); diff --git a/app/ldap/server/syncUsers.js b/app/ldap/server/syncUsers.js deleted file mode 100644 index 312d4dbe96d23..0000000000000 --- a/app/ldap/server/syncUsers.js +++ /dev/null @@ -1,31 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { sync } from './sync'; -import { hasRole } from '../../authorization'; -import { settings } from '../../settings'; - -Meteor.methods({ - ldap_sync_now() { - const user = Meteor.user(); - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'ldap_sync_users' }); - } - - if (!hasRole(user._id, 'admin')) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'ldap_sync_users' }); - } - - if (settings.get('LDAP_Enable') !== true) { - throw new Meteor.Error('LDAP_disabled'); - } - - Meteor.defer(() => { - sync(); - }); - - return { - message: 'Sync_in_progress', - params: [], - }; - }, -}); diff --git a/app/ldap/server/testConnection.js b/app/ldap/server/testConnection.js deleted file mode 100644 index 1511a944e238a..0000000000000 --- a/app/ldap/server/testConnection.js +++ /dev/null @@ -1,42 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import LDAP from './ldap'; -import { hasRole } from '../../authorization'; -import { settings } from '../../settings'; - -Meteor.methods({ - ldap_test_connection() { - const user = Meteor.user(); - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'ldap_test_connection' }); - } - - if (!hasRole(user._id, 'admin')) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'ldap_test_connection' }); - } - - if (settings.get('LDAP_Enable') !== true) { - throw new Meteor.Error('LDAP_disabled'); - } - - let ldap; - try { - ldap = new LDAP(); - ldap.connectSync(); - } catch (error) { - console.log(error); - throw new Meteor.Error(error.message); - } - - try { - ldap.bindIfNecessary(); - } catch (error) { - throw new Meteor.Error(error.name || error.message); - } - - return { - message: 'Connection_success', - params: [], - }; - }, -}); diff --git a/app/lib/README.md b/app/lib/README.md index 547fa1ee25d96..2308f1e0da4fa 100644 --- a/app/lib/README.md +++ b/app/lib/README.md @@ -8,7 +8,7 @@ This package contains the main libraries of Rocket.Chat. This is an example to create settings: ```javascript -RocketChat.settings.addGroup('Settings_Group', function() { +settingsRegistry.addGroup('Settings_Group', function() { this.add('SettingInGroup', 'default_value', { type: 'boolean', public: true }); this.section('Group_Section', function() { @@ -24,7 +24,7 @@ RocketChat.settings.addGroup('Settings_Group', function() { }); ``` -`RocketChat.settings.add` type: +`settingsRegistry.add` type: * `string` - Stores a string value * Additional options: @@ -40,7 +40,7 @@ RocketChat.settings.addGroup('Settings_Group', function() { * `actionText`: Translatable value of the button * `asset` - Creates an upload field -`RocketChat.settings.add` options: +`settingsRegistry.add` options: * `description` - Description of the setting * `public` - Boolean to set if the setting should be sent to client or not diff --git a/app/lib/client/CustomTranslations.js b/app/lib/client/CustomTranslations.js deleted file mode 100644 index 6c128512c4dd9..0000000000000 --- a/app/lib/client/CustomTranslations.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Session } from 'meteor/session'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { Tracker } from 'meteor/tracker'; - -import { applyCustomTranslations } from '../../utils'; - -Meteor.startup(function() { - Tracker.autorun(function() { - // Re apply translations if tap language was changed - Session.get(TAPi18n._loaded_lang_session_key); - applyCustomTranslations(); - }); -}); diff --git a/app/lib/client/UserDeleted.js b/app/lib/client/UserDeleted.js deleted file mode 100644 index 8be7d5ed5d460..0000000000000 --- a/app/lib/client/UserDeleted.js +++ /dev/null @@ -1,12 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { ChatMessage } from '../../models'; -import { Notifications } from '../../notifications'; - -Meteor.startup(function() { - Notifications.onLogged('Users:Deleted', ({ userId }) => - ChatMessage.remove({ - 'u._id': userId, - }), - ); -}); diff --git a/app/lib/client/index.js b/app/lib/client/index.js index 64ed16e27b14c..321f6f1e5bf32 100644 --- a/app/lib/client/index.js +++ b/app/lib/client/index.js @@ -1,11 +1,6 @@ -import '../lib/startup/settingsOnLoadSiteUrl'; +import './startup/settingsOnLoadSiteUrl'; import '../lib/MessageTypes'; -import './CustomTranslations'; import './OAuthProxy'; -import './UserDeleted'; -import './lib/startup/commands'; -import './lib/settings'; -import './lib/userRoles'; import './methods/sendMessage'; import './views/customFieldsForm.html'; import './views/customFieldsForm'; diff --git a/app/lib/client/lib/formatDate.js b/app/lib/client/lib/formatDate.js deleted file mode 100644 index 43251237c9a55..0000000000000 --- a/app/lib/client/lib/formatDate.js +++ /dev/null @@ -1,53 +0,0 @@ -import moment from 'moment'; -import mem from 'mem'; -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { getUserPreference, t } from '../../../utils'; -import { settings } from '../../../settings'; - -let lastDay = t('yesterday'); -let clockMode; -let sameDay; -const dayFormat = ['h:mm A', 'H:mm']; - -Meteor.startup(() => Tracker.autorun(() => { - clockMode = getUserPreference(Meteor.userId(), 'clockMode', false); - sameDay = dayFormat[clockMode - 1] || settings.get('Message_TimeFormat'); - lastDay = t('yesterday'); -})); - -export const formatTime = (time) => { - switch (clockMode) { - case 1: - case 2: - return moment(time).format(sameDay); - default: - return moment(time).format(settings.get('Message_TimeFormat')); - } -}; - -export const formatDateAndTime = (time) => { - switch (clockMode) { - case 1: - return moment(time).format('MMMM D, Y h:mm A'); - case 2: - return moment(time).format('MMMM D, Y H:mm'); - default: - return moment(time).format(settings.get('Message_TimeAndDateFormat')); - } -}; - -const sameElse = function(now) { - const diff = Math.ceil(this.diff(now, 'years', true)); - return diff < 0 ? 'MMM D YYYY' : 'MMM D'; -}; - -export const timeAgo = (date) => moment(date).calendar(null, { - lastDay: `[${ lastDay }]`, - sameDay, - lastWeek: 'dddd', - sameElse, -}); - -export const formatDate = mem((time) => moment(time).format(settings.get('Message_DateFormat')), { maxAge: 5000 }); diff --git a/app/lib/client/lib/index.js b/app/lib/client/lib/index.js index 4cc975c68ce28..5cade0413ef38 100644 --- a/app/lib/client/lib/index.js +++ b/app/lib/client/lib/index.js @@ -1,12 +1,2 @@ -/* - What is this file? Great question! To make Rocket.Chat more "modular" - and to make the "rocketchat:lib" package more of a core package - with the libraries, this index file contains the exported members - for the *client* pieces of code which does include the shared - library files. -*/ -import * as DateFormat from './formatDate'; - export { RocketChatAnnouncement } from './RocketChatAnnouncement'; export { LoginPresence } from './LoginPresence'; -export { DateFormat }; diff --git a/app/lib/client/lib/settings.js b/app/lib/client/lib/settings.js deleted file mode 100644 index 5d909e0a2d395..0000000000000 --- a/app/lib/client/lib/settings.js +++ /dev/null @@ -1,58 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; -import toastr from 'toastr'; - -import { t } from '../../../utils'; -import { settings } from '../../../settings'; -import { hasRole } from '../../../authorization'; -import { Roles } from '../../../models/client'; -import { imperativeModal } from '../../../../client/lib/imperativeModal'; -import UrlChangeModal from '../../../../client/components/UrlChangeModal'; -import { isSyncReady } from '../../../../client/lib/userData'; - -Meteor.startup(function() { - Tracker.autorun(function(c) { - if (!Meteor.userId()) { - return; - } - - if (!Roles.ready.get() || !isSyncReady.get()) { - return; - } - - if (hasRole(Meteor.userId(), 'admin') === false) { - return c.stop(); - } - - const siteUrl = settings.get('Site_Url'); - if (!siteUrl) { - return; - } - - const currentUrl = location.origin + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX; - if (__meteor_runtime_config__.ROOT_URL.replace(/\/$/, '') !== currentUrl) { - const confirm = () => { - imperativeModal.close(); - Meteor.call('saveSetting', 'Site_Url', currentUrl, function() { - toastr.success(t('Saved')); - }); - }; - imperativeModal.open({ - component: UrlChangeModal, - props: { - onConfirm: confirm, - siteUrl, - currentUrl, - onClose: imperativeModal.close, - }, - }); - } - - const documentDomain = settings.get('Document_Domain'); - if (documentDomain) { - window.document.domain = documentDomain; - } - - return c.stop(); - }); -}); diff --git a/app/lib/client/lib/startup/commands.js b/app/lib/client/lib/startup/commands.js deleted file mode 100644 index 0d9a7179ae9d5..0000000000000 --- a/app/lib/client/lib/startup/commands.js +++ /dev/null @@ -1,22 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { slashCommands, APIClient } from '../../../../utils'; - -// Track logins and when they login, get the commands -(() => { - let oldUserId = null; - - Tracker.autorun(() => { - const newUserId = Meteor.userId(); - if (oldUserId === null && newUserId) { - APIClient.v1.get('commands.list').then(function _loadedCommands(result) { - result.commands.forEach((command) => { - slashCommands.commands[command.command] = command; - }); - }); - } - - oldUserId = Meteor.userId(); - }); -})(); diff --git a/app/lib/client/lib/userRoles.js b/app/lib/client/lib/userRoles.js deleted file mode 100644 index c0414276b4082..0000000000000 --- a/app/lib/client/lib/userRoles.js +++ /dev/null @@ -1,42 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { UserRoles, RoomRoles, ChatMessage } from '../../../models'; -import { handleError } from '../../../utils'; -import { Notifications } from '../../../notifications'; - -Meteor.startup(function() { - Tracker.autorun(function() { - if (Meteor.userId()) { - Meteor.call('getUserRoles', (error, results) => { - if (error) { - return handleError(error); - } - - for (const record of results) { - UserRoles.upsert({ _id: record._id }, record); - } - }); - - Notifications.onLogged('roles-change', function(role) { - if (role.type === 'added') { - if (role.scope) { - RoomRoles.upsert({ rid: role.scope, 'u._id': role.u._id }, { $setOnInsert: { u: role.u }, $addToSet: { roles: role._id } }); - } else { - UserRoles.upsert({ _id: role.u._id }, { $addToSet: { roles: role._id }, $set: { username: role.u.username } }); - ChatMessage.update({ 'u._id': role.u._id }, { $addToSet: { roles: role._id } }, { multi: true }); - } - } else if (role.type === 'removed') { - if (role.scope) { - RoomRoles.update({ rid: role.scope, 'u._id': role.u._id }, { $pull: { roles: role._id } }); - } else { - UserRoles.update({ _id: role.u._id }, { $pull: { roles: role._id } }); - ChatMessage.update({ 'u._id': role.u._id }, { $pull: { roles: role._id } }, { multi: true }); - } - } else if (role.type === 'changed') { - ChatMessage.update({ roles: role._id }, { $inc: { rerender: 1 } }, { multi: true }); - } - }); - } - }); -}); diff --git a/app/lib/client/methods/sendMessage.js b/app/lib/client/methods/sendMessage.js index d765eefe27c7c..3e30b754b690c 100644 --- a/app/lib/client/methods/sendMessage.js +++ b/app/lib/client/methods/sendMessage.js @@ -1,13 +1,13 @@ import { Meteor } from 'meteor/meteor'; import { TimeSync } from 'meteor/mizzao:timesync'; import s from 'underscore.string'; -import toastr from 'toastr'; import { ChatMessage } from '../../../models'; import { settings } from '../../../settings'; import { callbacks } from '../../../callbacks'; -import { promises } from '../../../promises/client'; import { t } from '../../../utils/client'; +import { dispatchToastMessage } from '../../../../client/lib/toast'; +import { onClientMessageReceived } from '../../../../client/lib/onClientMessageReceived'; Meteor.methods({ sendMessage(message) { @@ -16,7 +16,7 @@ Meteor.methods({ } const messageAlreadyExists = message._id && ChatMessage.findOne({ _id: message._id }); if (messageAlreadyExists) { - return toastr.error(t('Message_Already_Sent')); + return dispatchToastMessage({ type: 'error', message: t('Message_Already_Sent') }); } const user = Meteor.user(); message.ts = isNaN(TimeSync.serverOffset()) ? new Date() : new Date(Date.now() + TimeSync.serverOffset()); @@ -32,7 +32,7 @@ Meteor.methods({ message.unread = true; } message = callbacks.run('beforeSaveMessage', message); - promises.run('onClientMessageReceived', message).then(function(message) { + onClientMessageReceived(message).then(function(message) { ChatMessage.insert(message); return callbacks.run('afterSaveMessage', message); }); diff --git a/app/lib/client/startup/settingsOnLoadSiteUrl.ts b/app/lib/client/startup/settingsOnLoadSiteUrl.ts new file mode 100644 index 0000000000000..c61ec53e19d1a --- /dev/null +++ b/app/lib/client/startup/settingsOnLoadSiteUrl.ts @@ -0,0 +1,18 @@ +import { Meteor } from 'meteor/meteor'; +import { Tracker } from 'meteor/tracker'; + +import { settings } from '../../../settings/client'; + +Meteor.startup(() => { + Tracker.autorun(() => { + const value = settings.get('Site_Url'); + if (value == null || value.trim() === '') { + return; + } + (window as any).__meteor_runtime_config__.ROOT_URL = value; + + if (Meteor.absoluteUrl.defaultOptions && Meteor.absoluteUrl.defaultOptions.rootUrl) { + Meteor.absoluteUrl.defaultOptions.rootUrl = value; + } + }); +}); diff --git a/app/lib/lib/MessageTypes.js b/app/lib/lib/MessageTypes.js index be706e8d3a09e..0ac925f68c00f 100644 --- a/app/lib/lib/MessageTypes.js +++ b/app/lib/lib/MessageTypes.js @@ -179,6 +179,46 @@ Meteor.startup(function() { }; }, }); + MessageTypes.registerType({ + id: 'room-removed-read-only', + system: true, + message: 'room_removed_read_only', + data(message) { + return { + user_by: message.u.username, + }; + }, + }); + MessageTypes.registerType({ + id: 'room-set-read-only', + system: true, + message: 'room_set_read_only', + data(message) { + return { + user_by: message.u.username, + }; + }, + }); + MessageTypes.registerType({ + id: 'room-allowed-reacting', + system: true, + message: 'room_allowed_reacting', + data(message) { + return { + user_by: message.u.username, + }; + }, + }); + MessageTypes.registerType({ + id: 'room-disallowed-reacting', + system: true, + message: 'room_disallowed_reacting', + data(message) { + return { + user_by: message.u.username, + }; + }, + }); MessageTypes.registerType({ id: 'room_e2e_enabled', system: true, @@ -262,4 +302,20 @@ export const MessageTypesValues = [ key: 'room_e2e_disabled', i18nLabel: 'Message_HideType_room_disabled_encryption', }, + { + key: 'room-removed-read-only', + i18nLabel: 'Message_HideType_room_removed_read_only', + }, + { + key: 'room-set-read-only', + i18nLabel: 'Message_HideType_room_set_read_only', + }, + { + key: 'room-disallowed-reacting', + i18nLabel: 'Message_HideType_room_disallowed_reacting', + }, + { + key: 'room-allowed-reacting', + i18nLabel: 'Message_HideType_room_allowed_reacting', + }, ]; diff --git a/app/lib/lib/startup/settingsOnLoadSiteUrl.js b/app/lib/lib/startup/settingsOnLoadSiteUrl.js deleted file mode 100644 index 2fb003f72df05..0000000000000 --- a/app/lib/lib/startup/settingsOnLoadSiteUrl.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { WebAppInternals } from 'meteor/webapp'; - -import { settings } from '../../../settings'; - -export let hostname; - -settings.get('Site_Url', function(key, value) { - if (value == null || value.trim() === '') { - return; - } - let host = value.replace(/\/$/, ''); - // let prefix = ''; - const match = value.match(/([^\/]+\/{2}[^\/]+)(\/.+)/); - if (match != null) { - host = match[1]; - // prefix = match[2].replace(/\/$/, ''); - } - __meteor_runtime_config__.ROOT_URL = value; - - if (Meteor.absoluteUrl.defaultOptions && Meteor.absoluteUrl.defaultOptions.rootUrl) { - Meteor.absoluteUrl.defaultOptions.rootUrl = value; - } - if (Meteor.isServer) { - hostname = host.replace(/^https?:\/\//, ''); - process.env.MOBILE_ROOT_URL = host; - process.env.MOBILE_DDP_URL = host; - if (typeof WebAppInternals !== 'undefined' && WebAppInternals.generateBoilerplate) { - return WebAppInternals.generateBoilerplate(); - } - } -}); diff --git a/app/lib/server/functions/addOAuthService.js b/app/lib/server/functions/addOAuthService.js deleted file mode 100644 index cb5013129e197..0000000000000 --- a/app/lib/server/functions/addOAuthService.js +++ /dev/null @@ -1,38 +0,0 @@ -/* eslint no-multi-spaces: 0 */ -/* eslint comma-spacing: 0 */ -import { capitalize } from '@rocket.chat/string-helpers'; - -import { settings } from '../../../settings'; - -export function addOAuthService(name, values = {}) { - name = name.toLowerCase().replace(/[^a-z0-9_]/g, ''); - name = capitalize(name); - settings.add(`Accounts_OAuth_Custom-${ name }` , values.enabled || false , { type: 'boolean', group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Enable', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-url` , values.serverURL || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'URL', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-token_path` , values.tokenPath || '/oauth/token' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Token_Path', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-token_sent_via` , values.tokenSentVia || 'payload' , { type: 'select' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Token_Sent_Via', persistent: true, values: [{ key: 'header', i18nLabel: 'Header' }, { key: 'payload', i18nLabel: 'Payload' }] }); - settings.add(`Accounts_OAuth_Custom-${ name }-identity_token_sent_via`, values.identityTokenSentVia || 'default' , { type: 'select' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Identity_Token_Sent_Via', persistent: true, values: [{ key: 'default', i18nLabel: 'Same_As_Token_Sent_Via' }, { key: 'header', i18nLabel: 'Header' }, { key: 'payload', i18nLabel: 'Payload' }] }); - settings.add(`Accounts_OAuth_Custom-${ name }-identity_path` , values.identityPath || '/me' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Identity_Path', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-authorize_path` , values.authorizePath || '/oauth/authorize' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Authorize_Path', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-scope` , values.scope || 'openid' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Scope', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-access_token_param` , values.accessTokenParam || 'access_token' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Access_Token_Param', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-id` , values.clientId || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_id', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-secret` , values.clientSecret || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Secret', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-login_style` , values.loginStyle || 'popup' , { type: 'select' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Login_Style', persistent: true, values: [{ key: 'redirect', i18nLabel: 'Redirect' }, { key: 'popup', i18nLabel: 'Popup' }, { key: '', i18nLabel: 'Default' }] }); - settings.add(`Accounts_OAuth_Custom-${ name }-button_label_text` , values.buttonLabelText || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Text', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-button_label_color` , values.buttonLabelColor || '#FFFFFF' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Color', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-button_color` , values.buttonColor || '#1d74f5' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Button_Color', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-key_field` , values.keyField || 'username' , { 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' }] }); - settings.add(`Accounts_OAuth_Custom-${ name }-username_field` , values.usernameField || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Username_Field', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-email_field` , values.emailField || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Email_Field', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-name_field` , values.nameField || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Name_Field', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-avatar_field` , values.avatarField || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Avatar_Field', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-roles_claim` , values.rolesClaim || 'roles' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Roles_Claim', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-groups_claim` , values.groupsClaim || 'groups' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Groups_Claim', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-channels_admin` , values.channelsAdmin || 'rocket.cat' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Channel_Admin', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-map_channels` , values.mapChannels || false , { type: 'boolean', group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Map_Channels', persistent: true }); - settings.add(`Accounts_OAuth_Custom-${ name }-merge_roles` , values.mergeRoles || false , { type: 'boolean', group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Merge_Roles', persistent: true }); - settings.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 }); - settings.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 }); - settings.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/addOAuthService.ts b/app/lib/server/functions/addOAuthService.ts new file mode 100644 index 0000000000000..1a2a220fcddc7 --- /dev/null +++ b/app/lib/server/functions/addOAuthService.ts @@ -0,0 +1,75 @@ +/* eslint no-multi-spaces: 0 */ +/* eslint comma-spacing: 0 */ +import { capitalize } from '@rocket.chat/string-helpers'; + +import { settingsRegistry } from '../../../settings/server'; + +export function addOAuthService(name: string, values: { [k: string]: string | boolean| undefined } = {}): void { + name = name.toLowerCase().replace(/[^a-z0-9_]/g, ''); + name = capitalize(name); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }` , values.enabled || false , { type: 'boolean', group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Enable', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-url` , values.serverURL || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'URL', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-token_path` , values.tokenPath || '/oauth/token' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Token_Path', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-token_sent_via` , values.tokenSentVia || 'payload' , { type: 'select' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Token_Sent_Via', persistent: true, values: [{ key: 'header', i18nLabel: 'Header' }, { key: 'payload', i18nLabel: 'Payload' }] }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-identity_token_sent_via`, values.identityTokenSentVia || 'default' , { type: 'select' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Identity_Token_Sent_Via', persistent: true, values: [{ key: 'default', i18nLabel: 'Same_As_Token_Sent_Via' }, { key: 'header', i18nLabel: 'Header' }, { key: 'payload', i18nLabel: 'Payload' }] }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-identity_path` , values.identityPath || '/me' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Identity_Path', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-authorize_path` , values.authorizePath || '/oauth/authorize' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Authorize_Path', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-scope` , values.scope || 'openid' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Scope', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-access_token_param` , values.accessTokenParam || 'access_token' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Access_Token_Param', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-id` , values.clientId || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_id', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-secret` , values.clientSecret || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Secret', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-login_style` , values.loginStyle || 'popup' , { type: 'select' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Login_Style', persistent: true, values: [{ key: 'redirect', i18nLabel: 'Redirect' }, { key: 'popup', i18nLabel: 'Popup' }, { key: '', i18nLabel: 'Default' }] }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-button_label_text` , values.buttonLabelText || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Text', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-button_label_color` , values.buttonLabelColor || '#FFFFFF' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Color', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-button_color` , values.buttonColor || '#1d74f5' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Button_Color', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-key_field` , values.keyField || 'username' , { 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' }] }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-username_field` , values.usernameField || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Username_Field', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-email_field` , values.emailField || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Email_Field', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-name_field` , values.nameField || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Name_Field', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-avatar_field` , values.avatarField || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Avatar_Field', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-roles_claim` , values.rolesClaim || 'roles', { type: 'string', + group: 'OAuth', + section: `Custom OAuth: ${ name }`, + i18nLabel: 'Accounts_OAuth_Custom_Roles_Claim', + enterprise: true, + invalidValue: 'roles', + modules: ['oauth-enterprise'] }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-groups_claim` , values.groupsClaim || 'groups', { type: 'string', + group: 'OAuth', + section: `Custom OAuth: ${ name }`, + i18nLabel: 'Accounts_OAuth_Custom_Groups_Claim', + enterprise: true, + invalidValue: 'groups', + modules: ['oauth-enterprise'] }); + + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-channels_admin` , values.channelsAdmin || 'rocket.cat' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Channel_Admin', persistent: true }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-map_channels` , values.mapChannels || false, { type: 'boolean', + group: 'OAuth', + section: `Custom OAuth: ${ name }`, + i18nLabel: 'Accounts_OAuth_Custom_Map_Channels', + enterprise: true, + invalidValue: false, + modules: ['oauth-enterprise'] }); + settingsRegistry.add(`Accounts_OAuth_Custom-${ name }-merge_roles` , values.mergeRoles || false, { type: 'boolean', + group: 'OAuth', + section: `Custom OAuth: ${ name }`, + i18nLabel: 'Accounts_OAuth_Custom_Merge_Roles', + 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/checkUsernameAvailability.js b/app/lib/server/functions/checkUsernameAvailability.js index f152e93266b79..305922981ec44 100644 --- a/app/lib/server/functions/checkUsernameAvailability.js +++ b/app/lib/server/functions/checkUsernameAvailability.js @@ -3,7 +3,7 @@ import s from 'underscore.string'; import _ from 'underscore'; import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { Team } from '../../../../server/sdk'; import { validateName } from './validateName'; @@ -11,7 +11,7 @@ let usernameBlackList = []; const toRegExp = (username) => new RegExp(`^${ escapeRegExp(username).trim() }$`, 'i'); -settings.get('Accounts_BlockedUsernameList', (key, value) => { +settings.watch('Accounts_BlockedUsernameList', (value) => { usernameBlackList = ['all', 'here'].concat(value.split(',')).map(toRegExp); }); diff --git a/app/lib/server/functions/closeOmnichannelConversations.ts b/app/lib/server/functions/closeOmnichannelConversations.ts index 46be29d8a9848..c5cff5081f35f 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 8f698842e205e..96e563b1a4648 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 680517db54055..4193774e11364 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/getFullUserData.js b/app/lib/server/functions/getFullUserData.js index 9113c777e81ee..c12ab44230d9c 100644 --- a/app/lib/server/functions/getFullUserData.js +++ b/app/lib/server/functions/getFullUserData.js @@ -1,8 +1,5 @@ -import s from 'underscore.string'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; - import { Logger } from '../../../logger'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { Users } from '../../../models/server'; import { hasPermission } from '../../../authorization'; @@ -38,7 +35,7 @@ const fullFields = { let publicCustomFields = {}; let customFields = {}; -settings.get('Accounts_CustomFields', (key, value) => { +settings.watch('Accounts_CustomFields', (value) => { publicCustomFields = {}; customFields = {}; @@ -92,45 +89,11 @@ export function getFullUserDataByIdOrUsername({ userId, filterId, filterUsername fields, }; const user = Users.findOneByIdOrUsername(filterId || filterUsername, options); + if (!user) { + return null; + } + user.canViewAllInfo = canViewAllInfo; return myself ? user : removePasswordInfo(user); } - -export const getFullUserData = function({ userId, filter, limit: l }) { - const username = s.trim(filter); - const userToRetrieveFullUserData = username && Users.findOneByUsername(username, { fields: { username: 1 } }); - if (!userToRetrieveFullUserData) { - return; - } - - const isMyOwnInfo = userToRetrieveFullUserData && userToRetrieveFullUserData._id === userId; - const viewFullOtherUserInfo = hasPermission(userId, 'view-full-other-user-info'); - - const canViewAllInfo = isMyOwnInfo || viewFullOtherUserInfo; - - const limit = !viewFullOtherUserInfo ? 1 : l; - - if (!username && limit <= 1) { - return undefined; - } - - const fields = getFields(canViewAllInfo); - - const options = { - fields, - limit, - sort: { username: 1 }, - }; - - if (!username) { - return Users.find({}, options); - } - - if (limit === 1) { - return Users.findByUsername(userToRetrieveFullUserData.username, options); - } - - const usernameReg = new RegExp(escapeRegExp(username), 'i'); - return Users.findByUsernameNameOrEmailAddress(usernameReg, options); -}; diff --git a/app/lib/server/functions/index.js b/app/lib/server/functions/index.js index fb3c898613412..5286201e0100c 100644 --- a/app/lib/server/functions/index.js +++ b/app/lib/server/functions/index.js @@ -10,7 +10,6 @@ export { createDirectRoom } from './createDirectRoom'; export { deleteMessage } from './deleteMessage'; export { deleteRoom } from './deleteRoom'; export { deleteUser } from './deleteUser'; -export { getFullUserData } from './getFullUserData'; export { getRoomByNameOrIdWithOptionToJoin } from './getRoomByNameOrIdWithOptionToJoin'; export { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; export { generateUsernameSuggestion } from './getUsernameSuggestion'; diff --git a/app/lib/server/functions/notifications/audio.js b/app/lib/server/functions/notifications/audio.js deleted file mode 100644 index 12ce5fb8d5fc8..0000000000000 --- a/app/lib/server/functions/notifications/audio.js +++ /dev/null @@ -1,49 +0,0 @@ -import { metrics } from '../../../../metrics'; -import { settings } from '../../../../settings'; -import { Notifications } from '../../../../notifications'; - -export function shouldNotifyAudio({ - disableAllMessageNotifications, - status, - statusConnection, - audioNotifications, - hasMentionToAll, - hasMentionToHere, - isHighlighted, - hasMentionToUser, - hasReplyToThread, - roomType, - isThread, -}) { - if (disableAllMessageNotifications && audioNotifications == null && !isHighlighted && !hasMentionToUser && !hasReplyToThread) { - return false; - } - - if (statusConnection === 'offline' || status === 'busy' || audioNotifications === 'nothing') { - return false; - } - - if (!audioNotifications) { - if (settings.get('Accounts_Default_User_Preferences_audioNotifications') === 'all' && (!isThread || hasReplyToThread)) { - return true; - } - if (settings.get('Accounts_Default_User_Preferences_audioNotifications') === 'nothing') { - return false; - } - } - - return (roomType === 'd' || (!disableAllMessageNotifications && (hasMentionToAll || hasMentionToHere)) || isHighlighted || audioNotifications === 'all' || hasMentionToUser) && (!isThread || hasReplyToThread); -} - -export function notifyAudioUser(userId, message, room) { - metrics.notificationsSent.inc({ notification_type: 'audio' }); - Notifications.notifyUser(userId, 'audioNotification', { - payload: { - _id: message._id, - rid: message.rid, - sender: message.u, - type: room.t, - name: room.name, - }, - }); -} diff --git a/app/lib/server/functions/notifications/email.js b/app/lib/server/functions/notifications/email.js index 3583899487ea9..9a8b077da3596 100644 --- a/app/lib/server/functions/notifications/email.js +++ b/app/lib/server/functions/notifications/email.js @@ -4,7 +4,7 @@ import s from 'underscore.string'; import { escapeHTML } from '@rocket.chat/string-helpers'; import * as Mailer from '../../../../mailer'; -import { settings } from '../../../../settings'; +import { settings } from '../../../../settings/server'; import { roomTypes } from '../../../../utils'; import { metrics } from '../../../../metrics'; import { callbacks } from '../../../../callbacks'; @@ -13,7 +13,7 @@ import { getURL } from '../../../../utils/server'; let advice = ''; let goToMessage = ''; Meteor.startup(() => { - settings.get('email_style', function() { + settings.watch('email_style', function() { goToMessage = Mailer.inlinecss('

{Offline_Link_Message}

'); }); Mailer.getTemplate('Email_Footer_Direct_Reply', (value) => { diff --git a/app/lib/server/functions/notifications/mobile.js b/app/lib/server/functions/notifications/mobile.js index 4dca10c465610..7211f9e0dfcfd 100644 --- a/app/lib/server/functions/notifications/mobile.js +++ b/app/lib/server/functions/notifications/mobile.js @@ -96,10 +96,10 @@ export function shouldNotifyMobile({ } if (!mobilePushNotifications) { - if (settings.get('Accounts_Default_User_Preferences_mobileNotifications') === 'all' && (!isThread || hasReplyToThread)) { + if (settings.get('Accounts_Default_User_Preferences_pushNotifications') === 'all' && (!isThread || hasReplyToThread)) { return true; } - if (settings.get('Accounts_Default_User_Preferences_mobileNotifications') === 'nothing') { + if (settings.get('Accounts_Default_User_Preferences_pushNotifications') === 'nothing') { return false; } } diff --git a/app/lib/server/functions/processWebhookMessage.js b/app/lib/server/functions/processWebhookMessage.js index 63eeeb16252bc..cfa23e7f5726f 100644 --- a/app/lib/server/functions/processWebhookMessage.js +++ b/app/lib/server/functions/processWebhookMessage.js @@ -1,21 +1,14 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import s from 'underscore.string'; -import mem from 'mem'; import { getRoomByNameOrIdWithOptionToJoin } from './getRoomByNameOrIdWithOptionToJoin'; import { sendMessage } from './sendMessage'; import { validateRoomMessagePermissions } from '../../../authorization/server/functions/canSendMessage'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { getDirectMessageByIdWithOptionToJoin, getDirectMessageByNameOrIdWithOptionToJoin } from './getDirectMessageByNameOrIdWithOptionToJoin'; -// show deprecation warning only once per hour for each integration -const showDeprecation = mem(({ integration, channels, username }, error) => { - console.warn(`Warning: The integration "${ integration }" failed to send a message to "${ [].concat(channels).join(',') }" because user "${ username }" doesn't have permission or is not a member of the channel.`); - console.warn('This behavior is deprecated and starting from version v4.0.0 the following error will be thrown and the message will not be sent.'); - console.error(error); -}, { maxAge: 360000, cacheKey: (integration) => JSON.stringify(integration) }); - -export const processWebhookMessage = function(messageObj, user, defaultValues = { channel: '', alias: '', avatar: '', emoji: '' }, integration = null) { +export const processWebhookMessage = function(messageObj, user, defaultValues = { channel: '', alias: '', avatar: '', emoji: '' }) { const sentData = []; const channels = [].concat(messageObj.channel || messageObj.roomId || defaultValues.channel); @@ -52,7 +45,7 @@ export const processWebhookMessage = function(messageObj, user, defaultValues = } if (messageObj.attachments && !Array.isArray(messageObj.attachments)) { - console.log('Attachments should be Array, ignoring value'.red, messageObj.attachments); + SystemLogger.warn({ msg: 'Attachments should be Array, ignoring value', attachments: messageObj.attachments }); messageObj.attachments = undefined; } @@ -86,18 +79,7 @@ export const processWebhookMessage = function(messageObj, user, defaultValues = } } - try { - validateRoomMessagePermissions(room, { uid: user._id, ...user }); - } catch (error) { - if (!integration) { - throw error; - } - showDeprecation({ - integration: integration.name, - channels: integration.channel, - username: integration.username, - }, error); - } + validateRoomMessagePermissions(room, { uid: user._id, ...user }); const messageReturn = sendMessage(user, message, room); sentData.push({ channel, message: messageReturn }); diff --git a/app/lib/server/functions/relinquishRoomOwnerships.js b/app/lib/server/functions/relinquishRoomOwnerships.js index 7c56e3bc05a52..f5c403f1b2b14 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/saveUser.js b/app/lib/server/functions/saveUser.js index eeb95d7e50187..83ea5273d870e 100644 --- a/app/lib/server/functions/saveUser.js +++ b/app/lib/server/functions/saveUser.js @@ -9,10 +9,11 @@ import { getRoles, hasPermission } from '../../../authorization'; import { settings } from '../../../settings'; import { passwordPolicy } from '../lib/passwordPolicy'; import { validateEmailDomain } from '../lib'; -import { validateUserRoles } from '../../../../ee/app/authorization/server/validateUserRoles'; import { getNewUserRoles } from '../../../../server/services/user/lib/getNewUserRoles'; import { saveUserIdentity } from './saveUserIdentity'; import { checkEmailAvailability, checkUsernameAvailability, setUserAvatar, setEmail, setStatusText } from '.'; +import { Users } from '../../../models/server'; +import { callbacks } from '../../../callbacks/server'; let html = ''; let passwordChangedHtml = ''; @@ -98,7 +99,7 @@ function validateUserData(userId, userData) { } if (userData.roles) { - validateUserRoles(userId, userData); + callbacks.run('validateUserRoles', userData); } let nameValidation; @@ -152,8 +153,12 @@ export function validateUserEditing(userId, userData) { const canEditOtherUserInfo = hasPermission(userId, 'edit-other-user-info'); const canEditOtherUserPassword = hasPermission(userId, 'edit-other-user-password'); + const user = Users.findOneById(userData._id); - if (userData.roles && !hasPermission(userId, 'assign-roles')) { + const isEditingUserRoles = (previousRoles, newRoles) => typeof newRoles !== 'undefined' && !_.isEqual(_.sortBy(previousRoles), _.sortBy(newRoles)); + const isEditingField = (previousValue, newValue) => typeof newValue !== 'undefined' && newValue !== previousValue; + + if (isEditingUserRoles(user.roles, userData.roles) && !hasPermission(userId, 'assign-roles')) { throw new Meteor.Error('error-action-not-allowed', 'Assign roles is not allowed', { method: 'insertOrUpdateUser', action: 'Assign_role', @@ -167,28 +172,28 @@ export function validateUserEditing(userId, userData) { }); } - if (userData.username && !settings.get('Accounts_AllowUsernameChange') && (!canEditOtherUserInfo || editingMyself)) { + if (isEditingField(user.username, userData.username) && !settings.get('Accounts_AllowUsernameChange') && (!canEditOtherUserInfo || editingMyself)) { throw new Meteor.Error('error-action-not-allowed', 'Edit username is not allowed', { method: 'insertOrUpdateUser', action: 'Update_user', }); } - if (userData.statusText && !settings.get('Accounts_AllowUserStatusMessageChange') && (!canEditOtherUserInfo || editingMyself)) { + if (isEditingField(user.statusText, userData.statusText) && !settings.get('Accounts_AllowUserStatusMessageChange') && (!canEditOtherUserInfo || editingMyself)) { throw new Meteor.Error('error-action-not-allowed', 'Edit user status is not allowed', { method: 'insertOrUpdateUser', action: 'Update_user', }); } - if (userData.name && !settings.get('Accounts_AllowRealNameChange') && (!canEditOtherUserInfo || editingMyself)) { + if (isEditingField(user.name, userData.name) && !settings.get('Accounts_AllowRealNameChange') && (!canEditOtherUserInfo || editingMyself)) { throw new Meteor.Error('error-action-not-allowed', 'Edit user real name is not allowed', { method: 'insertOrUpdateUser', action: 'Update_user', }); } - if (userData.email && !settings.get('Accounts_AllowEmailChange') && (!canEditOtherUserInfo || editingMyself)) { + if (user.emails[0] && isEditingField(user.emails[0].address, userData.email) && !settings.get('Accounts_AllowEmailChange') && (!canEditOtherUserInfo || editingMyself)) { throw new Meteor.Error('error-action-not-allowed', 'Edit user email is not allowed', { method: 'insertOrUpdateUser', action: 'Update_user', @@ -233,77 +238,85 @@ const handleNickname = (updateUser, nickname) => { } }; -export const saveUser = function(userId, userData) { - validateUserData(userId, userData); - let sendPassword = false; +const saveNewUser = function(userData, sendPassword) { + validateEmailDomain(userData.email); - if (userData.hasOwnProperty('setRandomPassword')) { - if (userData.setRandomPassword) { - userData.password = passwordPolicy.generatePassword(); - userData.requirePasswordChange = true; - sendPassword = true; - } + const roles = userData.roles || getNewUserRoles(); + const isGuest = roles && roles.length === 1 && roles.includes('guest'); - delete userData.setRandomPassword; + // insert user + const createUser = { + username: userData.username, + password: userData.password, + joinDefaultChannels: userData.joinDefaultChannels, + isGuest, + }; + if (userData.email) { + createUser.email = userData.email; } - if (!userData._id) { - validateEmailDomain(userData.email); + const _id = Accounts.createUser(createUser); - // insert user - const createUser = { - username: userData.username, - password: userData.password, - joinDefaultChannels: userData.joinDefaultChannels, - }; - if (userData.email) { - createUser.email = userData.email; - } + const updateUser = { + $set: { + roles, + ...typeof userData.name !== 'undefined' && { name: userData.name }, + settings: userData.settings || {}, + }, + }; - const _id = Accounts.createUser(createUser); + if (typeof userData.requirePasswordChange !== 'undefined') { + updateUser.$set.requirePasswordChange = userData.requirePasswordChange; + } - const updateUser = { - $set: { - roles: userData.roles || getNewUserRoles(), - ...typeof userData.name !== 'undefined' && { name: userData.name }, - settings: userData.settings || {}, - }, - }; + if (typeof userData.verified === 'boolean') { + updateUser.$set['emails.0.verified'] = userData.verified; + } - if (typeof userData.requirePasswordChange !== 'undefined') { - updateUser.$set.requirePasswordChange = userData.requirePasswordChange; - } + handleBio(updateUser, userData.bio); + handleNickname(updateUser, userData.nickname); - if (typeof userData.verified === 'boolean') { - updateUser.$set['emails.0.verified'] = userData.verified; - } + Meteor.users.update({ _id }, updateUser); - handleBio(updateUser, userData.bio); - handleNickname(updateUser, userData.nickname); + if (userData.sendWelcomeEmail) { + _sendUserEmail(settings.get('Accounts_UserAddedEmail_Subject'), html, userData); + } - Meteor.users.update({ _id }, updateUser); + if (sendPassword) { + _sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData); + } - if (userData.sendWelcomeEmail) { - _sendUserEmail(settings.get('Accounts_UserAddedEmail_Subject'), html, userData); - } + userData._id = _id; + + if (settings.get('Accounts_SetDefaultAvatar') === true && userData.email) { + const gravatarUrl = Gravatar.imageUrl(userData.email, { default: '404', size: 200, secure: true }); - if (sendPassword) { - _sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData); + try { + setUserAvatar(userData, gravatarUrl, '', 'url'); + } catch (e) { + // Ignore this error for now, as it not being successful isn't bad } + } - userData._id = _id; + return _id; +}; - if (settings.get('Accounts_SetDefaultAvatar') === true && userData.email) { - const gravatarUrl = Gravatar.imageUrl(userData.email, { default: '404', size: 200, secure: true }); +export const saveUser = function(userId, userData) { + validateUserData(userId, userData); + let sendPassword = false; - try { - setUserAvatar(userData, gravatarUrl, '', 'url'); - } catch (e) { - // Ignore this error for now, as it not being successful isn't bad - } + if (userData.hasOwnProperty('setRandomPassword')) { + if (userData.setRandomPassword) { + userData.password = passwordPolicy.generatePassword(); + userData.requirePasswordChange = true; + sendPassword = true; } - return _id; + delete userData.setRandomPassword; + } + + if (!userData._id) { + return saveNewUser(userData, sendPassword); } validateUserEditing(userId, userData); @@ -362,6 +375,8 @@ export const saveUser = function(userId, userData) { Meteor.users.update({ _id: userData._id }, updateUser); + callbacks.run('afterSaveUser', userData); + if (sendPassword) { _sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData); } diff --git a/app/lib/server/functions/sendMessage.js b/app/lib/server/functions/sendMessage.js index a8b38a0b6f8fe..c8c2420e928ee 100644 --- a/app/lib/server/functions/sendMessage.js +++ b/app/lib/server/functions/sendMessage.js @@ -8,6 +8,7 @@ import { Apps } from '../../../apps/server'; import { isURL, isRelativeURL } from '../../../utils/lib/isURL'; import { FileUpload } from '../../../file-upload/server'; import { hasPermission } from '../../../authorization/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { parseUrlsInMessage } from './parseUrlsInMessage'; const { DISABLE_MESSAGE_PARSER = 'false' } = process.env; @@ -197,7 +198,7 @@ export const sendMessage = function(user, message, room, upsert = false) { const prevent = Promise.await(Apps.getBridges().getListenerBridge().messageEvent('IPreMessageSentPrevent', message)); if (prevent) { if (settings.get('Apps_Framework_Development_Mode')) { - console.log('A Rocket.Chat App prevented the message sending.', message); + SystemLogger.info({ msg: 'A Rocket.Chat App prevented the message sending.', message }); } return; @@ -223,7 +224,7 @@ export const sendMessage = function(user, message, room, upsert = false) { message.md = parser(message.msg); } } catch (e) { - console.log(e); // errors logged while the parser is at experimental stage + SystemLogger.error(e); // errors logged while the parser is at experimental stage } if (message) { if (message._id && upsert) { diff --git a/app/lib/server/functions/setRoomAvatar.js b/app/lib/server/functions/setRoomAvatar.js index 9b0ea487c7583..540de27992019 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 28d3111d0d2f9..77a137695fdda 100644 --- a/app/lib/server/functions/setUserActiveStatus.js +++ b/app/lib/server/functions/setUserActiveStatus.js @@ -5,6 +5,7 @@ import { Accounts } from 'meteor/accounts-base'; import * as Mailer from '../../../mailer'; import { Users, Subscriptions, Rooms } from '../../../models'; import { settings } from '../../../settings'; +import { callbacks } from '../../../callbacks/server'; import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; import { closeOmnichannelConversations } from './closeOmnichannelConversations'; import { shouldRemoveOrChangeOwner, getSubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner'; @@ -39,8 +40,18 @@ export function setUserActiveStatus(userId, active, confirmRelinquish = false) { return false; } + // Users without username can't do anything, so there is no need to check for owned rooms if (user.username != null && !active) { + const userAdmin = Users.findOneAdmin(userId); + const adminsCount = Users.findActiveUsersInRoles(['admin']).count(); + if (userAdmin && adminsCount === 1) { + throw new Meteor.Error('error-action-not-allowed', 'Leaving the app without an active admin is not allowed', { + method: 'removeUserFromRole', + action: 'Remove_last_admin', + }); + } + const subscribedRooms = getSubscribedRoomsForUserWithDetails(userId); // give omnichannel rooms a special treatment :) const chatSubscribedRooms = subscribedRooms.filter(({ t }) => t !== 'l'); @@ -52,11 +63,23 @@ export function setUserActiveStatus(userId, active, confirmRelinquish = false) { } closeOmnichannelConversations(user, livechatSubscribedRooms); - relinquishRoomOwnerships(user, chatSubscribedRooms, false); + Promise.await(relinquishRoomOwnerships(user, chatSubscribedRooms, false)); + } + + if (active && !user.active) { + callbacks.run('beforeActivateUser', user); } Users.setUserActive(userId, active); + if (active && !user.active) { + callbacks.run('afterActivateUser', user); + } + + if (!active && user.active) { + callbacks.run('afterDeactivateUser', user); + } + if (user.username) { Subscriptions.setArchivedByUsername(user.username, !active); } diff --git a/app/lib/server/functions/setUserAvatar.js b/app/lib/server/functions/setUserAvatar.js index ecc6e88e895f2..8fa18240cf185 100644 --- a/app/lib/server/functions/setUserAvatar.js +++ b/app/lib/server/functions/setUserAvatar.js @@ -1,12 +1,13 @@ import { Meteor } from 'meteor/meteor'; import { HTTP } from 'meteor/http'; -import { RocketChatFile } from '../../../file'; -import { FileUpload } from '../../../file-upload'; -import { Users } from '../../../models'; +import { RocketChatFile } from '../../../file/server'; +import { FileUpload } from '../../../file-upload/server'; +import { Users } from '../../../models/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { api } from '../../../../server/sdk/api'; -export const setUserAvatar = function(user, dataURI, contentType, service) { +export const setUserAvatar = function(user, dataURI, contentType, service, etag = null) { let encoding; let image; @@ -18,23 +19,23 @@ export const setUserAvatar = function(user, dataURI, contentType, service) { try { result = HTTP.get(dataURI, { npmRequestOptions: { encoding: 'binary', rejectUnauthorized: false } }); if (!result) { - console.log(`Not a valid response, from the avatar url: ${ encodeURI(dataURI) }`); + SystemLogger.info(`Not a valid response, from the avatar url: ${ encodeURI(dataURI) }`); throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${ encodeURI(dataURI) }`, { function: 'setUserAvatar', url: dataURI }); } } catch (error) { if (!error.response || error.response.statusCode !== 404) { - console.log(`Error while handling the setting of the avatar from a url (${ encodeURI(dataURI) }) for ${ user.username }:`, error); + SystemLogger.info(`Error while handling the setting of the avatar from a url (${ encodeURI(dataURI) }) for ${ user.username }:`, error); throw new Meteor.Error('error-avatar-url-handling', `Error while handling avatar setting from a URL (${ encodeURI(dataURI) }) for ${ user.username }`, { function: 'RocketChat.setUserAvatar', url: dataURI, username: user.username }); } } if (result.statusCode !== 200) { - console.log(`Not a valid response, ${ result.statusCode }, from the avatar url: ${ dataURI }`); + SystemLogger.info(`Not a valid response, ${ result.statusCode }, from the avatar url: ${ dataURI }`); throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${ dataURI }`, { function: 'setUserAvatar', url: dataURI }); } if (!/image\/.+/.test(result.headers['content-type'])) { - console.log(`Not a valid content-type from the provided url, ${ result.headers['content-type'] }, from the avatar url: ${ dataURI }`); + SystemLogger.info(`Not a valid content-type from the provided url, ${ result.headers['content-type'] }, from the avatar url: ${ dataURI }`); throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${ dataURI }`, { function: 'setUserAvatar', url: dataURI }); } @@ -63,8 +64,8 @@ export const setUserAvatar = function(user, dataURI, contentType, service) { fileStore.insert(file, buffer, (err, result) => { Meteor.setTimeout(function() { - Users.setAvatarData(user._id, service, result.etag); - api.broadcast('user.avatarUpdate', { username: user.username, avatarETag: result.etag }); + Users.setAvatarData(user._id, service, etag || result.etag); + api.broadcast('user.avatarUpdate', { username: user.username, avatarETag: etag || result.etag }); }, 500); }); }; diff --git a/app/lib/server/functions/setUsername.js b/app/lib/server/functions/setUsername.js index 832db3defcf89..97a05291f5d1b 100644 --- a/app/lib/server/functions/setUsername.js +++ b/app/lib/server/functions/setUsername.js @@ -3,12 +3,14 @@ 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'; import { api } from '../../../../server/sdk/api'; import { checkUsernameAvailability, setUserAvatar, getAvatarSuggestionForUser } from '.'; +import { SystemLogger } from '../../../../server/lib/logger/system'; export const _setUsername = function(userId, u, fullUser) { const username = s.trim(u); @@ -44,7 +46,7 @@ export const _setUsername = function(userId, u, fullUser) { }); } } catch (e) { - console.error(e); + SystemLogger.error(e); } // Set new username* Users.setUsername(user._id, username); @@ -69,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/functions/updateMessage.js b/app/lib/server/functions/updateMessage.js index eaa60b0318b5d..bf68daf712d64 100644 --- a/app/lib/server/functions/updateMessage.js +++ b/app/lib/server/functions/updateMessage.js @@ -1,9 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { parser } from '@rocket.chat/message-parser'; -import { Messages, Rooms } from '../../../models'; -import { settings } from '../../../settings'; -import { callbacks } from '../../../callbacks'; +import { Messages, Rooms } from '../../../models/server'; +import { settings } from '../../../settings/server'; +import { callbacks } from '../../../callbacks/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { Apps } from '../../../apps/server'; import { parseUrlsInMessage } from './parseUrlsInMessage'; @@ -52,7 +53,7 @@ export const updateMessage = function(message, user, originalMessage) { message.md = parser(message.msg); } } catch (e) { - console.log(e); // errors logged while the parser is at experimental stage + SystemLogger.error(e); // errors logged while the parser is at experimental stage } const tempid = message._id; diff --git a/app/lib/server/index.js b/app/lib/server/index.js index 5a89c8e44a35b..0a0e59bc49b6c 100644 --- a/app/lib/server/index.js +++ b/app/lib/server/index.js @@ -7,10 +7,8 @@ import './startup/settingsOnLoadCdnPrefix'; import './startup/settingsOnLoadDirectReply'; import './startup/settingsOnLoadSMTP'; import '../lib/MessageTypes'; -import '../startup'; import '../startup/defaultRoomTypes'; import './lib/bugsnag'; -import './lib/configLogger'; import './lib/debug'; import './lib/loginErrorMessageOverride'; import './oauth/oauth'; @@ -36,10 +34,8 @@ import './methods/filterATAllTag'; import './methods/filterATHereTag'; import './methods/filterBadWords'; import './methods/getChannelHistory'; -import './methods/getFullUserData'; import './methods/getRoomJoinCode'; import './methods/getRoomRoles'; -import './methods/getServerInfo'; import './methods/getSingleMessage'; import './methods/getMessages'; import './methods/getSlashCommandPreviews'; diff --git a/app/lib/server/lib/bugsnag.js b/app/lib/server/lib/bugsnag.js deleted file mode 100644 index c707d7e2ca768..0000000000000 --- a/app/lib/server/lib/bugsnag.js +++ /dev/null @@ -1,35 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import bugsnag from 'bugsnag'; - -import { settings } from '../../../settings'; -import { Info } from '../../../utils'; - -settings.get('Bugsnag_api_key', (key, value) => { - if (value) { - bugsnag.register(value); - } -}); - -const notify = function(message, stack) { - if (typeof stack === 'string') { - message += ` ${ stack }`; - } - let options = {}; - if (Info) { - options = { app: { version: Info.version, info: Info } }; - } - const error = new Error(message); - error.stack = stack; - bugsnag.notify(error, options); -}; - -process.on('uncaughtException', Meteor.bindEnvironment((error) => { - notify(error.message, error.stack); - throw error; -})); - -const originalMeteorDebug = Meteor._debug; -Meteor._debug = function(...args) { - notify(...args); - return originalMeteorDebug(...args); -}; diff --git a/app/lib/server/lib/bugsnag.ts b/app/lib/server/lib/bugsnag.ts new file mode 100644 index 0000000000000..ce55ec3a65162 --- /dev/null +++ b/app/lib/server/lib/bugsnag.ts @@ -0,0 +1,43 @@ +import { Meteor } from 'meteor/meteor'; +import Bugsnag from '@bugsnag/js'; + +import { settings } from '../../../settings/server'; +import { Info } from '../../../utils/server'; +import { Logger } from '../../../logger/server'; + +const logger = new Logger('bugsnag'); + +const originalMeteorDebug = Meteor._debug; + +function _bugsnagDebug(message: any, stack: any, ...args: any): void { + if (stack instanceof Error) { + Bugsnag.notify(stack, (event) => { + event.context = message; + }); + } else { + if (typeof stack === 'string') { + message += ` ${ stack }`; + } + + const error = new Error(message); + error.stack = stack; + Bugsnag.notify(error); + } + + return originalMeteorDebug(message, stack, ...args); +} + +settings.watch('Bugsnag_api_key', (value) => { + if (!value) { + return; + } + + Bugsnag.start({ + apiKey: value as string, + appVersion: Info.version, + logger, + metadata: Info, + }); + + Meteor._debug = _bugsnagDebug; +}); diff --git a/app/lib/server/lib/configLogger.js b/app/lib/server/lib/configLogger.js deleted file mode 100644 index 3039cc6285989..0000000000000 --- a/app/lib/server/lib/configLogger.js +++ /dev/null @@ -1,19 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { LoggerManager } from '../../../logger'; -import { settings } from '../../../settings'; - -settings.get('Log_Package', function(key, value) { - LoggerManager.showPackage = value; -}); - -settings.get('Log_File', function(key, value) { - LoggerManager.showFileAndLine = value; -}); - -settings.get('Log_Level', function(key, value) { - if (value != null) { - LoggerManager.logLevel = parseInt(value); - Meteor.setTimeout(() => LoggerManager.enable(true), 200); - } -}); diff --git a/app/lib/server/lib/debug.js b/app/lib/server/lib/debug.js index 0fb22935c534c..9a64460371909 100644 --- a/app/lib/server/lib/debug.js +++ b/app/lib/server/lib/debug.js @@ -3,37 +3,22 @@ import { WebApp } from 'meteor/webapp'; import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; import _ from 'underscore'; -import { settings } from '../../../settings'; -import { metrics } from '../../../metrics'; -import { Logger } from '../../../logger'; - -const logger = new Logger('Meteor', { - methods: { - method: { - type: 'info', - }, - publish: { - type: 'debug', - }, - }, -}); - -const { - LOG_METHOD_PAYLOAD = 'false', - LOG_REST_METHOD_PAYLOADS = 'false', -} = process.env; +import { settings } from '../../../settings/server'; +import { metrics } from '../../../metrics/server'; +import { Logger } from '../../../../server/lib/logger/Logger'; +import { getMethodArgs } from '../../../../server/lib/logger/logPayloads'; -const addPayloadToLog = LOG_METHOD_PAYLOAD !== 'false' || LOG_REST_METHOD_PAYLOADS !== 'false'; +const logger = new Logger('Meteor'); let Log_Trace_Methods; let Log_Trace_Subscriptions; -settings.get('Log_Trace_Methods', (key, value) => { Log_Trace_Methods = value; }); -settings.get('Log_Trace_Subscriptions', (key, value) => { Log_Trace_Subscriptions = value; }); +settings.watch('Log_Trace_Methods', (value) => { Log_Trace_Methods = value; }); +settings.watch('Log_Trace_Subscriptions', (value) => { Log_Trace_Subscriptions = value; }); let Log_Trace_Methods_Filter; let Log_Trace_Subscriptions_Filter; -settings.get('Log_Trace_Methods_Filter', (key, value) => { Log_Trace_Methods_Filter = value ? new RegExp(value) : undefined; }); -settings.get('Log_Trace_Subscriptions_Filter', (key, value) => { Log_Trace_Subscriptions_Filter = value ? new RegExp(value) : undefined; }); +settings.watch('Log_Trace_Methods_Filter', (value) => { Log_Trace_Methods_Filter = value ? new RegExp(value) : undefined; }); +settings.watch('Log_Trace_Subscriptions_Filter', (value) => { Log_Trace_Subscriptions_Filter = value ? new RegExp(value) : undefined; }); const traceConnection = (enable, filter, prefix, name, connection, userId) => { if (!enable) { @@ -56,20 +41,6 @@ const traceConnection = (enable, filter, prefix, name, connection, userId) => { } }; -const omitKeyArgs = (args, name) => { - if (name === 'saveSettings') { - return [args[0].map((arg) => _.omit(arg, 'value'))]; - } - - if (name === 'saveSetting') { - return [args[0], args[2]]; - } - - return args.map((arg) => (typeof arg !== 'object' - ? arg - : _.omit(arg, 'password', 'msg', 'pass', 'username', 'message'))); -}; - const wrapMethods = function(name, originalHandler, methodsMap) { methodsMap[name] = function(...originalArgs) { traceConnection(Log_Trace_Methods, Log_Trace_Methods_Filter, 'method', name, this.connection, this.userId); @@ -81,11 +52,16 @@ const wrapMethods = function(name, originalHandler, methodsMap) { has_connection: this.connection != null, has_user: this.userId != null, }); - const args = name === 'ufsWrite' ? Array.prototype.slice.call(originalArgs, 1) : originalArgs; - const dateTime = new Date().toISOString(); - const userId = Meteor.userId(); - logger.method(() => `${ this.connection?.clientAddress } - ${ userId } [${ dateTime }] "METHOD ${ method }" - "${ this.connection?.httpHeaders.referer }" "${ this.connection?.httpHeaders['user-agent'] }" | ${ addPayloadToLog ? JSON.stringify(omitKeyArgs(args, name)) : '' }`); + logger.method({ + method, + userId: Meteor.userId(), + userAgent: this.connection?.httpHeaders['user-agent'], + referer: this.connection?.httpHeaders.referer, + remoteIP: this.connection?.clientAddress, + instanceId: InstanceStatus.id(), + ...getMethodArgs(name, originalArgs), + }); const result = originalHandler.apply(this, originalArgs); end(); @@ -107,7 +83,16 @@ const originalMeteorPublish = Meteor.publish; Meteor.publish = function(name, func) { return originalMeteorPublish(name, function(...args) { traceConnection(Log_Trace_Subscriptions, Log_Trace_Subscriptions_Filter, 'subscription', name, this.connection, this.userId); - logger.publish(() => `${ name } -> userId: ${ this.userId }, arguments: ${ JSON.stringify(omitKeyArgs(args)) }`); + + logger.subscription({ + publication: name, + userId: this.userId, + userAgent: this.connection?.httpHeaders['user-agent'], + referer: this.connection?.httpHeaders.referer, + remoteIP: this.connection?.clientAddress, + instanceId: InstanceStatus.id(), + }); + const end = metrics.meteorSubscriptions.startTimer({ subscription: name }); const originalReady = this.ready; diff --git a/app/lib/server/lib/deprecationWarningLogger.ts b/app/lib/server/lib/deprecationWarningLogger.ts new file mode 100644 index 0000000000000..f3cc3149661b4 --- /dev/null +++ b/app/lib/server/lib/deprecationWarningLogger.ts @@ -0,0 +1,7 @@ +import { Logger } from '../../../logger/server'; + +const deprecationLogger = new Logger('DeprecationWarning'); + +export const apiDeprecationLogger = deprecationLogger.section('API'); +export const methodDeprecationLogger = deprecationLogger.section('METHOD'); +export const functionDeprecationLogger = deprecationLogger.section('FUNCTION'); diff --git a/app/lib/server/lib/getHiddenSystemMessages.ts b/app/lib/server/lib/getHiddenSystemMessages.ts index 0d734dacfc545..592fe5dd7e668 100644 --- a/app/lib/server/lib/getHiddenSystemMessages.ts +++ b/app/lib/server/lib/getHiddenSystemMessages.ts @@ -3,7 +3,7 @@ import { IRoom } from '../../../../definition/IRoom'; const hideMessagesOfTypeServer = new Set(); -settings.get('Hide_System_Messages', function(_key, values) { +settings.watch('Hide_System_Messages', function(values) { if (!values || !Array.isArray(values)) { return; } diff --git a/app/lib/server/lib/getRoomRoles.js b/app/lib/server/lib/getRoomRoles.js index 9c3718628782a..6ed6527c53686 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/lib/index.js b/app/lib/server/lib/index.js index 958e8b04d095b..f003ebf15fceb 100644 --- a/app/lib/server/lib/index.js +++ b/app/lib/server/lib/index.js @@ -9,7 +9,7 @@ import './notifyUsersOnMessage'; import './meteorFixes'; export { sendNotification } from './sendNotificationsOnMessage'; -export { hostname } from '../../lib/startup/settingsOnLoadSiteUrl'; +export { hostname } from '../startup/settingsOnLoadSiteUrl'; export { passwordPolicy } from './passwordPolicy'; export { validateEmailDomain } from './validateEmailDomain'; export { RateLimiterClass as RateLimiter } from './RateLimiter'; diff --git a/app/lib/server/lib/interceptDirectReplyEmails.js b/app/lib/server/lib/interceptDirectReplyEmails.js index ed7f022b53137..5447f80f679a1 100644 --- a/app/lib/server/lib/interceptDirectReplyEmails.js +++ b/app/lib/server/lib/interceptDirectReplyEmails.js @@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor'; import POP3Lib from 'poplib'; import { simpleParser } from 'mailparser'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { IMAPInterceptor } from '../../../../server/email/IMAPInterceptor'; import { processDirectEmail } from '.'; @@ -45,7 +46,7 @@ export class POP3Intercepter { // run on start this.pop3.list(); } else { - console.log('Unable to Log-in ....'); + SystemLogger.info('Unable to Log-in ....'); } })); @@ -61,7 +62,7 @@ export class POP3Intercepter { this.pop3.quit(); } } else { - console.log('Cannot Get Emails ....'); + SystemLogger.info('Cannot Get Emails ....'); } })); @@ -78,7 +79,7 @@ export class POP3Intercepter { // delete email this.pop3.dele(msgnumber); } else { - console.log('Cannot Retrieve Message ....'); + SystemLogger.info('Cannot Retrieve Message ....'); } })); @@ -93,18 +94,18 @@ export class POP3Intercepter { this.pop3.quit(); } } else { - console.log('Cannot Delete Message....'); + SystemLogger.info('Cannot Delete Message....'); } })); // invalid server state this.pop3.on('invalid-state', function(cmd) { - console.log(`Invalid state. You tried calling ${ cmd }`); + SystemLogger.info(`Invalid state. You tried calling ${ cmd }`); }); // locked => command already running, not finished yet this.pop3.on('locked', function(cmd) { - console.log(`Current command has not finished yet. You tried calling ${ cmd }`); + SystemLogger.info(`Current command has not finished yet. You tried calling ${ cmd }`); }); } diff --git a/app/lib/server/lib/passwordPolicy.js b/app/lib/server/lib/passwordPolicy.js index c426fbd4cc11e..2c0c2d9c2b307 100644 --- a/app/lib/server/lib/passwordPolicy.js +++ b/app/lib/server/lib/passwordPolicy.js @@ -1,14 +1,14 @@ import PasswordPolicy from './PasswordPolicyClass'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; export const passwordPolicy = new PasswordPolicy(); -settings.get('Accounts_Password_Policy_Enabled', (key, value) => { passwordPolicy.enabled = value; }); -settings.get('Accounts_Password_Policy_MinLength', (key, value) => { passwordPolicy.minLength = value; }); -settings.get('Accounts_Password_Policy_MaxLength', (key, value) => { passwordPolicy.maxLength = value; }); -settings.get('Accounts_Password_Policy_ForbidRepeatingCharacters', (key, value) => { passwordPolicy.forbidRepeatingCharacters = value; }); -settings.get('Accounts_Password_Policy_ForbidRepeatingCharactersCount', (key, value) => { passwordPolicy.forbidRepeatingCharactersCount = value; }); -settings.get('Accounts_Password_Policy_AtLeastOneLowercase', (key, value) => { passwordPolicy.mustContainAtLeastOneLowercase = value; }); -settings.get('Accounts_Password_Policy_AtLeastOneUppercase', (key, value) => { passwordPolicy.mustContainAtLeastOneUppercase = value; }); -settings.get('Accounts_Password_Policy_AtLeastOneNumber', (key, value) => { passwordPolicy.mustContainAtLeastOneNumber = value; }); -settings.get('Accounts_Password_Policy_AtLeastOneSpecialCharacter', (key, value) => { passwordPolicy.mustContainAtLeastOneSpecialCharacter = value; }); +settings.watch('Accounts_Password_Policy_Enabled', (value) => { passwordPolicy.enabled = value; }); +settings.watch('Accounts_Password_Policy_MinLength', (value) => { passwordPolicy.minLength = value; }); +settings.watch('Accounts_Password_Policy_MaxLength', (value) => { passwordPolicy.maxLength = value; }); +settings.watch('Accounts_Password_Policy_ForbidRepeatingCharacters', (value) => { passwordPolicy.forbidRepeatingCharacters = value; }); +settings.watch('Accounts_Password_Policy_ForbidRepeatingCharactersCount', (value) => { passwordPolicy.forbidRepeatingCharactersCount = value; }); +settings.watch('Accounts_Password_Policy_AtLeastOneLowercase', (value) => { passwordPolicy.mustContainAtLeastOneLowercase = value; }); +settings.watch('Accounts_Password_Policy_AtLeastOneUppercase', (value) => { passwordPolicy.mustContainAtLeastOneUppercase = value; }); +settings.watch('Accounts_Password_Policy_AtLeastOneNumber', (value) => { passwordPolicy.mustContainAtLeastOneNumber = value; }); +settings.watch('Accounts_Password_Policy_AtLeastOneSpecialCharacter', (value) => { passwordPolicy.mustContainAtLeastOneSpecialCharacter = value; }); diff --git a/app/lib/server/lib/processDirectEmail.js b/app/lib/server/lib/processDirectEmail.js index b00a042d476dd..98313697028f8 100644 --- a/app/lib/server/lib/processDirectEmail.js +++ b/app/lib/server/lib/processDirectEmail.js @@ -2,10 +2,11 @@ import { Meteor } from 'meteor/meteor'; import { EmailReplyParser as reply } from 'emailreplyparser'; import moment from 'moment'; -import { settings } from '../../../settings'; -import { Rooms, Messages, Users, Subscriptions } from '../../../models'; -import { metrics } from '../../../metrics'; -import { hasPermission } from '../../../authorization'; +import { settings } from '../../../settings/server'; +import { Rooms, Messages, Users, Subscriptions } from '../../../models/server'; +import { metrics } from '../../../metrics/server'; +import { canAccessRoom, hasPermission } from '../../../authorization/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { sendMessage as _sendMessage } from '../functions'; export const processDirectEmail = function(email) { @@ -54,29 +55,25 @@ export const processDirectEmail = function(email) { } message.rid = prevMessage.rid; - const room = Meteor.call('canAccessRoom', message.rid, user._id); - if (!room) { + const room = Rooms.findOneById(message.rid); + + if (!canAccessRoom(room, user)) { return false; } - const roomInfo = Rooms.findOneById(message.rid, { - t: 1, - name: 1, - }); - // check mention - if (message.msg.indexOf(`@${ prevMessage.u.username }`) === -1 && roomInfo.t !== 'd') { + if (message.msg.indexOf(`@${ prevMessage.u.username }`) === -1 && room.t !== 'd') { message.msg = `@${ prevMessage.u.username } ${ message.msg }`; } // reply message link let prevMessageLink = `[ ](${ Meteor.absoluteUrl().replace(/\/$/, '') }`; - if (roomInfo.t === 'c') { - prevMessageLink += `/channel/${ roomInfo.name }?msg=${ email.headers.mid }) `; - } else if (roomInfo.t === 'd') { + if (room.t === 'c') { + prevMessageLink += `/channel/${ room.name }?msg=${ email.headers.mid }) `; + } else if (room.t === 'd') { prevMessageLink += `/direct/${ prevMessage.u.username }?msg=${ email.headers.mid }) `; - } else if (roomInfo.t === 'p') { - prevMessageLink += `/group/${ roomInfo.name }?msg=${ email.headers.mid }) `; + } else if (room.t === 'p') { + prevMessageLink += `/group/${ room.name }?msg=${ email.headers.mid }) `; } // add reply message link message.msg = prevMessageLink + message.msg; @@ -126,6 +123,6 @@ export const processDirectEmail = function(email) { email.headers.mid = email.headers.to.split('@')[0].split('+')[1]; sendMessage(email); } else { - console.log('Invalid Email....If not. Please report it.'); + SystemLogger.error('Invalid Email....If not. Please report it.'); } }; diff --git a/app/lib/server/lib/sendNotificationsOnMessage.js b/app/lib/server/lib/sendNotificationsOnMessage.js index a5bb1d688b1cf..7ad193d3c7f60 100644 --- a/app/lib/server/lib/sendNotificationsOnMessage.js +++ b/app/lib/server/lib/sendNotificationsOnMessage.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import moment from 'moment'; import { hasPermission } from '../../../authorization'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { callbacks } from '../../../callbacks/server'; import { Subscriptions, Users } from '../../../models/server'; import { roomTypes } from '../../../utils'; @@ -10,7 +10,6 @@ import { callJoinRoom, messageContainsHighlight, parseMessageTextPerUser, replac import { getEmailData, shouldNotifyEmail } from '../functions/notifications/email'; import { getPushData, shouldNotifyMobile } from '../functions/notifications/mobile'; import { notifyDesktopUser, shouldNotifyDesktop } from '../functions/notifications/desktop'; -import { notifyAudioUser, shouldNotifyAudio } from '../functions/notifications/audio'; import { Notification } from '../../../notification-queue/server/NotificationQueue'; import { getMentions } from './notifyUsersOnMessage'; @@ -74,29 +73,11 @@ export const sendNotification = async ({ const isHighlighted = messageContainsHighlight(message, subscription.userHighlights); const { - audioNotifications, desktopNotifications, mobilePushNotifications, emailNotifications, } = subscription; - // busy users don't receive audio notification - if (shouldNotifyAudio({ - disableAllMessageNotifications, - status: receiver.status, - statusConnection: receiver.statusConnection, - audioNotifications, - hasMentionToAll, - hasMentionToHere, - isHighlighted, - hasMentionToUser, - hasReplyToThread, - roomType, - isThread, - })) { - notifyAudioUser(subscription.u._id, message, room); - } - // busy users don't receive desktop notification if (shouldNotifyDesktop({ disableAllMessageNotifications, @@ -191,7 +172,6 @@ export const sendNotification = async ({ const project = { $project: { - audioNotifications: 1, desktopNotifications: 1, emailNotifications: 1, mobilePushNotifications: 1, @@ -413,7 +393,7 @@ export async function sendAllNotifications(message, room) { return message; } -settings.get('Troubleshoot_Disable_Notifications', (key, value) => { +settings.watch('Troubleshoot_Disable_Notifications', (value) => { if (TroubleshootDisableNotifications === value) { return; } TroubleshootDisableNotifications = value; diff --git a/app/lib/server/lib/validateEmailDomain.js b/app/lib/server/lib/validateEmailDomain.js index 1bdac7ecf78c4..85df94bed533a 100644 --- a/app/lib/server/lib/validateEmailDomain.js +++ b/app/lib/server/lib/validateEmailDomain.js @@ -5,14 +5,13 @@ import { Meteor } from 'meteor/meteor'; import { emailDomainDefaultBlackList } from './defaultBlockedDomainsList'; import { settings } from '../../../settings/server'; +const dnsResolveMx = Meteor.wrapAsync(dns.resolveMx); const emailValidationRegex = /^[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])?)*$/; let emailDomainBlackList = []; let emailDomainWhiteList = []; -let useDefaultBlackList = false; -let useDNSDomainCheck = false; -settings.get('Accounts_BlockedDomainsList', function(key, value) { +settings.watch('Accounts_BlockedDomainsList', function(value) { if (!value) { emailDomainBlackList = []; return; @@ -20,7 +19,7 @@ settings.get('Accounts_BlockedDomainsList', function(key, value) { emailDomainBlackList = value.split(',').filter(Boolean).map((domain) => domain.trim()); }); -settings.get('Accounts_AllowedDomainsList', function(key, value) { +settings.watch('Accounts_AllowedDomainsList', function(value) { if (!value) { emailDomainWhiteList = []; return; @@ -28,12 +27,6 @@ settings.get('Accounts_AllowedDomainsList', function(key, value) { emailDomainWhiteList = value.split(',').filter(Boolean).map((domain) => domain.trim()); }); -settings.get('Accounts_UseDefaultBlockedDomainsList', function(key, value) { - useDefaultBlackList = value; -}); -settings.get('Accounts_UseDNSDomainCheck', function(key, value) { - useDNSDomainCheck = value; -}); export const validateEmailDomain = function(email) { if (!emailValidationRegex.test(email)) { @@ -45,13 +38,13 @@ export const validateEmailDomain = function(email) { if (emailDomainWhiteList.length && !emailDomainWhiteList.includes(emailDomain)) { throw new Meteor.Error('error-invalid-domain', 'The email domain is not in whitelist', { function: 'RocketChat.validateEmailDomain' }); } - if (emailDomainBlackList.length && (emailDomainBlackList.indexOf(emailDomain) !== -1 || (useDefaultBlackList && emailDomainDefaultBlackList.indexOf(emailDomain) !== -1))) { + if (emailDomainBlackList.length && (emailDomainBlackList.indexOf(emailDomain) !== -1 || (settings.get('Accounts_UseDefaultBlockedDomainsList') && emailDomainDefaultBlackList.indexOf(emailDomain) !== -1))) { throw new Meteor.Error('error-email-domain-blacklisted', 'The email domain is blacklisted', { function: 'RocketChat.validateEmailDomain' }); } - if (useDNSDomainCheck) { + if (settings.get('Accounts_UseDNSDomainCheck')) { try { - Meteor.wrapAsync(dns.resolveMx)(emailDomain); + dnsResolveMx(emailDomain); } catch (e) { throw new Meteor.Error('error-invalid-domain', 'Invalid domain', { function: 'RocketChat.validateEmailDomain' }); } diff --git a/app/lib/server/methods/deleteMessage.js b/app/lib/server/methods/deleteMessage.js index 086b9caee7f91..8be7da4e5e456 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 1ff7494a87516..2d7f269b08f75 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/filterBadWords.ts b/app/lib/server/methods/filterBadWords.ts index 57f89059fc7ab..b4544ad5d394c 100644 --- a/app/lib/server/methods/filterBadWords.ts +++ b/app/lib/server/methods/filterBadWords.ts @@ -8,7 +8,7 @@ import { IMessage } from '../../../../definition/IMessage'; const Dep = new Tracker.Dependency(); Meteor.startup(() => { - settings.get(/Message_AllowBadWordsFilter|Message_BadWordsFilterList|Message_BadWordsWhitelist/, () => { + settings.watchMultiple(['Message_AllowBadWordsFilter', 'Message_BadWordsFilterList', 'Message_BadWordsWhitelist'], () => { Dep.changed(); }); Tracker.autorun(() => { diff --git a/app/lib/server/methods/getChannelHistory.js b/app/lib/server/methods/getChannelHistory.js index 25c645231038b..80237842b118d 100644 --- a/app/lib/server/methods/getChannelHistory.js +++ b/app/lib/server/methods/getChannelHistory.js @@ -2,8 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import _ from 'underscore'; -import { hasPermission } from '../../../authorization/server'; -import { Subscriptions, Messages } from '../../../models/server'; +import { canAccessRoom, hasPermission } from '../../../authorization/server'; +import { Subscriptions, Messages, Rooms } from '../../../models/server'; import { settings } from '../../../settings/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { getHiddenSystemMessages } from '../lib/getHiddenSystemMessages'; @@ -17,11 +17,15 @@ Meteor.methods({ } const fromUserId = Meteor.userId(); - const room = Meteor.call('canAccessRoom', rid, fromUserId); + const room = Rooms.findOneById(rid); if (!room) { return false; } + if (!canAccessRoom(room, { _id: fromUserId })) { + return false; + } + // Make sure they can access the room if (room.t === 'c' && !hasPermission(fromUserId, 'preview-c-room') && !Subscriptions.findOneByRoomIdAndUserId(rid, fromUserId, { fields: { _id: 1 } })) { return false; diff --git a/app/lib/server/methods/getFullUserData.js b/app/lib/server/methods/getFullUserData.js deleted file mode 100644 index 3c551dacd1ee5..0000000000000 --- a/app/lib/server/methods/getFullUserData.js +++ /dev/null @@ -1,17 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { getFullUserData } from '../functions'; - -Meteor.methods({ - getFullUserData({ filter = '', username = '', limit = 1 }) { - console.warn('Method "getFullUserData" is deprecated and will be removed after v4.0.0'); - - if (!Meteor.userId()) { - throw new Meteor.Error('not-authorized'); - } - - const result = getFullUserData({ userId: Meteor.userId(), filter: filter || username, limit }); - - return result && result.fetch(); - }, -}); diff --git a/app/lib/server/methods/getMessages.js b/app/lib/server/methods/getMessages.js index d4b3ce4bd20a4..f8ccbbbb6f740 100644 --- a/app/lib/server/methods/getMessages.js +++ b/app/lib/server/methods/getMessages.js @@ -1,28 +1,22 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { Messages } from '../../../models'; +import { canAccessRoom } from '../../../authorization/server'; +import { Messages } from '../../../models/server'; Meteor.methods({ getMessages(messages) { check(messages, [String]); - const cache = {}; + const msgs = Messages.findVisibleByIds(messages).fetch(); - return messages.map((msgId) => { - const msg = Messages.findOneById(msgId); + const user = { _id: Meteor.userId() }; - if (!msg || !msg.rid) { - return undefined; - } + const rids = [...new Set(msgs.map((m) => m.rid))]; + if (!rids.every((_id) => canAccessRoom({ _id }, user))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getSingleMessage' }); + } - cache[msg.rid] = cache[msg.rid] || Meteor.call('canAccessRoom', msg.rid, Meteor.userId()); - - if (!cache[msg.rid]) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getSingleMessage' }); - } - - return msg; - }); + return msgs; }, }); diff --git a/app/lib/server/methods/getServerInfo.js b/app/lib/server/methods/getServerInfo.js deleted file mode 100644 index 4445eaf36f358..0000000000000 --- a/app/lib/server/methods/getServerInfo.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Info } from '../../../utils'; - -Meteor.methods({ - getServerInfo() { - if (!Meteor.userId()) { - console.warning('Method "getServerInfo" is deprecated and will be removed after v4.0.0'); - throw new Meteor.Error('not-authorized'); - } - - return Info; - }, -}); diff --git a/app/lib/server/methods/getSingleMessage.js b/app/lib/server/methods/getSingleMessage.js index 399aa335cab33..604ac2f1b4f70 100644 --- a/app/lib/server/methods/getSingleMessage.js +++ b/app/lib/server/methods/getSingleMessage.js @@ -1,7 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { Messages } from '../../../models'; +import { canAccessRoom } from '../../../authorization/server'; +import { Messages } from '../../../models/server'; Meteor.methods({ getSingleMessage(msgId) { @@ -13,7 +14,7 @@ Meteor.methods({ return undefined; } - if (!Meteor.call('canAccessRoom', msg.rid, Meteor.userId())) { + if (!canAccessRoom({ _id: msg.rid }, { _id: Meteor.userId() })) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getSingleMessage' }); } diff --git a/app/lib/server/methods/leaveRoom.js b/app/lib/server/methods/leaveRoom.js deleted file mode 100644 index 561d7bdb548ac..0000000000000 --- a/app/lib/server/methods/leaveRoom.js +++ /dev/null @@ -1,43 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; - -import { hasPermission, hasRole, getUsersInRole } from '../../../authorization'; -import { Subscriptions, Rooms } from '../../../models'; -import { removeUserFromRoom } from '../functions'; -import { roomTypes, RoomMemberActions } from '../../../utils/server'; - -Meteor.methods({ - leaveRoom(rid) { - check(rid, String); - - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'leaveRoom' }); - } - - const room = Rooms.findOneById(rid); - const user = Meteor.user(); - - if (!roomTypes.getConfig(room.t).allowMemberAction(room, RoomMemberActions.LEAVE)) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'leaveRoom' }); - } - - if ((room.t === 'c' && !hasPermission(user._id, 'leave-c')) || (room.t === 'p' && !hasPermission(user._id, 'leave-p'))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'leaveRoom' }); - } - - const subscription = Subscriptions.findOneByRoomIdAndUserId(rid, user._id, { fields: { _id: 1 } }); - if (!subscription) { - throw new Meteor.Error('error-user-not-in-room', 'You are not in this room', { method: 'leaveRoom' }); - } - - // 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(); - 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' }); - } - } - - return removeUserFromRoom(rid, user); - }, -}); diff --git a/app/lib/server/methods/leaveRoom.ts b/app/lib/server/methods/leaveRoom.ts new file mode 100644 index 0000000000000..cce12a25b8f84 --- /dev/null +++ b/app/lib/server/methods/leaveRoom.ts @@ -0,0 +1,45 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; + +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({ + async leaveRoom(rid) { + check(rid, String); + + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'leaveRoom' }); + } + + const room = Rooms.findOneById(rid); + const user = Meteor.user(); + + if (!user || !roomTypes.getConfig(room.t).allowMemberAction(room, RoomMemberActions.LEAVE)) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'leaveRoom' }); + } + + if ((room.t === 'c' && !hasPermission(user._id, 'leave-c')) || (room.t === 'p' && !hasPermission(user._id, 'leave-p'))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'leaveRoom' }); + } + + const subscription = Subscriptions.findOneByRoomIdAndUserId(rid, user._id, { fields: { _id: 1 } }); + if (!subscription) { + throw new Meteor.Error('error-user-not-in-room', 'You are not in this room', { method: 'leaveRoom' }); + } + + // If user is room owner, check if there are other owners. If there isn't anyone else, warn user to set a new owner. + if (hasRole(user._id, 'owner', room._id)) { + const cursor = await Roles.findUsersInRole('owner', room._id); + const numOwners = Promise.await(cursor.count()); + 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' }); + } + } + + return removeUserFromRoom(rid, user); + }, +}); diff --git a/app/lib/server/methods/refreshOAuthService.js b/app/lib/server/methods/refreshOAuthService.js deleted file mode 100644 index e0ef565cb45e1..0000000000000 --- a/app/lib/server/methods/refreshOAuthService.js +++ /dev/null @@ -1,21 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { ServiceConfiguration } from 'meteor/service-configuration'; - -import { hasPermission } from '../../../authorization'; -import { Settings } from '../../../models'; - -Meteor.methods({ - refreshOAuthService() { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'refreshOAuthService' }); - } - - if (hasPermission(Meteor.userId(), 'add-oauth-service') !== true) { - throw new Meteor.Error('error-action-not-allowed', 'Refresh OAuth Services is not allowed', { method: 'refreshOAuthService', action: 'Refreshing_OAuth_Services' }); - } - - ServiceConfiguration.configurations.remove({}); - - Settings.update({ _id: /^(Accounts_OAuth_|SAML_|CAS_|Blockstack_).+/ }, { $set: { _updatedAt: new Date() } }, { multi: true }); - }, -}); diff --git a/app/lib/server/methods/refreshOAuthService.ts b/app/lib/server/methods/refreshOAuthService.ts new file mode 100644 index 0000000000000..14dbeaa1fcd6f --- /dev/null +++ b/app/lib/server/methods/refreshOAuthService.ts @@ -0,0 +1,21 @@ +import { Meteor } from 'meteor/meteor'; +import { ServiceConfiguration } from 'meteor/service-configuration'; + +import { hasPermission } from '../../../authorization/server'; +import { Settings } from '../../../models/server/raw'; + +Meteor.methods({ + async refreshOAuthService() { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'refreshOAuthService' }); + } + + if (hasPermission(Meteor.userId(), 'add-oauth-service') !== true) { + throw new Meteor.Error('error-action-not-allowed', 'Refresh OAuth Services is not allowed', { method: 'refreshOAuthService', action: 'Refreshing_OAuth_Services' }); + } + + ServiceConfiguration.configurations.remove({}); + + 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 ba6f6a4dd5a34..0000000000000 --- 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 '../../../settings'; - -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 0000000000000..4aecbcdc53df1 --- /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 e8270d35835c4..993915db53f61 100644 --- a/app/lib/server/methods/saveSetting.js +++ b/app/lib/server/methods/saveSetting.js @@ -3,12 +3,11 @@ import { Match, check } from 'meteor/check'; import { hasPermission, hasAllPermission } from '../../../authorization/server'; import { getSettingPermissionId } from '../../../authorization/lib'; -import { settings } from '../../../settings'; -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', { @@ -27,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) { @@ -45,7 +44,7 @@ Meteor.methods({ break; } - settings.updateById(_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 d46bca64aac99..6da862bd9aa55 100644 --- a/app/lib/server/methods/saveSettings.js +++ b/app/lib/server/methods/saveSettings.js @@ -1,14 +1,13 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { hasPermission } from '../../../authorization'; -import { settings } from '../../../settings'; -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) { @@ -19,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; @@ -45,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', { @@ -54,8 +53,8 @@ Meteor.methods({ }); } - params.forEach(({ _id, value, editor }) => settings.updateById(_id, value, editor)); + await Promise.all(params.map(({ _id, value, editor }) => Settings.updateValueById(_id, value, editor))); return true; - }), + }, {}), }); diff --git a/app/lib/server/methods/sendMessage.js b/app/lib/server/methods/sendMessage.js index 3d91d72022d08..871bd69a431f0 100644 --- a/app/lib/server/methods/sendMessage.js +++ b/app/lib/server/methods/sendMessage.js @@ -11,7 +11,7 @@ import { Users, Messages } from '../../../models'; import { sendMessage } from '../functions'; import { RateLimiter } from '../lib'; import { canSendMessage } from '../../../authorization/server'; -import { SystemLogger } from '../../../logger/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { api } from '../../../../server/sdk/api'; export function executeSendMessage(uid, message) { diff --git a/app/lib/server/oauth/twitter.js b/app/lib/server/oauth/twitter.js index 132af472a03d4..c3284ad249086 100644 --- a/app/lib/server/oauth/twitter.js +++ b/app/lib/server/oauth/twitter.js @@ -1,4 +1,3 @@ -import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import Twit from 'twit'; import _ from 'underscore'; @@ -22,9 +21,10 @@ const getIdentity = function(accessToken, appId, appSecret, accessTokenSecret) { access_token: accessToken, access_token_secret: accessTokenSecret, }); - const syncTwitter = Meteor.wrapAsync(Twitter.get, Twitter); try { - return syncTwitter('account/verify_credentials.json?include_email=true'); + const result = Promise.await(Twitter.get('account/verify_credentials.json?include_email=true')); + + return result.data; } catch (err) { throw _.extend(new Error(`Failed to fetch identity from Twwiter. ${ err.message }`), { response: err.response }); diff --git a/app/lib/server/startup/email.js b/app/lib/server/startup/email.js deleted file mode 100644 index 40cac50e9c8bf..0000000000000 --- a/app/lib/server/startup/email.js +++ /dev/null @@ -1,483 +0,0 @@ -import { settings } from '../../../settings'; - -settings.addGroup('Email', function() { - this.section('Style', function() { - this.add('email_plain_text_only', false, { - type: 'boolean', - }); - - this.add('email_style', `html, body, .body { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Helvetica Neue','Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Meiryo UI',Arial,sans-serif; } - - body, .body { - width: 100%; - height: 100%; - } - - a { - color: #1D74F5; - font-weight: bold; - text-decoration: none; - line-height: 1.8; - padding-left: 2px; - padding-right: 2px; - } - p { - margin: 1rem 0; - } - .btn { - text-decoration: none; - color: #FFF; - background-color: #1D74F5; - padding: 12px 18px; - font-weight: 500; - font-size: 14px; - margin-top: 8px; - text-align: center; - cursor: pointer; - display: inline-block; - border-radius: 2px; - } - - ol, ul, div { - list-style-position: inside; - padding: 16px 0 ; - } - li { - padding: 8px 0; - font-weight: 600; - } - .wrap { - width: 100%; - clear: both; - } - - h1,h2,h3,h4,h5,h6 { - line-height: 1.1; margin:0 0 16px 0; color: #000; - } - - h1 { font-weight: 100; font-size: 44px;} - h2 { font-weight: 600; font-size: 30px; color: #2F343D;} - h3 { font-weight: 100; font-size: 27px;} - h4 { font-weight: 500; font-size: 14px; color: #2F343D;} - h5 { font-weight: 500; font-size: 13px; line-height: 1.6; color: #2F343D} - h6 { font-weight: 500; font-size: 10px; color: #6c727A; line-height: 1.7;} - - .container { - display: block; - max-width: 640px; - margin: 0 auto; /* makes it centered */ - clear: both; - border-radius: 2px; - } - - .content { - padding: 36px; - } - - .header-content { - padding-top: 36px; - padding-bottom: 36px; - padding-left: 36px; - padding-right: 36px; - max-width: 640px; - margin: 0 auto; - display: block; - } - - .lead { - margin-bottom: 32px; - color: #2f343d; - line-height: 22px; - font-size: 14px; - } - - .advice { - height: 20px; - color: #9EA2A8; - font-size: 12px; - font-weight: normal; - margin-bottom: 0; - } - .social { - font-size: 12px - } - `, { - type: 'code', - code: 'css', - multiline: true, - i18nLabel: 'email_style_label', - i18nDescription: 'email_style_description', - enableQuery: { - _id: 'email_plain_text_only', - value: false, - }, - }); - }); - - this.section('Subject', function() { - this.add('Offline_DM_Email', '[[Site_Name]] You have been direct messaged by [User]', { - type: 'code', - code: 'text', - multiline: true, - i18nLabel: 'Offline_DM_Email', - i18nDescription: 'Offline_Email_Subject_Description', - }); - this.add('Offline_Mention_Email', '[[Site_Name]] You have been mentioned by [User] in #[Room]', { - type: 'code', - code: 'text', - multiline: true, - i18nLabel: 'Offline_Mention_Email', - i18nDescription: 'Offline_Email_Subject_Description', - }); - this.add('Offline_Mention_All_Email', '[User] has posted a message in #[Room]', { - type: 'code', - code: 'text', - multiline: true, - i18nLabel: 'Offline_Mention_All_Email', - i18nDescription: 'Offline_Email_Subject_Description', - }); - }); - this.section('Header_and_Footer', function() { - this.add('Email_Header', 'Rocket.Chat Cloud
Rocket.chat
', { - type: 'code', - code: 'text/html', - multiline: true, - i18nLabel: 'Header', - }); - this.add('Email_Footer', '
© Rocket.Chat Technologies Corp.
Made with ❤️ in 🇧🇷 🇨🇦 🇩🇪 🇮🇳 🇬🇧 🇺🇸
', { - type: 'code', - code: 'text/html', - multiline: true, - i18nLabel: 'Footer', - }); - this.add('Email_Footer_Direct_Reply', '

{Direct_Reply_Advice}

', { - type: 'code', - code: 'text/html', - multiline: true, - i18nLabel: 'Footer_Direct_Reply', - }); - }); - this.section('Direct_Reply', function() { - this.add('Direct_Reply_Enable', false, { - type: 'boolean', - env: true, - i18nLabel: 'Direct_Reply_Enable', - }); - this.add('Direct_Reply_Debug', false, { - type: 'boolean', - env: true, - i18nLabel: 'Direct_Reply_Debug', - i18nDescription: 'Direct_Reply_Debug_Description', - }); - this.add('Direct_Reply_Protocol', 'IMAP', { - type: 'select', - values: [ - { - key: 'IMAP', - i18nLabel: 'IMAP', - }, { - key: 'POP', - i18nLabel: 'POP', - }, - ], - env: true, - i18nLabel: 'Protocol', - }); - this.add('Direct_Reply_Host', '', { - type: 'string', - env: true, - i18nLabel: 'Host', - }); - this.add('Direct_Reply_Port', '', { - type: 'string', - env: true, - i18nLabel: 'Port', - }); - this.add('Direct_Reply_IgnoreTLS', false, { - type: 'boolean', - env: true, - i18nLabel: 'IgnoreTLS', - }); - this.add('Direct_Reply_Frequency', 5, { - type: 'int', - env: true, - i18nLabel: 'Direct_Reply_Frequency', - enableQuery: { - _id: 'Direct_Reply_Protocol', - value: 'POP', - }, - }); - this.add('Direct_Reply_Delete', true, { - type: 'boolean', - env: true, - i18nLabel: 'Direct_Reply_Delete', - enableQuery: { - _id: 'Direct_Reply_Protocol', - value: 'IMAP', - }, - }); - this.add('Direct_Reply_Separator', '+', { - type: 'select', - values: [ - { - key: '!', - i18nLabel: '!', - }, { - key: '#', - i18nLabel: '#', - }, { - key: '$', - i18nLabel: '$', - }, { - key: '%', - i18nLabel: '%', - }, { - key: '&', - i18nLabel: '&', - }, { - key: '\'', - i18nLabel: '\'', - }, { - key: '*', - i18nLabel: '*', - }, { - key: '+', - i18nLabel: '+', - }, { - key: '-', - i18nLabel: '-', - }, { - key: '/', - i18nLabel: '/', - }, { - key: '=', - i18nLabel: '=', - }, { - key: '?', - i18nLabel: '?', - }, { - key: '^', - i18nLabel: '^', - }, { - key: '_', - i18nLabel: '_', - }, { - key: '`', - i18nLabel: '`', - }, { - key: '{', - i18nLabel: '{', - }, { - key: '|', - i18nLabel: '|', - }, { - key: '}', - i18nLabel: '}', - }, { - key: '~', - i18nLabel: '~', - }, - ], - env: true, - i18nLabel: 'Direct_Reply_Separator', - }); - this.add('Direct_Reply_Username', '', { - type: 'string', - env: true, - i18nLabel: 'Username', - placeholder: 'email@domain', - secret: true, - }); - this.add('Direct_Reply_ReplyTo', '', { - type: 'string', - env: true, - i18nLabel: 'ReplyTo', - placeholder: 'email@domain', - }); - return this.add('Direct_Reply_Password', '', { - type: 'password', - env: true, - i18nLabel: 'Password', - secret: true, - }); - }); - this.section('SMTP', function() { - this.add('SMTP_Protocol', 'smtp', { - type: 'select', - values: [ - { - key: 'smtp', - i18nLabel: 'smtp', - }, { - key: 'smtps', - i18nLabel: 'smtps', - }, - ], - env: true, - i18nLabel: 'Protocol', - }); - this.add('SMTP_Host', '', { - type: 'string', - env: true, - i18nLabel: 'Host', - }); - this.add('SMTP_Port', '', { - type: 'string', - env: true, - i18nLabel: 'Port', - }); - this.add('SMTP_IgnoreTLS', true, { - type: 'boolean', - env: true, - i18nLabel: 'IgnoreTLS', - enableQuery: { - _id: 'SMTP_Protocol', - value: 'smtp', - }, - }); - this.add('SMTP_Pool', true, { - type: 'boolean', - env: true, - i18nLabel: 'Pool', - }); - this.add('SMTP_Username', '', { - type: 'string', - env: true, - i18nLabel: 'Username', - autocomplete: false, - secret: true, - }); - this.add('SMTP_Password', '', { - type: 'password', - env: true, - i18nLabel: 'Password', - autocomplete: false, - secret: true, - }); - this.add('From_Email', '', { - type: 'string', - placeholder: 'email@domain', - }); - return this.add('SMTP_Test_Button', 'sendSMTPTestEmail', { - type: 'action', - actionText: 'Send_a_test_mail_to_my_user', - }); - }); - - this.section('Registration', function() { - this.add('Accounts_Enrollment_Email_Subject', '{Welcome_to Site_name}', { - type: 'string', - i18nLabel: 'Subject', - }); - this.add('Accounts_Enrollment_Email', '

{Welcome_to Site_Name}

{Visit_Site_Url_and_try_the_best_open_source_chat_solution_available_today}

{Login}', { - type: 'code', - code: 'text/html', - multiline: true, - i18nLabel: 'Body', - }); - }); - - this.section('Registration_via_Admin', function() { - this.add('Accounts_UserAddedEmail_Subject', '{Welcome_to Site_Name}', { - type: 'string', - i18nLabel: 'Subject', - }); - this.add('Accounts_UserAddedEmail_Email', '

{Welcome_to Site_Name}

{Visit_Site_Url_and_try_the_best_open_source_chat_solution_available_today}

{Login}', { - type: 'code', - code: 'text/html', - multiline: true, - i18nLabel: 'Body', - i18nDescription: 'Accounts_UserAddedEmail_Description', - }); - }); - - this.section('Verification', function() { - this.add('Verification_Email_Subject', '{Verification_Email_Subject}', { - type: 'string', - i18nLabel: 'Subject', - }); - - this.add('Verification_Email', '

{Hi_username}

{Verification_email_body}

{Verify_your_email}', { - type: 'code', - code: 'text/html', - multiline: true, - i18nLabel: 'Body', - i18nDescription: 'Verification_Description', - }); - }); - - this.section('Offline_Message', function() { - this.add('Offline_Message_Use_DeepLink', true, { - type: 'boolean', - }); - }); - - this.section('Invitation', function() { - this.add('Invitation_Subject', '{Invitation_Subject_Default}', { - type: 'string', - i18nLabel: 'Subject', - }); - this.add('Invitation_Email', '

{Welcome_to Site_Name}

{Visit_Site_Url_and_try_the_best_open_source_chat_solution_available_today}

{Join_Chat}', { - type: 'code', - code: 'text/html', - multiline: true, - i18nLabel: 'Body', - i18nDescription: 'Invitation_Email_Description', - }); - }); - - this.section('Forgot_password_section', function() { - this.add('Forgot_Password_Email_Subject', '{Forgot_Password_Email_Subject}', { - type: 'string', - i18nLabel: 'Subject', - }); - - this.add('Forgot_Password_Email', '

{Forgot_password}

{Lets_get_you_new_one}

{Reset}

{If_you_didnt_ask_for_reset_ignore_this_email}

', { - type: 'code', - code: 'text/html', - multiline: true, - i18nLabel: 'Body', - i18nDescription: 'Forgot_Password_Description', - }); - }); - - this.section('Email_changed_section', function() { - this.add('Email_Changed_Email_Subject', '{Email_Changed_Email_Subject}', { - type: 'string', - i18nLabel: 'Subject', - }); - - this.add('Email_Changed_Email', '

{Hi},

{Your_email_address_has_changed}

{Your_new_email_is_email}

{Login}', { - type: 'code', - code: 'text/html', - multiline: true, - i18nLabel: 'Body', - i18nDescription: 'Email_Changed_Description', - }); - }); - - this.section('Password_changed_section', function() { - this.add('Password_Changed_Email_Subject', '{Password_Changed_Email_Subject}', { - type: 'string', - i18nLabel: 'Subject', - }); - - this.add('Password_Changed_Email', '

{Hi},

{Your_password_was_changed_by_an_admin}

{Your_temporary_password_is_password}

{Login}', { - type: 'code', - code: 'text/html', - multiline: true, - i18nLabel: 'Body', - i18nDescription: 'Password_Changed_Description', - }); - }); - - this.section('Privacy', function() { - this.add('Email_notification_show_message', true, { - type: 'boolean', - public: true, - }); - this.add('Add_Sender_To_ReplyTo', false, { - type: 'boolean', - }); - }); -}); diff --git a/app/lib/server/startup/email.ts b/app/lib/server/startup/email.ts new file mode 100644 index 0000000000000..b8b378d93d380 --- /dev/null +++ b/app/lib/server/startup/email.ts @@ -0,0 +1,483 @@ +import { settingsRegistry } from '../../../settings/server'; + +settingsRegistry.addGroup('Email', function() { + this.section('Style', function() { + this.add('email_plain_text_only', false, { + type: 'boolean', + }); + + this.add('email_style', `html, body, .body { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Helvetica Neue','Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Meiryo UI',Arial,sans-serif; } + + body, .body { + width: 100%; + height: 100%; + } + + a { + color: #1D74F5; + font-weight: bold; + text-decoration: none; + line-height: 1.8; + padding-left: 2px; + padding-right: 2px; + } + p { + margin: 1rem 0; + } + .btn { + text-decoration: none; + color: #FFF; + background-color: #1D74F5; + padding: 12px 18px; + font-weight: 500; + font-size: 14px; + margin-top: 8px; + text-align: center; + cursor: pointer; + display: inline-block; + border-radius: 2px; + } + + ol, ul, div { + list-style-position: inside; + padding: 16px 0 ; + } + li { + padding: 8px 0; + font-weight: 600; + } + .wrap { + width: 100%; + clear: both; + } + + h1,h2,h3,h4,h5,h6 { + line-height: 1.1; margin:0 0 16px 0; color: #000; + } + + h1 { font-weight: 100; font-size: 44px;} + h2 { font-weight: 600; font-size: 30px; color: #2F343D;} + h3 { font-weight: 100; font-size: 27px;} + h4 { font-weight: 500; font-size: 14px; color: #2F343D;} + h5 { font-weight: 500; font-size: 13px; line-height: 1.6; color: #2F343D} + h6 { font-weight: 500; font-size: 10px; color: #6c727A; line-height: 1.7;} + + .container { + display: block; + max-width: 640px; + margin: 0 auto; /* makes it centered */ + clear: both; + border-radius: 2px; + } + + .content { + padding: 36px; + } + + .header-content { + padding-top: 36px; + padding-bottom: 36px; + padding-left: 36px; + padding-right: 36px; + max-width: 640px; + margin: 0 auto; + display: block; + } + + .lead { + margin-bottom: 32px; + color: #2f343d; + line-height: 22px; + font-size: 14px; + } + + .advice { + height: 20px; + color: #9EA2A8; + font-size: 12px; + font-weight: normal; + margin-bottom: 0; + } + .social { + font-size: 12px + } + `, { + type: 'code', + code: 'css', + multiline: true, + i18nLabel: 'email_style_label', + i18nDescription: 'email_style_description', + enableQuery: { + _id: 'email_plain_text_only', + value: false, + }, + }); + }); + + this.section('Subject', function() { + this.add('Offline_DM_Email', '[[Site_Name]] You have been direct messaged by [User]', { + type: 'code', + code: 'text', + multiline: true, + i18nLabel: 'Offline_DM_Email', + i18nDescription: 'Offline_Email_Subject_Description', + }); + this.add('Offline_Mention_Email', '[[Site_Name]] You have been mentioned by [User] in #[Room]', { + type: 'code', + code: 'text', + multiline: true, + i18nLabel: 'Offline_Mention_Email', + i18nDescription: 'Offline_Email_Subject_Description', + }); + this.add('Offline_Mention_All_Email', '[User] has posted a message in #[Room]', { + type: 'code', + code: 'text', + multiline: true, + i18nLabel: 'Offline_Mention_All_Email', + i18nDescription: 'Offline_Email_Subject_Description', + }); + }); + this.section('Header_and_Footer', function() { + this.add('Email_Header', 'Rocket.Chat Cloud
Rocket.chat
', { + type: 'code', + code: 'text/html', + multiline: true, + i18nLabel: 'Header', + }); + this.add('Email_Footer', '
© Rocket.Chat Technologies Corp.
Made with ❤️ in 🇧🇷 🇨🇦 🇩🇪 🇮🇳 🇬🇧 🇺🇸
', { + type: 'code', + code: 'text/html', + multiline: true, + i18nLabel: 'Footer', + }); + this.add('Email_Footer_Direct_Reply', '

{Direct_Reply_Advice}

', { + type: 'code', + code: 'text/html', + multiline: true, + i18nLabel: 'Footer_Direct_Reply', + }); + }); + this.section('Direct_Reply', function() { + this.add('Direct_Reply_Enable', false, { + type: 'boolean', + env: true, + i18nLabel: 'Direct_Reply_Enable', + }); + this.add('Direct_Reply_Debug', false, { + type: 'boolean', + env: true, + i18nLabel: 'Direct_Reply_Debug', + i18nDescription: 'Direct_Reply_Debug_Description', + }); + this.add('Direct_Reply_Protocol', 'IMAP', { + type: 'select', + values: [ + { + key: 'IMAP', + i18nLabel: 'IMAP', + }, { + key: 'POP', + i18nLabel: 'POP', + }, + ], + env: true, + i18nLabel: 'Protocol', + }); + this.add('Direct_Reply_Host', '', { + type: 'string', + env: true, + i18nLabel: 'Host', + }); + this.add('Direct_Reply_Port', '', { + type: 'string', + env: true, + i18nLabel: 'Port', + }); + this.add('Direct_Reply_IgnoreTLS', false, { + type: 'boolean', + env: true, + i18nLabel: 'IgnoreTLS', + }); + this.add('Direct_Reply_Frequency', 5, { + type: 'int', + env: true, + i18nLabel: 'Direct_Reply_Frequency', + enableQuery: { + _id: 'Direct_Reply_Protocol', + value: 'POP', + }, + }); + this.add('Direct_Reply_Delete', true, { + type: 'boolean', + env: true, + i18nLabel: 'Direct_Reply_Delete', + enableQuery: { + _id: 'Direct_Reply_Protocol', + value: 'IMAP', + }, + }); + this.add('Direct_Reply_Separator', '+', { + type: 'select', + values: [ + { + key: '!', + i18nLabel: '!', + }, { + key: '#', + i18nLabel: '#', + }, { + key: '$', + i18nLabel: '$', + }, { + key: '%', + i18nLabel: '%', + }, { + key: '&', + i18nLabel: '&', + }, { + key: '\'', + i18nLabel: '\'', + }, { + key: '*', + i18nLabel: '*', + }, { + key: '+', + i18nLabel: '+', + }, { + key: '-', + i18nLabel: '-', + }, { + key: '/', + i18nLabel: '/', + }, { + key: '=', + i18nLabel: '=', + }, { + key: '?', + i18nLabel: '?', + }, { + key: '^', + i18nLabel: '^', + }, { + key: '_', + i18nLabel: '_', + }, { + key: '`', + i18nLabel: '`', + }, { + key: '{', + i18nLabel: '{', + }, { + key: '|', + i18nLabel: '|', + }, { + key: '}', + i18nLabel: '}', + }, { + key: '~', + i18nLabel: '~', + }, + ], + env: true, + i18nLabel: 'Direct_Reply_Separator', + }); + this.add('Direct_Reply_Username', '', { + type: 'string', + env: true, + i18nLabel: 'Username', + placeholder: 'email@domain', + secret: true, + }); + this.add('Direct_Reply_ReplyTo', '', { + type: 'string', + env: true, + i18nLabel: 'ReplyTo', + placeholder: 'email@domain', + }); + return this.add('Direct_Reply_Password', '', { + type: 'password', + env: true, + i18nLabel: 'Password', + secret: true, + }); + }); + this.section('SMTP', function() { + this.add('SMTP_Protocol', 'smtp', { + type: 'select', + values: [ + { + key: 'smtp', + i18nLabel: 'smtp', + }, { + key: 'smtps', + i18nLabel: 'smtps', + }, + ], + env: true, + i18nLabel: 'Protocol', + }); + this.add('SMTP_Host', '', { + type: 'string', + env: true, + i18nLabel: 'Host', + }); + this.add('SMTP_Port', '', { + type: 'string', + env: true, + i18nLabel: 'Port', + }); + this.add('SMTP_IgnoreTLS', true, { + type: 'boolean', + env: true, + i18nLabel: 'IgnoreTLS', + enableQuery: { + _id: 'SMTP_Protocol', + value: 'smtp', + }, + }); + this.add('SMTP_Pool', true, { + type: 'boolean', + env: true, + i18nLabel: 'Pool', + }); + this.add('SMTP_Username', '', { + type: 'string', + env: true, + i18nLabel: 'Username', + autocomplete: false, + secret: true, + }); + this.add('SMTP_Password', '', { + type: 'password', + env: true, + i18nLabel: 'Password', + autocomplete: false, + secret: true, + }); + this.add('From_Email', '', { + type: 'string', + placeholder: 'email@domain', + }); + return this.add('SMTP_Test_Button', 'sendSMTPTestEmail', { + type: 'action', + actionText: 'Send_a_test_mail_to_my_user', + }); + }); + + this.section('Registration', function() { + this.add('Accounts_Enrollment_Email_Subject', '{Welcome_to Site_name}', { + type: 'string', + i18nLabel: 'Subject', + }); + this.add('Accounts_Enrollment_Email', '

{Welcome_to Site_Name}

{Visit_Site_Url_and_try_the_best_open_source_chat_solution_available_today}

{Login}', { + type: 'code', + code: 'text/html', + multiline: true, + i18nLabel: 'Body', + }); + }); + + this.section('Registration_via_Admin', function() { + this.add('Accounts_UserAddedEmail_Subject', '{Welcome_to Site_Name}', { + type: 'string', + i18nLabel: 'Subject', + }); + this.add('Accounts_UserAddedEmail_Email', '

{Welcome_to Site_Name}

{Visit_Site_Url_and_try_the_best_open_source_chat_solution_available_today}

{Login}', { + type: 'code', + code: 'text/html', + multiline: true, + i18nLabel: 'Body', + i18nDescription: 'Accounts_UserAddedEmail_Description', + }); + }); + + this.section('Verification', function() { + this.add('Verification_Email_Subject', '{Verification_Email_Subject}', { + type: 'string', + i18nLabel: 'Subject', + }); + + this.add('Verification_Email', '

{Hi_username}

{Verification_email_body}

{Verify_your_email}', { + type: 'code', + code: 'text/html', + multiline: true, + i18nLabel: 'Body', + i18nDescription: 'Verification_Description', + }); + }); + + this.section('Offline_Message', function() { + this.add('Offline_Message_Use_DeepLink', true, { + type: 'boolean', + }); + }); + + this.section('Invitation', function() { + this.add('Invitation_Subject', '{Invitation_Subject_Default}', { + type: 'string', + i18nLabel: 'Subject', + }); + this.add('Invitation_Email', '

{Welcome_to Site_Name}

{Visit_Site_Url_and_try_the_best_open_source_chat_solution_available_today}

{Join_Chat}', { + type: 'code', + code: 'text/html', + multiline: true, + i18nLabel: 'Body', + i18nDescription: 'Invitation_Email_Description', + }); + }); + + this.section('Forgot_password_section', function() { + this.add('Forgot_Password_Email_Subject', '{Forgot_Password_Email_Subject}', { + type: 'string', + i18nLabel: 'Subject', + }); + + this.add('Forgot_Password_Email', '

{Forgot_password}

{Lets_get_you_new_one}

{Reset}

{If_you_didnt_ask_for_reset_ignore_this_email}

', { + type: 'code', + code: 'text/html', + multiline: true, + i18nLabel: 'Body', + i18nDescription: 'Forgot_Password_Description', + }); + }); + + this.section('Email_changed_section', function() { + this.add('Email_Changed_Email_Subject', '{Email_Changed_Email_Subject}', { + type: 'string', + i18nLabel: 'Subject', + }); + + this.add('Email_Changed_Email', '

{Hi},

{Your_email_address_has_changed}

{Your_new_email_is_email}

{Login}', { + type: 'code', + code: 'text/html', + multiline: true, + i18nLabel: 'Body', + i18nDescription: 'Email_Changed_Description', + }); + }); + + this.section('Password_changed_section', function() { + this.add('Password_Changed_Email_Subject', '{Password_Changed_Email_Subject}', { + type: 'string', + i18nLabel: 'Subject', + }); + + this.add('Password_Changed_Email', '

{Hi},

{Your_password_was_changed_by_an_admin}

{Your_temporary_password_is_password}

{Login}', { + type: 'code', + code: 'text/html', + multiline: true, + i18nLabel: 'Body', + i18nDescription: 'Password_Changed_Description', + }); + }); + + this.section('Privacy', function() { + this.add('Email_notification_show_message', true, { + type: 'boolean', + public: true, + }); + this.add('Add_Sender_To_ReplyTo', false, { + type: 'boolean', + }); + }); +}); diff --git a/app/lib/server/startup/oAuthServicesUpdate.js b/app/lib/server/startup/oAuthServicesUpdate.js index 2e35236f05c55..c0a4d6f46b588 100644 --- a/app/lib/server/startup/oAuthServicesUpdate.js +++ b/app/lib/server/startup/oAuthServicesUpdate.js @@ -4,64 +4,59 @@ import _ from 'underscore'; import { CustomOAuth } from '../../../custom-oauth'; import { Logger } from '../../../logger'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { addOAuthService } from '../functions/addOAuthService'; -const logger = new Logger('rocketchat:lib', { - methods: { - oauth_updated: { - type: 'info', - }, - }, -}); +const logger = new Logger('rocketchat:lib'); function _OAuthServicesUpdate() { - const services = settings.get(/^(Accounts_OAuth_|Accounts_OAuth_Custom-)[a-z0-9_]+$/i); - services.forEach((service) => { - logger.oauth_updated(service.key); - let serviceName = service.key.replace('Accounts_OAuth_', ''); + const services = settings.getByRegexp(/^(Accounts_OAuth_|Accounts_OAuth_Custom-)[a-z0-9_]+$/i); + services.filter(([, value]) => typeof value === 'boolean').forEach(([key, value]) => { + logger.debug({ oauth_updated: key }); + let serviceName = key.replace('Accounts_OAuth_', ''); if (serviceName === 'Meteor') { serviceName = 'meteor-developer'; } - if (/Accounts_OAuth_Custom-/.test(service.key)) { - serviceName = service.key.replace('Accounts_OAuth_Custom-', ''); + if (/Accounts_OAuth_Custom-/.test(key)) { + serviceName = key.replace('Accounts_OAuth_Custom-', ''); } - if (service.value === true) { + if (value === true) { const data = { - clientId: settings.get(`${ service.key }_id`), - secret: settings.get(`${ service.key }_secret`), + clientId: settings.get(`${ key }_id`), + secret: settings.get(`${ key }_secret`), }; - if (/Accounts_OAuth_Custom-/.test(service.key)) { + if (/Accounts_OAuth_Custom-/.test(key)) { data.custom = true; - data.clientId = settings.get(`${ service.key }-id`); - data.secret = settings.get(`${ service.key }-secret`); - data.serverURL = settings.get(`${ service.key }-url`); - data.tokenPath = settings.get(`${ service.key }-token_path`); - data.identityPath = settings.get(`${ service.key }-identity_path`); - data.authorizePath = settings.get(`${ service.key }-authorize_path`); - data.scope = settings.get(`${ service.key }-scope`); - data.accessTokenParam = settings.get(`${ service.key }-access_token_param`); - data.buttonLabelText = settings.get(`${ service.key }-button_label_text`); - data.buttonLabelColor = settings.get(`${ service.key }-button_label_color`); - data.loginStyle = settings.get(`${ service.key }-login_style`); - data.buttonColor = settings.get(`${ service.key }-button_color`); - data.tokenSentVia = settings.get(`${ service.key }-token_sent_via`); - data.identityTokenSentVia = settings.get(`${ service.key }-identity_token_sent_via`); - data.keyField = settings.get(`${ service.key }-key_field`); - data.usernameField = settings.get(`${ service.key }-username_field`); - data.emailField = settings.get(`${ service.key }-email_field`); - data.nameField = settings.get(`${ service.key }-name_field`); - data.avatarField = settings.get(`${ service.key }-avatar_field`); - data.rolesClaim = settings.get(`${ service.key }-roles_claim`); - data.groupsClaim = settings.get(`${ service.key }-groups_claim`); - data.channelsMap = settings.get(`${ service.key }-groups_channel_map`); - data.channelsAdmin = settings.get(`${ service.key }-channels_admin`); - data.mergeUsers = settings.get(`${ service.key }-merge_users`); - data.mapChannels = settings.get(`${ service.key }-map_channels`); - data.mergeRoles = settings.get(`${ service.key }-merge_roles`); - data.showButton = settings.get(`${ service.key }-show_button`); + data.clientId = settings.get(`${ key }-id`); + data.secret = settings.get(`${ key }-secret`); + data.serverURL = settings.get(`${ key }-url`); + data.tokenPath = settings.get(`${ key }-token_path`); + data.identityPath = settings.get(`${ key }-identity_path`); + data.authorizePath = settings.get(`${ key }-authorize_path`); + data.scope = settings.get(`${ key }-scope`); + data.accessTokenParam = settings.get(`${ key }-access_token_param`); + data.buttonLabelText = settings.get(`${ key }-button_label_text`); + data.buttonLabelColor = settings.get(`${ key }-button_label_color`); + data.loginStyle = settings.get(`${ key }-login_style`); + data.buttonColor = settings.get(`${ key }-button_color`); + data.tokenSentVia = settings.get(`${ key }-token_sent_via`); + data.identityTokenSentVia = settings.get(`${ key }-identity_token_sent_via`); + data.keyField = settings.get(`${ key }-key_field`); + data.usernameField = settings.get(`${ key }-username_field`); + data.emailField = settings.get(`${ key }-email_field`); + data.nameField = settings.get(`${ key }-name_field`); + data.avatarField = settings.get(`${ key }-avatar_field`); + data.rolesClaim = settings.get(`${ key }-roles_claim`); + data.groupsClaim = settings.get(`${ key }-groups_claim`); + data.channelsMap = settings.get(`${ key }-groups_channel_map`); + data.channelsAdmin = settings.get(`${ key }-channels_admin`); + 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(), { serverURL: data.serverURL, @@ -84,6 +79,7 @@ function _OAuthServicesUpdate() { channelsAdmin: data.channelsAdmin, mergeUsers: data.mergeUsers, mergeRoles: data.mergeRoles, + rolesToSync: data.rolesToSync, accessTokenParam: data.accessTokenParam, showButton: data.showButton, }); @@ -137,11 +133,11 @@ function OAuthServicesRemove(_id) { }); } -settings.get(/^Accounts_OAuth_.+/, function() { +settings.watchByRegex(/^Accounts_OAuth_.+/, function() { return OAuthServicesUpdate(); // eslint-disable-line new-cap }); -settings.get(/^Accounts_OAuth_Custom-[a-z0-9_]+/, function(key, value) { +settings.watchByRegex(/^Accounts_OAuth_Custom-[a-z0-9_]+/, function(key, value) { if (!value) { return OAuthServicesRemove(key);// eslint-disable-line new-cap } @@ -190,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 f12f23caa8c85..e10921f3e8dea 100644 --- a/app/lib/server/startup/rateLimiter.js +++ b/app/lib/server/startup/rateLimiter.js @@ -3,11 +3,11 @@ import { Meteor } from 'meteor/meteor'; import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import { RateLimiter } from 'meteor/rate-limit'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { metrics } from '../../../metrics'; import { Logger } from '../../../logger'; -const logger = new Logger('RateLimiter', {}); +const logger = new Logger('RateLimiter'); // Get initial set of names already registered for rules const names = new Set(Object.values(DDPRateLimiter.printRules()) @@ -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(JSON.stringify({ ...reply, ...input }, null, 2)); + logger.info({ msg, reply, input }); metrics.ddpRateLimitExceeded.inc({ limit_name: name, user_id: input.userId, @@ -203,9 +202,9 @@ const configConnectionByMethod = _.debounce(() => { }, 1000); if (!process.env.TEST_MODE) { - settings.get(/^DDP_Rate_Limit_IP_.+/, configIP); - settings.get(/^DDP_Rate_Limit_User_[^B].+/, configUser); - settings.get(/^DDP_Rate_Limit_Connection_[^B].+/, configConnection); - settings.get(/^DDP_Rate_Limit_User_By_Method_.+/, configUserByMethod); - settings.get(/^DDP_Rate_Limit_Connection_By_Method_.+/, configConnectionByMethod); + settings.watchByRegex(/^DDP_Rate_Limit_IP_.+/, configIP); + settings.watchByRegex(/^DDP_Rate_Limit_User_[^B].+/, configUser); + settings.watchByRegex(/^DDP_Rate_Limit_Connection_[^B].+/, configConnection); + settings.watchByRegex(/^DDP_Rate_Limit_User_By_Method_.+/, configUserByMethod); + settings.watchByRegex(/^DDP_Rate_Limit_Connection_By_Method_.+/, configConnectionByMethod); } diff --git a/app/lib/server/startup/settings.js b/app/lib/server/startup/settings.js deleted file mode 100644 index 1e3e5613e19a8..0000000000000 --- a/app/lib/server/startup/settings.js +++ /dev/null @@ -1,3075 +0,0 @@ -import { Random } from 'meteor/random'; - -import { settings } from '../../../settings/server'; -import './email'; -import { MessageTypesValues } from '../../lib/MessageTypes'; - -// Insert server unique id if it doesn't exist -settings.add('uniqueID', process.env.DEPLOYMENT_ID || Random.id(), { - public: true, -}); - -// When you define a setting and want to add a description, you don't need to automatically define the i18nDescription -// if you add a node to the i18n.json with the same setting name but with `_Description` it will automatically work. - -settings.addGroup('Accounts', function() { - this.add('Accounts_AllowAnonymousRead', false, { - type: 'boolean', - public: true, - }); - this.add('Accounts_AllowAnonymousWrite', false, { - type: 'boolean', - public: true, - enableQuery: { - _id: 'Accounts_AllowAnonymousRead', - value: true, - }, - }); - this.add('Accounts_AllowDeleteOwnAccount', false, { - type: 'boolean', - public: true, - enableQuery: { - _id: 'Accounts_AllowUserProfileChange', - value: true, - }, - }); - this.add('Accounts_AllowUserProfileChange', true, { - type: 'boolean', - public: true, - }); - this.add('Accounts_AllowUserAvatarChange', true, { - type: 'boolean', - public: true, - }); - this.add('Accounts_AllowRealNameChange', true, { - type: 'boolean', - public: true, - }); - this.add('Accounts_AllowUserStatusMessageChange', true, { - type: 'boolean', - public: true, - }); - this.add('Accounts_AllowUsernameChange', true, { - type: 'boolean', - public: true, - }); - this.add('Accounts_AllowEmailChange', true, { - type: 'boolean', - public: true, - }); - this.add('Accounts_AllowPasswordChange', true, { - type: 'boolean', - public: true, - }); - this.add('Accounts_AllowPasswordChangeForOAuthUsers', true, { - type: 'boolean', - public: true, - }); - this.add('Accounts_AllowEmailNotifications', true, { - type: 'boolean', - public: true, - }); - this.add('Accounts_CustomFieldsToShowInUserInfo', '', { - type: 'string', - public: true, - }); - this.add('Accounts_LoginExpiration', 90, { - type: 'int', - public: true, - }); - this.add('Accounts_ShowFormLogin', true, { - type: 'boolean', - public: true, - }); - this.add('Accounts_EmailOrUsernamePlaceholder', '', { - type: 'string', - public: true, - i18nLabel: 'Placeholder_for_email_or_username_login_field', - }); - this.add('Accounts_PasswordPlaceholder', '', { - type: 'string', - public: true, - i18nLabel: 'Placeholder_for_password_login_field', - }); - - this.add('Accounts_ConfirmPasswordPlaceholder', '', { - type: 'string', - public: true, - i18nLabel: 'Placeholder_for_password_login_confirm_field', - }); - this.add('Accounts_ForgetUserSessionOnWindowClose', false, { - type: 'boolean', - public: true, - }); - this.add('Accounts_SearchFields', 'username, name, bio, nickname', { - type: 'string', - }); - this.add('Accounts_Directory_DefaultView', 'channels', { - type: 'select', - values: [ - { - key: 'channels', - i18nLabel: 'Channels', - }, - { - key: 'users', - i18nLabel: 'Users', - }, - ], - public: true, - }); - this.add('Accounts_AllowInvisibleStatusOption', true, { - type: 'boolean', - public: true, - i18nLabel: 'Accounts_AllowInvisibleStatusOption', - }); - - this.section('Registration', function() { - this.add('Accounts_Send_Email_When_Activating', true, { - type: 'boolean', - }); - this.add('Accounts_Send_Email_When_Deactivating', true, { - type: 'boolean', - }); - this.add('Accounts_DefaultUsernamePrefixSuggestion', 'user', { - type: 'string', - }); - this.add('Accounts_RequireNameForSignUp', true, { // TODO rename to Accounts_RequireFullName - type: 'boolean', - public: true, - }); - this.add('Accounts_RequirePasswordConfirmation', true, { - type: 'boolean', - public: true, - }); - this.add('Accounts_EmailVerification', false, { - type: 'boolean', - public: true, - enableQuery: { - _id: 'SMTP_Host', - value: { - $exists: 1, - $ne: '', - }, - }, - }); - this.add('Accounts_Verify_Email_For_External_Accounts', true, { - type: 'boolean', - }); - this.add('Accounts_ManuallyApproveNewUsers', false, { - public: true, - type: 'boolean', - }); - this.add('Accounts_AllowedDomainsList', '', { - type: 'string', - public: true, - }); - this.add('Accounts_BlockedDomainsList', '', { - type: 'string', - }); - this.add('Accounts_BlockedUsernameList', '', { - type: 'string', - }); - this.add('Accounts_SystemBlockedUsernameList', 'admin,administrator,system,user', { - type: 'string', - hidden: true, - }); - this.add('Accounts_UseDefaultBlockedDomainsList', true, { - type: 'boolean', - }); - this.add('Accounts_UseDNSDomainCheck', false, { - type: 'boolean', - }); - this.add('Accounts_RegistrationForm', 'Public', { - type: 'select', - public: true, - values: [ - { - key: 'Public', - i18nLabel: 'Accounts_RegistrationForm_Public', - }, { - key: 'Disabled', - i18nLabel: 'Accounts_RegistrationForm_Disabled', - }, { - key: 'Secret URL', - i18nLabel: 'Accounts_RegistrationForm_Secret_URL', - }, - ], - }); - this.add('Accounts_RegistrationForm_SecretURL', Random.id(), { - type: 'string', - secret: true, - }); - this.add('Accounts_Registration_InviteUrlType', 'proxy', { - type: 'select', - values: [ - { - key: 'direct', - i18nLabel: 'Accounts_Registration_InviteUrlType_Direct', - }, { - key: 'proxy', - i18nLabel: 'Accounts_Registration_InviteUrlType_Proxy', - }, - ], - }); - - this.add('Accounts_RegistrationForm_LinkReplacementText', 'New user registration is currently disabled', { - type: 'string', - public: true, - }); - this.add('Accounts_Registration_AuthenticationServices_Enabled', true, { - type: 'boolean', - public: true, - }); - this.add('Accounts_Registration_AuthenticationServices_Default_Roles', 'user', { - type: 'string', - enableQuery: { - _id: 'Accounts_Registration_AuthenticationServices_Enabled', - value: true, - }, - }); - this.add('Accounts_Registration_Users_Default_Roles', 'user', { - type: 'string', - }); - this.add('Accounts_PasswordReset', true, { - type: 'boolean', - public: true, - }); - this.add('Accounts_CustomFields', '', { - type: 'code', - public: true, - i18nLabel: 'Custom_Fields', - }); - }); - - this.section('Accounts_Default_User_Preferences', function() { - this.add('Accounts_Default_User_Preferences_enableAutoAway', true, { - type: 'boolean', - public: true, - i18nLabel: 'Enable_Auto_Away', - }); - this.add('Accounts_Default_User_Preferences_idleTimeLimit', 300, { - type: 'int', - public: true, - i18nLabel: 'Idle_Time_Limit', - }); - this.add('Accounts_Default_User_Preferences_desktopNotificationRequireInteraction', false, { - type: 'boolean', - public: true, - i18nLabel: 'Notification_RequireInteraction', - i18nDescription: 'Notification_RequireInteraction_Description', - }); - this.add('Accounts_Default_User_Preferences_audioNotifications', 'mentions', { - type: 'select', - values: [ - { - key: 'all', - i18nLabel: 'All_messages', - }, - { - key: 'mentions', - i18nLabel: 'Mentions', - }, - { - key: 'nothing', - i18nLabel: 'Nothing', - }, - ], - public: true, - }); - this.add('Accounts_Default_User_Preferences_desktopNotifications', 'all', { - type: 'select', - values: [ - { - key: 'all', - i18nLabel: 'All_messages', - }, - { - key: 'mentions', - i18nLabel: 'Mentions', - }, - { - key: 'nothing', - i18nLabel: 'Nothing', - }, - ], - public: true, - }); - this.add('Accounts_Default_User_Preferences_mobileNotifications', 'all', { - type: 'select', - values: [ - { - key: 'all', - i18nLabel: 'All_messages', - }, - { - key: 'mentions', - i18nLabel: 'Mentions', - }, - { - key: 'nothing', - i18nLabel: 'Nothing', - }, - ], - public: true, - }); - this.add('Accounts_Default_User_Preferences_unreadAlert', true, { - type: 'boolean', - public: true, - i18nLabel: 'Unread_Tray_Icon_Alert', - }); - this.add('Accounts_Default_User_Preferences_useEmojis', true, { - type: 'boolean', - public: true, - i18nLabel: 'Use_Emojis', - }); - this.add('Accounts_Default_User_Preferences_convertAsciiEmoji', true, { - type: 'boolean', - public: true, - i18nLabel: 'Convert_Ascii_Emojis', - }); - this.add('Accounts_Default_User_Preferences_autoImageLoad', true, { - type: 'boolean', - public: true, - i18nLabel: 'Auto_Load_Images', - }); - this.add('Accounts_Default_User_Preferences_saveMobileBandwidth', true, { - type: 'boolean', - public: true, - i18nLabel: 'Save_Mobile_Bandwidth', - }); - this.add('Accounts_Default_User_Preferences_collapseMediaByDefault', false, { - type: 'boolean', - public: true, - i18nLabel: 'Collapse_Embedded_Media_By_Default', - }); - this.add('Accounts_Default_User_Preferences_hideUsernames', false, { - type: 'boolean', - public: true, - i18nLabel: 'Hide_usernames', - }); - this.add('Accounts_Default_User_Preferences_hideRoles', false, { - type: 'boolean', - public: true, - i18nLabel: 'Hide_roles', - }); - this.add('Accounts_Default_User_Preferences_hideFlexTab', false, { - type: 'boolean', - public: true, - i18nLabel: 'Hide_flextab', - }); - this.add('Accounts_Default_User_Preferences_displayAvatars', true, { - type: 'boolean', - public: true, - i18nLabel: 'Display_avatars', - }); - this.add('Accounts_Default_User_Preferences_sidebarGroupByType', true, { - type: 'boolean', - public: true, - i18nLabel: 'Group_by_Type', - }); - this.add('Accounts_Default_User_Preferences_sidebarViewMode', 'medium', { - type: 'select', - values: [ - { - key: 'extended', - i18nLabel: 'Extended', - }, - { - key: 'medium', - i18nLabel: 'Medium', - }, - { - key: 'condensed', - i18nLabel: 'Condensed', - }, - ], - public: true, - i18nLabel: 'Sidebar_list_mode', - }); - this.add('Accounts_Default_User_Preferences_sidebarDisplayAvatar', true, { - type: 'boolean', - public: true, - i18nLabel: 'Display_Avatars_Sidebar', - }); - - this.add('Accounts_Default_User_Preferences_sidebarShowUnread', false, { - type: 'boolean', - public: true, - i18nLabel: 'Unread_on_top', - }); - - this.add('Accounts_Default_User_Preferences_sidebarSortby', 'activity', { - type: 'select', - values: [ - { - key: 'activity', - i18nLabel: 'Activity', - }, - { - key: 'alphabetical', - i18nLabel: 'Alphabetical', - }, - ], - public: true, - i18nLabel: 'Sort_By', - }); - - this.add('Accounts_Default_User_Preferences_showMessageInMainThread', false, { - type: 'boolean', - public: true, - i18nLabel: 'Show_Message_In_Main_Thread', - }); - - this.add('Accounts_Default_User_Preferences_sidebarShowFavorites', true, { - type: 'boolean', - public: true, - i18nLabel: 'Group_favorites', - }); - - this.add('Accounts_Default_User_Preferences_sendOnEnter', 'normal', { - type: 'select', - values: [ - { - key: 'normal', - i18nLabel: 'Enter_Normal', - }, - { - key: 'alternative', - i18nLabel: 'Enter_Alternative', - }, - { - key: 'desktop', - i18nLabel: 'Only_On_Desktop', - }, - ], - public: true, - i18nLabel: 'Enter_Behaviour', - }); - this.add('Accounts_Default_User_Preferences_messageViewMode', 0, { - type: 'select', - values: [ - { - key: 0, - i18nLabel: 'Normal', - }, - { - key: 1, - i18nLabel: 'Cozy', - }, - { - key: 2, - i18nLabel: 'Compact', - }, - ], - public: true, - i18nLabel: 'MessageBox_view_mode', - }); - this.add('Accounts_Default_User_Preferences_emailNotificationMode', 'mentions', { - type: 'select', - values: [ - { - key: 'nothing', - i18nLabel: 'Email_Notification_Mode_Disabled', - }, - { - key: 'mentions', - i18nLabel: 'Email_Notification_Mode_All', - }, - ], - public: true, - i18nLabel: 'Email_Notification_Mode', - }); - this.add('Accounts_Default_User_Preferences_newRoomNotification', 'door', { - type: 'select', - values: [ - { - key: 'none', - i18nLabel: 'None', - }, - { - key: 'door', - i18nLabel: 'Default', - }, - ], - public: true, - i18nLabel: 'New_Room_Notification', - }); - this.add('Accounts_Default_User_Preferences_newMessageNotification', 'chime', { - type: 'select', - values: [ - { - key: 'none', - i18nLabel: 'None', - }, - { - key: 'chime', - i18nLabel: 'Default', - }, - ], - public: true, - i18nLabel: 'New_Message_Notification', - }); - - this.add('Accounts_Default_User_Preferences_muteFocusedConversations', true, { - type: 'boolean', - public: true, - i18nLabel: 'Mute_Focused_Conversations', - }); - - this.add('Accounts_Default_User_Preferences_notificationsSoundVolume', 100, { - type: 'int', - public: true, - i18nLabel: 'Notifications_Sound_Volume', - }); - - this.add('Accounts_Default_User_Preferences_enableMessageParserEarlyAdoption', false, { - type: 'boolean', - public: true, - i18nLabel: 'Enable_message_parser_early_adoption', - alert: 'Enable_message_parser_early_adoption_alert', - }); - }); - - this.section('Avatar', function() { - this.add('Accounts_AvatarResize', true, { - type: 'boolean', - }); - this.add('Accounts_AvatarSize', 200, { - type: 'int', - enableQuery: { - _id: 'Accounts_AvatarResize', - value: true, - }, - }); - - this.add('Accounts_AvatarExternalProviderUrl', '', { - type: 'string', - public: true, - }); - - this.add('Accounts_RoomAvatarExternalProviderUrl', '', { - type: 'string', - public: true, - }); - - this.add('Accounts_AvatarCacheTime', 3600, { - type: 'int', - i18nDescription: 'Accounts_AvatarCacheTime_description', - }); - - this.add('Accounts_AvatarBlockUnauthenticatedAccess', false, { - type: 'boolean', - public: true, - }); - - return this.add('Accounts_SetDefaultAvatar', true, { - type: 'boolean', - }); - }); - - this.section('Password_Policy', function() { - this.add('Accounts_Password_Policy_Enabled', false, { - type: 'boolean', - }); - - const enableQuery = { - _id: 'Accounts_Password_Policy_Enabled', - value: true, - }; - - this.add('Accounts_Password_Policy_MinLength', 7, { - type: 'int', - enableQuery, - }); - - this.add('Accounts_Password_Policy_MaxLength', -1, { - type: 'int', - enableQuery, - }); - - this.add('Accounts_Password_Policy_ForbidRepeatingCharacters', true, { - type: 'boolean', - enableQuery, - }); - - this.add('Accounts_Password_Policy_ForbidRepeatingCharactersCount', 3, { - type: 'int', - enableQuery, - }); - - this.add('Accounts_Password_Policy_AtLeastOneLowercase', true, { - type: 'boolean', - enableQuery, - }); - - this.add('Accounts_Password_Policy_AtLeastOneUppercase', true, { - type: 'boolean', - enableQuery, - }); - - this.add('Accounts_Password_Policy_AtLeastOneNumber', true, { - type: 'boolean', - enableQuery, - }); - - this.add('Accounts_Password_Policy_AtLeastOneSpecialCharacter', true, { - type: 'boolean', - enableQuery, - }); - }); - - this.section('Password_History', function() { - this.add('Accounts_Password_History_Enabled', false, { - type: 'boolean', - i18nLabel: 'Enable_Password_History', - i18nDescription: 'Enable_Password_History_Description', - }); - - const enableQuery = { - _id: 'Accounts_Password_History_Enabled', - value: true, - }; - - this.add('Accounts_Password_History_Amount', 5, { - type: 'int', - enableQuery, - i18nLabel: 'Password_History_Amount', - i18nDescription: 'Password_History_Amount_Description', - }); - }); -}); - -settings.addGroup('OAuth', function() { - this.section('Facebook', function() { - const enableQuery = { - _id: 'Accounts_OAuth_Facebook', - value: true, - }; - this.add('Accounts_OAuth_Facebook', false, { - type: 'boolean', - public: true, - }); - this.add('Accounts_OAuth_Facebook_id', '', { - type: 'string', - enableQuery, - }); - this.add('Accounts_OAuth_Facebook_secret', '', { - type: 'string', - enableQuery, - secret: true, - }); - return this.add('Accounts_OAuth_Facebook_callback_url', '_oauth/facebook', { - type: 'relativeUrl', - readonly: true, - force: true, - enableQuery, - }); - }); - this.section('Google', function() { - const enableQuery = { - _id: 'Accounts_OAuth_Google', - value: true, - }; - this.add('Accounts_OAuth_Google', false, { - type: 'boolean', - public: true, - }); - this.add('Accounts_OAuth_Google_id', '', { - type: 'string', - enableQuery, - }); - this.add('Accounts_OAuth_Google_secret', '', { - type: 'string', - enableQuery, - secret: true, - }); - return this.add('Accounts_OAuth_Google_callback_url', '_oauth/google', { - type: 'relativeUrl', - readonly: true, - force: true, - enableQuery, - }); - }); - this.section('GitHub', function() { - const enableQuery = { - _id: 'Accounts_OAuth_Github', - value: true, - }; - this.add('Accounts_OAuth_Github', false, { - type: 'boolean', - public: true, - }); - this.add('Accounts_OAuth_Github_id', '', { - type: 'string', - enableQuery, - }); - this.add('Accounts_OAuth_Github_secret', '', { - type: 'string', - enableQuery, - secret: true, - }); - return this.add('Accounts_OAuth_Github_callback_url', '_oauth/github', { - type: 'relativeUrl', - readonly: true, - force: true, - enableQuery, - }); - }); - this.section('Linkedin', function() { - const enableQuery = { - _id: 'Accounts_OAuth_Linkedin', - value: true, - }; - this.add('Accounts_OAuth_Linkedin', false, { - type: 'boolean', - public: true, - }); - this.add('Accounts_OAuth_Linkedin_id', '', { - type: 'string', - enableQuery, - }); - this.add('Accounts_OAuth_Linkedin_secret', '', { - type: 'string', - enableQuery, - secret: true, - }); - return this.add('Accounts_OAuth_Linkedin_callback_url', '_oauth/linkedin', { - type: 'relativeUrl', - readonly: true, - force: true, - enableQuery, - }); - }); - this.section('Meteor', function() { - const enableQuery = { - _id: 'Accounts_OAuth_Meteor', - value: true, - }; - this.add('Accounts_OAuth_Meteor', false, { - type: 'boolean', - public: true, - }); - this.add('Accounts_OAuth_Meteor_id', '', { - type: 'string', - enableQuery, - }); - this.add('Accounts_OAuth_Meteor_secret', '', { - type: 'string', - enableQuery, - secret: true, - }); - return this.add('Accounts_OAuth_Meteor_callback_url', '_oauth/meteor', { - type: 'relativeUrl', - readonly: true, - force: true, - enableQuery, - }); - }); - this.section('Twitter', function() { - const enableQuery = { - _id: 'Accounts_OAuth_Twitter', - value: true, - }; - this.add('Accounts_OAuth_Twitter', false, { - type: 'boolean', - public: true, - }); - this.add('Accounts_OAuth_Twitter_id', '', { - type: 'string', - enableQuery, - }); - this.add('Accounts_OAuth_Twitter_secret', '', { - type: 'string', - enableQuery, - secret: true, - }); - return this.add('Accounts_OAuth_Twitter_callback_url', '_oauth/twitter', { - type: 'relativeUrl', - readonly: true, - force: true, - enableQuery, - }); - }); - return this.section('Proxy', function() { - this.add('Accounts_OAuth_Proxy_host', 'https://oauth-proxy.rocket.chat', { - type: 'string', - public: true, - }); - return this.add('Accounts_OAuth_Proxy_services', '', { - type: 'string', - public: true, - }); - }); -}); - -settings.addGroup('General', function() { - this.add('Show_Setup_Wizard', 'pending', { - type: 'select', - public: true, - readonly: true, - values: [ - { - key: 'pending', - i18nLabel: 'Pending', - }, { - key: 'in_progress', - i18nLabel: 'In_progress', - }, { - key: 'completed', - i18nLabel: 'Completed', - }, - ], - }); - this.add('Site_Url', typeof __meteor_runtime_config__ !== 'undefined' && __meteor_runtime_config__ !== null ? __meteor_runtime_config__.ROOT_URL : null, { - type: 'string', - i18nDescription: 'Site_Url_Description', - public: true, - }); - this.add('Site_Name', 'Rocket.Chat', { - type: 'string', - public: true, - wizard: { - step: 3, - order: 0, - }, - }); - this.add('Document_Domain', '', { - type: 'string', - public: true, - }); - this.add('Language', '', { - type: 'language', - public: true, - wizard: { - step: 3, - order: 1, - }, - }); - this.add('Allow_Invalid_SelfSigned_Certs', false, { - type: 'boolean', - secret: true, - }); - - this.add('Enable_CSP', true, { - type: 'boolean', - }); - - this.add('Iframe_Restrict_Access', true, { - type: 'boolean', - secret: true, - }); - this.add('Iframe_X_Frame_Options', 'sameorigin', { - type: 'string', - secret: true, - enableQuery: { - _id: 'Iframe_Restrict_Access', - value: true, - }, - }); - this.add('Favorite_Rooms', true, { - type: 'boolean', - public: true, - }); - this.add('First_Channel_After_Login', '', { - type: 'string', - public: true, - }); - this.add('Unread_Count', 'user_and_group_mentions_only', { - type: 'select', - values: [ - { - key: 'all_messages', - i18nLabel: 'All_messages', - }, { - key: 'user_mentions_only', - i18nLabel: 'User_mentions_only', - }, { - key: 'group_mentions_only', - i18nLabel: 'Group_mentions_only', - }, { - key: 'user_and_group_mentions_only', - i18nLabel: 'User_and_group_mentions_only', - }, - ], - public: true, - }); - this.add('Unread_Count_DM', 'all_messages', { - type: 'select', - values: [ - { - key: 'all_messages', - i18nLabel: 'All_messages', - }, { - key: 'mentions_only', - i18nLabel: 'Mentions_only', - }, - ], - public: true, - }); - - this.add('DeepLink_Url', 'https://go.rocket.chat', { - type: 'string', - public: true, - }); - - this.add('CDN_PREFIX', '', { - type: 'string', - public: true, - }); - this.add('CDN_PREFIX_ALL', true, { - type: 'boolean', - public: true, - }); - this.add('CDN_JSCSS_PREFIX', '', { - type: 'string', - public: true, - enableQuery: { - _id: 'CDN_PREFIX_ALL', - value: false, - }, - }); - this.add('Force_SSL', false, { - type: 'boolean', - public: true, - }); - - // Deprecated setting - this.add('Support_Cordova_App', false, { - type: 'boolean', - i18nDescription: 'Support_Cordova_App_Description', - alert: 'Support_Cordova_App_Alert', - }); - this.add('GoogleTagManager_id', '', { - type: 'string', - public: true, - secret: true, - }); - this.add('Bugsnag_api_key', '', { - type: 'string', - public: false, - secret: true, - }); - this.add('Restart', 'restart_server', { - type: 'action', - actionText: 'Restart_the_server', - }); - this.add('Store_Last_Message', true, { - type: 'boolean', - public: true, - i18nDescription: 'Store_Last_Message_Sent_per_Room', - }); - this.add('Robot_Instructions_File_Content', 'User-agent: *\nDisallow: /', { - type: 'string', - public: true, - multiline: true, - }); - this.add('Default_Referrer_Policy', 'same-origin', { - type: 'select', - values: [ - { - key: 'no-referrer', - i18nLabel: 'No_Referrer', - }, { - key: 'no-referrer-when-downgrade', - i18nLabel: 'No_Referrer_When_Downgrade', - }, { - key: 'origin', - i18nLabel: 'Origin', - }, { - key: 'origin-when-cross-origin', - i18nLabel: 'Origin_When_Cross_Origin', - }, { - key: 'same-origin', - i18nLabel: 'Same_Origin', - }, { - key: 'strict-origin', - i18nLabel: 'Strict_Origin', - }, { - key: 'strict-origin-when-cross-origin', - i18nLabel: 'Strict_Origin_When_Cross_Origin', - }, { - key: 'unsafe-url', - i18nLabel: 'Unsafe_Url', - }, - ], - public: true, - }); - this.add('ECDH_Enabled', false, { - type: 'boolean', - alert: 'This_feature_is_currently_in_alpha', - }); - this.section('UTF8', function() { - this.add('UTF8_User_Names_Validation', '[0-9a-zA-Z-_.]+', { - type: 'string', - public: true, - i18nDescription: 'UTF8_User_Names_Validation_Description', - }); - this.add('UTF8_Channel_Names_Validation', '[0-9a-zA-Z-_.]+', { - type: 'string', - public: true, - i18nDescription: 'UTF8_Channel_Names_Validation_Description', - }); - return this.add('UTF8_Names_Slugify', true, { - type: 'boolean', - public: true, - }); - }); - this.section('Reporting', function() { - return this.add('Statistics_reporting', true, { - type: 'boolean', - }); - }); - this.section('Notifications', function() { - this.add('Notifications_Max_Room_Members', 100, { - type: 'int', - public: true, - i18nDescription: 'Notifications_Max_Room_Members_Description', - }); - }); - this.section('REST API', function() { - return this.add('API_User_Limit', 500, { - type: 'int', - public: true, - i18nDescription: 'API_User_Limit', - }); - }); - this.section('Iframe_Integration', function() { - this.add('Iframe_Integration_send_enable', false, { - type: 'boolean', - public: true, - }); - this.add('Iframe_Integration_send_target_origin', '*', { - type: 'string', - public: true, - enableQuery: { - _id: 'Iframe_Integration_send_enable', - value: true, - }, - }); - this.add('Iframe_Integration_receive_enable', false, { - type: 'boolean', - public: true, - }); - return this.add('Iframe_Integration_receive_origin', '*', { - type: 'string', - public: true, - enableQuery: { - _id: 'Iframe_Integration_receive_enable', - value: true, - }, - }); - }); - this.section('Translations', function() { - return this.add('Custom_Translations', '', { - type: 'code', - public: true, - }); - }); - this.section('Stream_Cast', function() { - return this.add('Stream_Cast_Address', '', { - type: 'string', - }); - }); - this.section('NPS', function() { - this.add('NPS_survey_enabled', true, { - type: 'boolean', - }); - }); - this.section('Timezone', function() { - this.add('Default_Timezone_For_Reporting', 'server', { - type: 'select', - values: [{ - key: 'server', - i18nLabel: 'Default_Server_Timezone', - }, { - key: 'custom', - i18nLabel: 'Default_Custom_Timezone', - }, { - key: 'user', - i18nLabel: 'Default_User_Timezone', - }], - }); - this.add('Default_Custom_Timezone', '', { - type: 'timezone', - enableQuery: { - _id: 'Default_Timezone_For_Reporting', - value: 'custom', - }, - }); - }); -}); - -settings.addGroup('Message', function() { - this.section('Message_Attachments', function() { - this.add('Message_Attachments_GroupAttach', false, { - type: 'boolean', - public: true, - i18nDescription: 'Message_Attachments_GroupAttachDescription', - }); - - this.add('Message_Attachments_Thumbnails_Enabled', true, { - type: 'boolean', - public: true, - i18nDescription: 'Message_Attachments_Thumbnails_EnabledDesc', - }); - - this.add('Message_Attachments_Thumbnails_Width', 480, { - type: 'int', - public: true, - enableQuery: [ - { - _id: 'Message_Attachments_Thumbnails_Enabled', - value: true, - }, - ], - }); - - this.add('Message_Attachments_Thumbnails_Height', 360, { - type: 'int', - public: true, - enableQuery: [ - { - _id: 'Message_Attachments_Thumbnails_Enabled', - value: true, - }, - ], - }); - - this.add('Message_Attachments_Strip_Exif', false, { - type: 'boolean', - public: true, - i18nDescription: 'Message_Attachments_Strip_ExifDescription', - }); - }); - this.section('Message_Audio', function() { - this.add('Message_AudioRecorderEnabled', true, { - type: 'boolean', - public: true, - i18nDescription: 'Message_AudioRecorderEnabledDescription', - }); - this.add('Message_Audio_bitRate', 32, { - type: 'int', - public: true, - }); - }); - this.add('Message_AllowEditing', true, { - type: 'boolean', - public: true, - }); - this.add('Message_AllowEditing_BlockEditInMinutes', 0, { - type: 'int', - public: true, - i18nDescription: 'Message_AllowEditing_BlockEditInMinutesDescription', - }); - this.add('Message_AllowDeleting', true, { - type: 'boolean', - public: true, - }); - this.add('Message_AllowDeleting_BlockDeleteInMinutes', 0, { - type: 'int', - public: true, - i18nDescription: 'Message_AllowDeleting_BlockDeleteInMinutes', - }); - this.add('Message_AllowUnrecognizedSlashCommand', false, { - type: 'boolean', - public: true, - }); - this.add('Message_AllowDirectMessagesToYourself', true, { - type: 'boolean', - public: true, - }); - this.add('Message_AlwaysSearchRegExp', false, { - type: 'boolean', - }); - this.add('Message_ShowEditedStatus', true, { - type: 'boolean', - public: true, - }); - this.add('Message_ShowDeletedStatus', false, { - type: 'boolean', - public: true, - }); - this.add('Message_AllowBadWordsFilter', false, { - type: 'boolean', - public: true, - }); - this.add('Message_BadWordsFilterList', '', { - type: 'string', - public: true, - }); - this.add('Message_BadWordsWhitelist', '', { - type: 'string', - public: true, - }); - this.add('Message_KeepHistory', false, { - type: 'boolean', - public: true, - }); - this.add('Message_MaxAll', 0, { - type: 'int', - public: true, - }); - this.add('Message_MaxAllowedSize', 5000, { - type: 'int', - public: true, - }); - this.add('Message_AllowConvertLongMessagesToAttachment', true, { - type: 'boolean', - public: true, - }); - this.add('Message_ShowFormattingTips', true, { - type: 'boolean', - public: true, - }); - this.add('Message_GroupingPeriod', 300, { - type: 'int', - public: true, - i18nDescription: 'Message_GroupingPeriodDescription', - }); - this.add('API_Embed', true, { - type: 'boolean', - public: true, - }); - this.add('API_Embed_UserAgent', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36', { - type: 'string', - public: true, - }); - this.add('API_EmbedCacheExpirationDays', 30, { - type: 'int', - public: false, - }); - this.add('API_Embed_clear_cache_now', 'OEmbedCacheCleanup', { - type: 'action', - actionText: 'clear', - i18nLabel: 'clear_cache_now', - }); - this.add('API_EmbedDisabledFor', '', { - type: 'string', - public: true, - i18nDescription: 'API_EmbedDisabledFor_Description', - }); - this.add('API_EmbedIgnoredHosts', 'localhost, 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16', { - type: 'string', - i18nDescription: 'API_EmbedIgnoredHosts_Description', - }); - this.add('API_EmbedSafePorts', '80, 443', { - type: 'string', - }); - this.add('Message_TimeFormat', 'LT', { - type: 'string', - public: true, - i18nDescription: 'Message_TimeFormat_Description', - }); - this.add('Message_DateFormat', 'LL', { - type: 'string', - public: true, - i18nDescription: 'Message_DateFormat_Description', - }); - this.add('Message_TimeAndDateFormat', 'LLL', { - type: 'string', - public: true, - i18nDescription: 'Message_TimeAndDateFormat_Description', - }); - this.add('Message_QuoteChainLimit', 2, { - type: 'int', - public: true, - }); - - - this.add('Hide_System_Messages', [], { - type: 'multiSelect', - public: true, - values: MessageTypesValues, - }); - - this.add('DirectMesssage_maxUsers', 8, { - type: 'int', - public: true, - }); - - this.add('Message_ErasureType', 'Delete', { - type: 'select', - public: true, - values: [ - { - key: 'Keep', - i18nLabel: 'Message_ErasureType_Keep', - }, { - key: 'Delete', - i18nLabel: 'Message_ErasureType_Delete', - }, { - key: 'Unlink', - i18nLabel: 'Message_ErasureType_Unlink', - }, - ], - }); -}); - -settings.addGroup('Meta', function() { - this.add('Meta_language', '', { - type: 'string', - }); - this.add('Meta_fb_app_id', '', { - type: 'string', - secret: true, - }); - this.add('Meta_robots', 'INDEX,FOLLOW', { - type: 'string', - }); - this.add('Meta_google-site-verification', '', { - type: 'string', - secret: true, - }); - this.add('Meta_msvalidate01', '', { - type: 'string', - secret: true, - }); - return this.add('Meta_custom', '', { - type: 'code', - code: 'text/html', - multiline: true, - }); -}); - -settings.addGroup('Mobile', function() { - this.add('Allow_Save_Media_to_Gallery', true, { - type: 'boolean', - public: true, - }); - this.section('Screen_Lock', function() { - this.add('Force_Screen_Lock', false, { type: 'boolean', i18nDescription: 'Force_Screen_Lock_description', public: true }); - this.add('Force_Screen_Lock_After', 1800, { type: 'int', i18nDescription: 'Force_Screen_Lock_After_description', enableQuery: { _id: 'Force_Screen_Lock', value: true }, public: true }); - }); -}); - -const pushEnabledWithoutGateway = [ - { - _id: 'Push_enable', - value: true, - }, { - _id: 'Push_enable_gateway', - value: false, - }, -]; - -settings.addGroup('Push', function() { - this.add('Push_enable', true, { - type: 'boolean', - public: true, - alert: 'Push_Setting_Requires_Restart_Alert', - }); - - this.add('Push_enable_gateway', true, { - type: 'boolean', - alert: 'Push_Setting_Requires_Restart_Alert', - enableQuery: [ - { - _id: 'Push_enable', - value: true, - }, - { - _id: 'Register_Server', - value: true, - }, - { - _id: 'Cloud_Service_Agree_PrivacyTerms', - value: true, - }, - ], - }); - this.add('Push_gateway', 'https://gateway.rocket.chat', { - type: 'string', - i18nDescription: 'Push_gateway_description', - alert: 'Push_Setting_Requires_Restart_Alert', - multiline: true, - enableQuery: [ - { - _id: 'Push_enable', - value: true, - }, { - _id: 'Push_enable_gateway', - value: true, - }, - ], - }); - this.add('Push_production', true, { - type: 'boolean', - public: true, - alert: 'Push_Setting_Requires_Restart_Alert', - enableQuery: pushEnabledWithoutGateway, - }); - this.add('Push_test_push', 'push_test', { - type: 'action', - actionText: 'Send_a_test_push_to_my_user', - enableQuery: { - _id: 'Push_enable', - value: true, - }, - }); - this.section('Certificates_and_Keys', function() { - this.add('Push_apn_passphrase', '', { - type: 'string', - enableQuery: [], - secret: true, - }); - this.add('Push_apn_key', '', { - type: 'string', - multiline: true, - enableQuery: [], - secret: true, - }); - this.add('Push_apn_cert', '', { - type: 'string', - multiline: true, - enableQuery: [], - secret: true, - }); - this.add('Push_apn_dev_passphrase', '', { - type: 'string', - enableQuery: [], - secret: true, - }); - this.add('Push_apn_dev_key', '', { - type: 'string', - multiline: true, - enableQuery: [], - secret: true, - }); - this.add('Push_apn_dev_cert', '', { - type: 'string', - multiline: true, - enableQuery: [], - secret: true, - }); - this.add('Push_gcm_api_key', '', { - type: 'string', - enableQuery: [], - secret: true, - }); - return this.add('Push_gcm_project_number', '', { - type: 'string', - public: true, - enableQuery: [], - secret: true, - }); - }); - return this.section('Privacy', function() { - this.add('Push_show_username_room', true, { - type: 'boolean', - public: true, - }); - this.add('Push_show_message', true, { - type: 'boolean', - public: true, - }); - this.add('Push_request_content_from_server', true, { - type: 'boolean', - enterprise: true, - invalidValue: false, - modules: [ - 'push-privacy', - ], - }); - }); -}); - -settings.addGroup('Layout', function() { - this.section('Content', function() { - this.add('Layout_Home_Title', 'Home', { - type: 'string', - public: true, - }); - this.add('Layout_Show_Home_Button', true, { - type: 'boolean', - public: true, - }); - this.add('Layout_Home_Body', '

Welcome to Rocket.Chat!

\n

The Rocket.Chat desktops apps for Windows, macOS and Linux are available to download here.

The native mobile app, Rocket.Chat,\n for Android and iOS is available from Google Play and the App Store.

\n

For further help, please consult the documentation.

\n

If you\'re an admin, feel free to change this content via AdministrationLayoutHome Body. Or clicking here.

', { - type: 'code', - code: 'text/html', - multiline: true, - public: true, - }); - this.add('Layout_Terms_of_Service', 'Terms of Service
Go to APP SETTINGS → Layout to customize this page.', { - type: 'code', - code: 'text/html', - multiline: true, - public: true, - }); - this.add('Layout_Login_Terms', 'By proceeding you are agreeing to our Terms of Service, Privacy Policy and Legal Notice.', { - type: 'string', - multiline: true, - public: true, - }); - this.add('Layout_Privacy_Policy', 'Privacy Policy
Go to APP SETTINGS → Layout to customize this page.', { - type: 'code', - code: 'text/html', - multiline: true, - public: true, - }); - this.add('Layout_Legal_Notice', 'Legal Notice
Go to APP SETTINGS -> Layout to customize this page.', { - type: 'code', - code: 'text/html', - multiline: true, - public: true, - }); - return this.add('Layout_Sidenav_Footer', 'Home', { - type: 'code', - code: 'text/html', - public: true, - i18nDescription: 'Layout_Sidenav_Footer_description', - }); - }); - this.section('Custom_Scripts', function() { - this.add('Custom_Script_On_Logout', '//Add your script', { - type: 'code', - multiline: true, - public: true, - }); - this.add('Custom_Script_Logged_Out', '//Add your script', { - type: 'code', - multiline: true, - public: true, - }); - return this.add('Custom_Script_Logged_In', '//Add your script', { - type: 'code', - multiline: true, - public: true, - }); - }); - return this.section('User_Interface', function() { - this.add('UI_DisplayRoles', true, { - type: 'boolean', - public: true, - }); - this.add('UI_Group_Channels_By_Type', true, { - type: 'boolean', - public: false, - }); - this.add('UI_Use_Name_Avatar', false, { - type: 'boolean', - public: true, - }); - this.add('UI_Use_Real_Name', false, { - type: 'boolean', - public: true, - }); - this.add('UI_Click_Direct_Message', false, { - type: 'boolean', - public: true, - }); - - this.add('Number_of_users_autocomplete_suggestions', 5, { - type: 'int', - public: true, - }); - - this.add('UI_Unread_Counter_Style', 'Different_Style_For_User_Mentions', { - type: 'select', - values: [ - { - key: 'Same_Style_For_Mentions', - i18nLabel: 'Same_Style_For_Mentions', - }, { - key: 'Different_Style_For_User_Mentions', - i18nLabel: 'Different_Style_For_User_Mentions', - }, - ], - public: true, - }); - this.add('UI_Allow_room_names_with_special_chars', false, { - type: 'boolean', - public: true, - }); - return this.add('UI_Show_top_navbar_embedded_layout', false, { - type: 'boolean', - public: true, - }); - }); -}); - -settings.addGroup('Logs', function() { - this.add('Log_Level', '0', { - type: 'select', - values: [ - { - key: '0', - i18nLabel: '0_Errors_Only', - }, { - key: '1', - i18nLabel: '1_Errors_and_Information', - }, { - key: '2', - i18nLabel: '2_Erros_Information_and_Debug', - }, - ], - public: true, - }); - this.add('Log_Package', false, { - type: 'boolean', - public: true, - }); - this.add('Log_File', false, { - type: 'boolean', - public: true, - }); - this.add('Log_View_Limit', 1000, { - type: 'int', - }); - - this.add('Log_Trace_Methods', false, { - type: 'boolean', - }); - - this.add('Log_Trace_Methods_Filter', '', { - type: 'string', - enableQuery: { - _id: 'Log_Trace_Methods', - value: true, - }, - }); - - this.add('Log_Trace_Subscriptions', false, { - type: 'boolean', - }); - - this.add('Log_Trace_Subscriptions_Filter', '', { - type: 'string', - enableQuery: { - _id: 'Log_Trace_Subscriptions', - value: true, - }, - }); - - this.section('Prometheus', function() { - this.add('Prometheus_Enabled', false, { - type: 'boolean', - i18nLabel: 'Enabled', - }); - // See the default port allocation at https://github.com/prometheus/prometheus/wiki/Default-port-allocations - this.add('Prometheus_Port', 9458, { - type: 'string', - i18nLabel: 'Port', - }); - this.add('Prometheus_Reset_Interval', 0, { - type: 'int', - }); - this.add('Prometheus_Garbage_Collector', false, { - type: 'boolean', - alert: 'Prometheus_Garbage_Collector_Alert', - }); - this.add('Prometheus_API_User_Agent', false, { - type: 'boolean', - }); - }); -}); - -settings.addGroup('Setup_Wizard', function() { - this.section('Organization_Info', function() { - this.add('Organization_Type', '', { - type: 'select', - values: [ - { - key: 'community', - i18nLabel: 'Community', - }, - { - key: 'enterprise', - i18nLabel: 'Enterprise', - }, - { - key: 'government', - i18nLabel: 'Government', - }, - { - key: 'nonprofit', - i18nLabel: 'Nonprofit', - }, - ], - wizard: { - step: 2, - order: 0, - }, - }); - this.add('Organization_Name', '', { - type: 'string', - wizard: { - step: 2, - order: 1, - }, - }); - this.add('Industry', '', { - type: 'select', - values: [ - { - key: 'aerospaceDefense', - i18nLabel: 'Aerospace_and_Defense', - }, - { - key: 'blockchain', - i18nLabel: 'Blockchain', - }, - { - key: 'consulting', - i18nLabel: 'Consulting', - }, - { - key: 'consumerGoods', - i18nLabel: 'Consumer_Packaged_Goods', - }, - { - key: 'contactCenter', - i18nLabel: 'Contact_Center', - }, - { - key: 'education', - i18nLabel: 'Education', - }, - { - key: 'entertainment', - i18nLabel: 'Entertainment', - }, - { - key: 'financialServices', - i18nLabel: 'Financial_Services', - }, - { - key: 'gaming', - i18nLabel: 'Gaming', - }, - { - key: 'healthcare', - i18nLabel: 'Healthcare', - }, - { - key: 'hospitalityBusinness', - i18nLabel: 'Hospitality_Businness', - }, - { - key: 'insurance', - i18nLabel: 'Insurance', - }, - { - key: 'itSecurity', - i18nLabel: 'It_Security', - }, - { - key: 'logistics', - i18nLabel: 'Logistics', - }, - { - key: 'manufacturing', - i18nLabel: 'Manufacturing', - }, - { - key: 'media', - i18nLabel: 'Media', - }, - { - key: 'pharmaceutical', - i18nLabel: 'Pharmaceutical', - }, - { - key: 'realEstate', - i18nLabel: 'Real_Estate', - }, - { - key: 'religious', - i18nLabel: 'Religious', - }, - { - key: 'retail', - i18nLabel: 'Retail', - }, - { - key: 'socialNetwork', - i18nLabel: 'Social_Network', - }, - { - key: 'technologyProvider', - i18nLabel: 'Technology_Provider', - }, - { - key: 'technologyServices', - i18nLabel: 'Technology_Services', - }, - { - key: 'telecom', - i18nLabel: 'Telecom', - }, - { - key: 'utilities', - i18nLabel: 'Utilities', - }, - { - key: 'other', - i18nLabel: 'Other', - }, - ], - wizard: { - step: 2, - order: 2, - }, - }); - this.add('Size', '', { - type: 'select', - values: [ - { - key: '0', - i18nLabel: '1-10 people', - }, - { - key: '1', - i18nLabel: '11-50 people', - }, - { - key: '2', - i18nLabel: '51-100 people', - }, - { - key: '3', - i18nLabel: '101-250 people', - }, - { - key: '4', - i18nLabel: '251-500 people', - }, - { - key: '5', - i18nLabel: '501-1000 people', - }, - { - key: '6', - i18nLabel: '1001-4000 people', - }, - { - key: '7', - i18nLabel: '4000 or more people', - }, - ], - wizard: { - step: 2, - order: 3, - }, - }); - this.add('Country', '', { - type: 'select', - values: [ - { - key: 'afghanistan', - i18nLabel: 'Country_Afghanistan', - }, - { - key: 'albania', - i18nLabel: 'Country_Albania', - }, - { - key: 'algeria', - i18nLabel: 'Country_Algeria', - }, - { - key: 'americanSamoa', - i18nLabel: 'Country_American_Samoa', - }, - { - key: 'andorra', - i18nLabel: 'Country_Andorra', - }, - { - key: 'angola', - i18nLabel: 'Country_Angola', - }, - { - key: 'anguilla', - i18nLabel: 'Country_Anguilla', - }, - { - key: 'antarctica', - i18nLabel: 'Country_Antarctica', - }, - { - key: 'antiguaAndBarbuda', - i18nLabel: 'Country_Antigua_and_Barbuda', - }, - { - key: 'argentina', - i18nLabel: 'Country_Argentina', - }, - { - key: 'armenia', - i18nLabel: 'Country_Armenia', - }, - { - key: 'aruba', - i18nLabel: 'Country_Aruba', - }, - { - key: 'australia', - i18nLabel: 'Country_Australia', - }, - { - key: 'austria', - i18nLabel: 'Country_Austria', - }, - { - key: 'azerbaijan', - i18nLabel: 'Country_Azerbaijan', - }, - { - key: 'bahamas', - i18nLabel: 'Country_Bahamas', - }, - { - key: 'bahrain', - i18nLabel: 'Country_Bahrain', - }, - { - key: 'bangladesh', - i18nLabel: 'Country_Bangladesh', - }, - { - key: 'barbados', - i18nLabel: 'Country_Barbados', - }, - { - key: 'belarus', - i18nLabel: 'Country_Belarus', - }, - { - key: 'belgium', - i18nLabel: 'Country_Belgium', - }, - { - key: 'belize', - i18nLabel: 'Country_Belize', - }, - { - key: 'benin', - i18nLabel: 'Country_Benin', - }, - { - key: 'bermuda', - i18nLabel: 'Country_Bermuda', - }, - { - key: 'bhutan', - i18nLabel: 'Country_Bhutan', - }, - { - key: 'bolivia', - i18nLabel: 'Country_Bolivia', - }, - { - key: 'bosniaAndHerzegovina', - i18nLabel: 'Country_Bosnia_and_Herzegovina', - }, - { - key: 'botswana', - i18nLabel: 'Country_Botswana', - }, - { - key: 'bouvetIsland', - i18nLabel: 'Country_Bouvet_Island', - }, - { - key: 'brazil', - i18nLabel: 'Country_Brazil', - }, - { - key: 'britishIndianOceanTerritory', - i18nLabel: 'Country_British_Indian_Ocean_Territory', - }, - { - key: 'bruneiDarussalam', - i18nLabel: 'Country_Brunei_Darussalam', - }, - { - key: 'bulgaria', - i18nLabel: 'Country_Bulgaria', - }, - { - key: 'burkinaFaso', - i18nLabel: 'Country_Burkina_Faso', - }, - { - key: 'burundi', - i18nLabel: 'Country_Burundi', - }, - { - key: 'cambodia', - i18nLabel: 'Country_Cambodia', - }, - { - key: 'cameroon', - i18nLabel: 'Country_Cameroon', - }, - { - key: 'canada', - i18nLabel: 'Country_Canada', - }, - { - key: 'capeVerde', - i18nLabel: 'Country_Cape_Verde', - }, - { - key: 'caymanIslands', - i18nLabel: 'Country_Cayman_Islands', - }, - { - key: 'centralAfricanRepublic', - i18nLabel: 'Country_Central_African_Republic', - }, - { - key: 'chad', - i18nLabel: 'Country_Chad', - }, - { - key: 'chile', - i18nLabel: 'Country_Chile', - }, - { - key: 'china', - i18nLabel: 'Country_China', - }, - { - key: 'christmasIsland', - i18nLabel: 'Country_Christmas_Island', - }, - { - key: 'cocosKeelingIslands', - i18nLabel: 'Country_Cocos_Keeling_Islands', - }, - { - key: 'colombia', - i18nLabel: 'Country_Colombia', - }, - { - key: 'comoros', - i18nLabel: 'Country_Comoros', - }, - { - key: 'congo', - i18nLabel: 'Country_Congo', - }, - { - key: 'congoTheDemocraticRepublicOfThe', - i18nLabel: 'Country_Congo_The_Democratic_Republic_of_The', - }, - { - key: 'cookIslands', - i18nLabel: 'Country_Cook_Islands', - }, - { - key: 'costaRica', - i18nLabel: 'Country_Costa_Rica', - }, - { - key: 'coteDivoire', - i18nLabel: 'Country_Cote_Divoire', - }, - { - key: 'croatia', - i18nLabel: 'Country_Croatia', - }, - { - key: 'cuba', - i18nLabel: 'Country_Cuba', - }, - { - key: 'cyprus', - i18nLabel: 'Country_Cyprus', - }, - { - key: 'czechRepublic', - i18nLabel: 'Country_Czech_Republic', - }, - { - key: 'denmark', - i18nLabel: 'Country_Denmark', - }, - { - key: 'djibouti', - i18nLabel: 'Country_Djibouti', - }, - { - key: 'dominica', - i18nLabel: 'Country_Dominica', - }, - { - key: 'dominicanRepublic', - i18nLabel: 'Country_Dominican_Republic', - }, - { - key: 'ecuador', - i18nLabel: 'Country_Ecuador', - }, - { - key: 'egypt', - i18nLabel: 'Country_Egypt', - }, - { - key: 'elSalvador', - i18nLabel: 'Country_El_Salvador', - }, - { - key: 'equatorialGuinea', - i18nLabel: 'Country_Equatorial_Guinea', - }, - { - key: 'eritrea', - i18nLabel: 'Country_Eritrea', - }, - { - key: 'estonia', - i18nLabel: 'Country_Estonia', - }, - { - key: 'ethiopia', - i18nLabel: 'Country_Ethiopia', - }, - { - key: 'falklandIslandsMalvinas', - i18nLabel: 'Country_Falkland_Islands_Malvinas', - }, - { - key: 'faroeIslands', - i18nLabel: 'Country_Faroe_Islands', - }, - { - key: 'fiji', - i18nLabel: 'Country_Fiji', - }, - { - key: 'finland', - i18nLabel: 'Country_Finland', - }, - { - key: 'france', - i18nLabel: 'Country_France', - }, - { - key: 'frenchGuiana', - i18nLabel: 'Country_French_Guiana', - }, - { - key: 'frenchPolynesia', - i18nLabel: 'Country_French_Polynesia', - }, - { - key: 'frenchSouthernTerritories', - i18nLabel: 'Country_French_Southern_Territories', - }, - { - key: 'gabon', - i18nLabel: 'Country_Gabon', - }, - { - key: 'gambia', - i18nLabel: 'Country_Gambia', - }, - { - key: 'georgia', - i18nLabel: 'Country_Georgia', - }, - { - key: 'germany', - i18nLabel: 'Country_Germany', - }, - { - key: 'ghana', - i18nLabel: 'Country_Ghana', - }, - { - key: 'gibraltar', - i18nLabel: 'Country_Gibraltar', - }, - { - key: 'greece', - i18nLabel: 'Country_Greece', - }, - { - key: 'greenland', - i18nLabel: 'Country_Greenland', - }, - { - key: 'grenada', - i18nLabel: 'Country_Grenada', - }, - { - key: 'guadeloupe', - i18nLabel: 'Country_Guadeloupe', - }, - { - key: 'guam', - i18nLabel: 'Country_Guam', - }, - { - key: 'guatemala', - i18nLabel: 'Country_Guatemala', - }, - { - key: 'guinea', - i18nLabel: 'Country_Guinea', - }, - { - key: 'guineaBissau', - i18nLabel: 'Country_Guinea_bissau', - }, - { - key: 'guyana', - i18nLabel: 'Country_Guyana', - }, - { - key: 'haiti', - i18nLabel: 'Country_Haiti', - }, - { - key: 'heardIslandAndMcdonaldIslands', - i18nLabel: 'Country_Heard_Island_and_Mcdonald_Islands', - }, - { - key: 'holySeeVaticanCityState', - i18nLabel: 'Country_Holy_See_Vatican_City_State', - }, - { - key: 'honduras', - i18nLabel: 'Country_Honduras', - }, - { - key: 'hongKong', - i18nLabel: 'Country_Hong_Kong', - }, - { - key: 'hungary', - i18nLabel: 'Country_Hungary', - }, - { - key: 'iceland', - i18nLabel: 'Country_Iceland', - }, - { - key: 'india', - i18nLabel: 'Country_India', - }, - { - key: 'indonesia', - i18nLabel: 'Country_Indonesia', - }, - { - key: 'iranIslamicRepublicOf', - i18nLabel: 'Country_Iran_Islamic_Republic_of', - }, - { - key: 'iraq', - i18nLabel: 'Country_Iraq', - }, - { - key: 'ireland', - i18nLabel: 'Country_Ireland', - }, - { - key: 'israel', - i18nLabel: 'Country_Israel', - }, - { - key: 'italy', - i18nLabel: 'Country_Italy', - }, - { - key: 'jamaica', - i18nLabel: 'Country_Jamaica', - }, - { - key: 'japan', - i18nLabel: 'Country_Japan', - }, - { - key: 'jordan', - i18nLabel: 'Country_Jordan', - }, - { - key: 'kazakhstan', - i18nLabel: 'Country_Kazakhstan', - }, - { - key: 'kenya', - i18nLabel: 'Country_Kenya', - }, - { - key: 'kiribati', - i18nLabel: 'Country_Kiribati', - }, - { - key: 'koreaDemocraticPeoplesRepublicOf', - i18nLabel: 'Country_Korea_Democratic_Peoples_Republic_of', - }, - { - key: 'koreaRepublicOf', - i18nLabel: 'Country_Korea_Republic_of', - }, - { - key: 'kuwait', - i18nLabel: 'Country_Kuwait', - }, - { - key: 'kyrgyzstan', - i18nLabel: 'Country_Kyrgyzstan', - }, - { - key: 'laoPeoplesDemocraticRepublic', - i18nLabel: 'Country_Lao_Peoples_Democratic_Republic', - }, - { - key: 'latvia', - i18nLabel: 'Country_Latvia', - }, - { - key: 'lebanon', - i18nLabel: 'Country_Lebanon', - }, - { - key: 'lesotho', - i18nLabel: 'Country_Lesotho', - }, - { - key: 'liberia', - i18nLabel: 'Country_Liberia', - }, - { - key: 'libyanArabJamahiriya', - i18nLabel: 'Country_Libyan_Arab_Jamahiriya', - }, - { - key: 'liechtenstein', - i18nLabel: 'Country_Liechtenstein', - }, - { - key: 'lithuania', - i18nLabel: 'Country_Lithuania', - }, - { - key: 'luxembourg', - i18nLabel: 'Country_Luxembourg', - }, - { - key: 'macao', - i18nLabel: 'Country_Macao', - }, - { - key: 'macedoniaTheFormerYugoslavRepublicOf', - i18nLabel: 'Country_Macedonia_The_Former_Yugoslav_Republic_of', - }, - { - key: 'madagascar', - i18nLabel: 'Country_Madagascar', - }, - { - key: 'malawi', - i18nLabel: 'Country_Malawi', - }, - { - key: 'malaysia', - i18nLabel: 'Country_Malaysia', - }, - { - key: 'maldives', - i18nLabel: 'Country_Maldives', - }, - { - key: 'mali', - i18nLabel: 'Country_Mali', - }, - { - key: 'malta', - i18nLabel: 'Country_Malta', - }, - { - key: 'marshallIslands', - i18nLabel: 'Country_Marshall_Islands', - }, - { - key: 'martinique', - i18nLabel: 'Country_Martinique', - }, - { - key: 'mauritania', - i18nLabel: 'Country_Mauritania', - }, - { - key: 'mauritius', - i18nLabel: 'Country_Mauritius', - }, - { - key: 'mayotte', - i18nLabel: 'Country_Mayotte', - }, - { - key: 'mexico', - i18nLabel: 'Country_Mexico', - }, - { - key: 'micronesiaFederatedStatesOf', - i18nLabel: 'Country_Micronesia_Federated_States_of', - }, - { - key: 'moldovaRepublicOf', - i18nLabel: 'Country_Moldova_Republic_of', - }, - { - key: 'monaco', - i18nLabel: 'Country_Monaco', - }, - { - key: 'mongolia', - i18nLabel: 'Country_Mongolia', - }, - { - key: 'montserrat', - i18nLabel: 'Country_Montserrat', - }, - { - key: 'morocco', - i18nLabel: 'Country_Morocco', - }, - { - key: 'mozambique', - i18nLabel: 'Country_Mozambique', - }, - { - key: 'myanmar', - i18nLabel: 'Country_Myanmar', - }, - { - key: 'namibia', - i18nLabel: 'Country_Namibia', - }, - { - key: 'nauru', - i18nLabel: 'Country_Nauru', - }, - { - key: 'nepal', - i18nLabel: 'Country_Nepal', - }, - { - key: 'netherlands', - i18nLabel: 'Country_Netherlands', - }, - { - key: 'netherlandsAntilles', - i18nLabel: 'Country_Netherlands_Antilles', - }, - { - key: 'newCaledonia', - i18nLabel: 'Country_New_Caledonia', - }, - { - key: 'newZealand', - i18nLabel: 'Country_New_Zealand', - }, - { - key: 'nicaragua', - i18nLabel: 'Country_Nicaragua', - }, - { - key: 'niger', - i18nLabel: 'Country_Niger', - }, - { - key: 'nigeria', - i18nLabel: 'Country_Nigeria', - }, - { - key: 'niue', - i18nLabel: 'Country_Niue', - }, - { - key: 'norfolkIsland', - i18nLabel: 'Country_Norfolk_Island', - }, - { - key: 'northernMarianaIslands', - i18nLabel: 'Country_Northern_Mariana_Islands', - }, - { - key: 'norway', - i18nLabel: 'Country_Norway', - }, - { - key: 'oman', - i18nLabel: 'Country_Oman', - }, - { - key: 'pakistan', - i18nLabel: 'Country_Pakistan', - }, - { - key: 'palau', - i18nLabel: 'Country_Palau', - }, - { - key: 'palestinianTerritoryOccupied', - i18nLabel: 'Country_Palestinian_Territory_Occupied', - }, - { - key: 'panama', - i18nLabel: 'Country_Panama', - }, - { - key: 'papuaNewGuinea', - i18nLabel: 'Country_Papua_New_Guinea', - }, - { - key: 'paraguay', - i18nLabel: 'Country_Paraguay', - }, - { - key: 'peru', - i18nLabel: 'Country_Peru', - }, - { - key: 'philippines', - i18nLabel: 'Country_Philippines', - }, - { - key: 'pitcairn', - i18nLabel: 'Country_Pitcairn', - }, - { - key: 'poland', - i18nLabel: 'Country_Poland', - }, - { - key: 'portugal', - i18nLabel: 'Country_Portugal', - }, - { - key: 'puertoRico', - i18nLabel: 'Country_Puerto_Rico', - }, - { - key: 'qatar', - i18nLabel: 'Country_Qatar', - }, - { - key: 'reunion', - i18nLabel: 'Country_Reunion', - }, - { - key: 'romania', - i18nLabel: 'Country_Romania', - }, - { - key: 'russianFederation', - i18nLabel: 'Country_Russian_Federation', - }, - { - key: 'rwanda', - i18nLabel: 'Country_Rwanda', - }, - { - key: 'saintHelena', - i18nLabel: 'Country_Saint_Helena', - }, - { - key: 'saintKittsAndNevis', - i18nLabel: 'Country_Saint_Kitts_and_Nevis', - }, - { - key: 'saintLucia', - i18nLabel: 'Country_Saint_Lucia', - }, - { - key: 'saintPierreAndMiquelon', - i18nLabel: 'Country_Saint_Pierre_and_Miquelon', - }, - { - key: 'saintVincentAndTheGrenadines', - i18nLabel: 'Country_Saint_Vincent_and_The_Grenadines', - }, - { - key: 'samoa', - i18nLabel: 'Country_Samoa', - }, - { - key: 'sanMarino', - i18nLabel: 'Country_San_Marino', - }, - { - key: 'saoTomeAndPrincipe', - i18nLabel: 'Country_Sao_Tome_and_Principe', - }, - { - key: 'saudiArabia', - i18nLabel: 'Country_Saudi_Arabia', - }, - { - key: 'senegal', - i18nLabel: 'Country_Senegal', - }, - { - key: 'serbiaAndMontenegro', - i18nLabel: 'Country_Serbia_and_Montenegro', - }, - { - key: 'seychelles', - i18nLabel: 'Country_Seychelles', - }, - { - key: 'sierraLeone', - i18nLabel: 'Country_Sierra_Leone', - }, - { - key: 'singapore', - i18nLabel: 'Country_Singapore', - }, - { - key: 'slovakia', - i18nLabel: 'Country_Slovakia', - }, - { - key: 'slovenia', - i18nLabel: 'Country_Slovenia', - }, - { - key: 'solomonIslands', - i18nLabel: 'Country_Solomon_Islands', - }, - { - key: 'somalia', - i18nLabel: 'Country_Somalia', - }, - { - key: 'southAfrica', - i18nLabel: 'Country_South_Africa', - }, - { - key: 'southGeorgiaAndTheSouthSandwichIslands', - i18nLabel: 'Country_South_Georgia_and_The_South_Sandwich_Islands', - }, - { - key: 'spain', - i18nLabel: 'Country_Spain', - }, - { - key: 'sriLanka', - i18nLabel: 'Country_Sri_Lanka', - }, - { - key: 'sudan', - i18nLabel: 'Country_Sudan', - }, - { - key: 'suriname', - i18nLabel: 'Country_Suriname', - }, - { - key: 'svalbardAndJanMayen', - i18nLabel: 'Country_Svalbard_and_Jan_Mayen', - }, - { - key: 'swaziland', - i18nLabel: 'Country_Swaziland', - }, - { - key: 'sweden', - i18nLabel: 'Country_Sweden', - }, - { - key: 'switzerland', - i18nLabel: 'Country_Switzerland', - }, - { - key: 'syrianArabRepublic', - i18nLabel: 'Country_Syrian_Arab_Republic', - }, - { - key: 'taiwanProvinceOfChina', - i18nLabel: 'Country_Taiwan_Province_of_China', - }, - { - key: 'tajikistan', - i18nLabel: 'Country_Tajikistan', - }, - { - key: 'tanzaniaUnitedRepublicOf', - i18nLabel: 'Country_Tanzania_United_Republic_of', - }, - { - key: 'thailand', - i18nLabel: 'Country_Thailand', - }, - { - key: 'timorLeste', - i18nLabel: 'Country_Timor_leste', - }, - { - key: 'togo', - i18nLabel: 'Country_Togo', - }, - { - key: 'tokelau', - i18nLabel: 'Country_Tokelau', - }, - { - key: 'tonga', - i18nLabel: 'Country_Tonga', - }, - { - key: 'trinidadAndTobago', - i18nLabel: 'Country_Trinidad_and_Tobago', - }, - { - key: 'tunisia', - i18nLabel: 'Country_Tunisia', - }, - { - key: 'turkey', - i18nLabel: 'Country_Turkey', - }, - { - key: 'turkmenistan', - i18nLabel: 'Country_Turkmenistan', - }, - { - key: 'turksAndCaicosIslands', - i18nLabel: 'Country_Turks_and_Caicos_Islands', - }, - { - key: 'tuvalu', - i18nLabel: 'Country_Tuvalu', - }, - { - key: 'uganda', - i18nLabel: 'Country_Uganda', - }, - { - key: 'ukraine', - i18nLabel: 'Country_Ukraine', - }, - { - key: 'unitedArabEmirates', - i18nLabel: 'Country_United_Arab_Emirates', - }, - { - key: 'unitedKingdom', - i18nLabel: 'Country_United_Kingdom', - }, - { - key: 'unitedStates', - i18nLabel: 'Country_United_States', - }, - { - key: 'unitedStatesMinorOutlyingIslands', - i18nLabel: 'Country_United_States_Minor_Outlying_Islands', - }, - { - key: 'uruguay', - i18nLabel: 'Country_Uruguay', - }, - { - key: 'uzbekistan', - i18nLabel: 'Country_Uzbekistan', - }, - { - key: 'vanuatu', - i18nLabel: 'Country_Vanuatu', - }, - { - key: 'venezuela', - i18nLabel: 'Country_Venezuela', - }, - { - key: 'vietNam', - i18nLabel: 'Country_Viet_Nam', - }, - { - key: 'virginIslandsBritish', - i18nLabel: 'Country_Virgin_Islands_British', - }, - { - key: 'virginIslandsUS', - i18nLabel: 'Country_Virgin_Islands_US', - }, - { - key: 'wallisAndFutuna', - i18nLabel: 'Country_Wallis_and_Futuna', - }, - { - key: 'westernSahara', - i18nLabel: 'Country_Western_Sahara', - }, - { - key: 'yemen', - i18nLabel: 'Country_Yemen', - }, - { - key: 'zambia', - i18nLabel: 'Country_Zambia', - }, - { - key: 'zimbabwe', - i18nLabel: 'Country_Zimbabwe', - }, - { - key: 'worldwide', - i18nLabel: 'Worldwide', - }, - ], - wizard: { - step: 2, - order: 4, - }, - }); - this.add('Website', '', { - type: 'string', - wizard: { - step: 2, - order: 5, - }, - }); - this.add('Server_Type', '', { - type: 'select', - values: [ - { - key: 'privateTeam', - i18nLabel: 'Private_Team', - }, - { - key: 'publicCommunity', - i18nLabel: 'Public_Community', - }, - ], - wizard: { - step: 3, - order: 2, - }, - }); - this.add('Allow_Marketing_Emails', true, { - type: 'boolean', - }); - this.add('Register_Server', false, { - type: 'boolean', - }); - this.add('Organization_Email', '', { - type: 'string', - }); - }); - - this.section('Cloud_Info', function() { - this.add('Nps_Url', 'https://nps.rocket.chat', { - type: 'string', - hidden: true, - readonly: true, - enableQuery: { - _id: 'Register_Server', - value: true, - }, - secret: true, - }); - - this.add('Cloud_Url', 'https://cloud.rocket.chat', { - type: 'string', - hidden: true, - readonly: true, - enableQuery: { - _id: 'Register_Server', - value: true, - }, - secret: true, - }); - - this.add('Cloud_Service_Agree_PrivacyTerms', false, { - type: 'boolean', - }); - - this.add('Cloud_Workspace_Id', '', { - type: 'string', - hidden: true, - readonly: true, - enableQuery: { - _id: 'Register_Server', - value: true, - }, - secret: true, - }); - - this.add('Cloud_Workspace_Name', '', { - type: 'string', - hidden: true, - readonly: true, - enableQuery: { - _id: 'Register_Server', - value: true, - }, - secret: true, - }); - - this.add('Cloud_Workspace_Client_Id', '', { - type: 'string', - hidden: true, - readonly: true, - enableQuery: { - _id: 'Register_Server', - value: true, - }, - secret: true, - }); - - this.add('Cloud_Workspace_Client_Secret', '', { - type: 'string', - hidden: true, - readonly: true, - enableQuery: { - _id: 'Register_Server', - value: true, - }, - secret: true, - }); - - this.add('Cloud_Workspace_Client_Secret_Expires_At', '', { - type: 'int', - hidden: true, - readonly: true, - enableQuery: { - _id: 'Register_Server', - value: true, - }, - secret: true, - }); - - this.add('Cloud_Workspace_Registration_Client_Uri', '', { - type: 'string', - hidden: true, - readonly: true, - enableQuery: { - _id: 'Register_Server', - value: true, - }, - secret: true, - }); - - this.add('Cloud_Workspace_PublicKey', '', { - type: 'string', - hidden: true, - readonly: true, - enableQuery: { - _id: 'Register_Server', - value: true, - }, - secret: true, - }); - - this.add('Cloud_Workspace_License', '', { - type: 'string', - hidden: true, - readonly: true, - enableQuery: { - _id: 'Register_Server', - value: true, - }, - secret: true, - }); - - this.add('Cloud_Workspace_Access_Token', '', { - type: 'string', - hidden: true, - readonly: true, - enableQuery: { - _id: 'Register_Server', - value: true, - }, - secret: true, - }); - - this.add('Cloud_Workspace_Access_Token_Expires_At', new Date(0), { - type: 'date', - hidden: true, - readonly: true, - enableQuery: { - _id: 'Register_Server', - value: true, - }, - secret: true, - }); - - this.add('Cloud_Workspace_Registration_State', '', { - type: 'string', - hidden: true, - readonly: true, - enableQuery: { - _id: 'Register_Server', - value: true, - }, - secret: true, - }); - }); -}); - -settings.addGroup('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 } }); - - this.add('DDP_Rate_Limit_User_Enabled', true, { type: 'boolean' }); - this.add('DDP_Rate_Limit_User_Requests_Allowed', 1200, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_User_Enabled', value: true } }); - this.add('DDP_Rate_Limit_User_Interval_Time', 60000, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_User_Enabled', value: true } }); - - this.add('DDP_Rate_Limit_Connection_Enabled', true, { type: 'boolean' }); - this.add('DDP_Rate_Limit_Connection_Requests_Allowed', 600, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_Connection_Enabled', value: true } }); - this.add('DDP_Rate_Limit_Connection_Interval_Time', 60000, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_Connection_Enabled', value: true } }); - - this.add('DDP_Rate_Limit_User_By_Method_Enabled', true, { type: 'boolean' }); - this.add('DDP_Rate_Limit_User_By_Method_Requests_Allowed', 20, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_User_By_Method_Enabled', value: true } }); - this.add('DDP_Rate_Limit_User_By_Method_Interval_Time', 10000, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_User_By_Method_Enabled', value: true } }); - - this.add('DDP_Rate_Limit_Connection_By_Method_Enabled', true, { type: 'boolean' }); - this.add('DDP_Rate_Limit_Connection_By_Method_Requests_Allowed', 10, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_Connection_By_Method_Enabled', value: true } }); - 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.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 } }); - }); -}); - -settings.addGroup('Troubleshoot', function() { - this.add('Troubleshoot_Disable_Notifications', false, { - type: 'boolean', - alert: 'Troubleshoot_Disable_Notifications_Alert', - }); - this.add('Troubleshoot_Disable_Presence_Broadcast', false, { - type: 'boolean', - alert: 'Troubleshoot_Disable_Presence_Broadcast_Alert', - }); - this.add('Troubleshoot_Disable_Instance_Broadcast', false, { - type: 'boolean', - alert: 'Troubleshoot_Disable_Instance_Broadcast_Alert', - }); - this.add('Troubleshoot_Disable_Sessions_Monitor', false, { - type: 'boolean', - alert: 'Troubleshoot_Disable_Sessions_Monitor_Alert', - }); - this.add('Troubleshoot_Disable_Livechat_Activity_Monitor', false, { - type: 'boolean', - alert: 'Troubleshoot_Disable_Livechat_Activity_Monitor_Alert', - }); - this.add('Troubleshoot_Disable_Statistics_Generator', false, { - type: 'boolean', - alert: 'Troubleshoot_Disable_Statistics_Generator_Alert', - }); - this.add('Troubleshoot_Disable_Data_Exporter_Processor', false, { - type: 'boolean', - alert: 'Troubleshoot_Disable_Data_Exporter_Processor_Alert', - }); - this.add('Troubleshoot_Disable_Workspace_Sync', false, { - type: 'boolean', - alert: 'Troubleshoot_Disable_Workspace_Sync_Alert', - }); -}); - -settings.init(); diff --git a/app/lib/server/startup/settings.ts b/app/lib/server/startup/settings.ts new file mode 100644 index 0000000000000..1562634a24ee9 --- /dev/null +++ b/app/lib/server/startup/settings.ts @@ -0,0 +1,3043 @@ +import { Random } from 'meteor/random'; + +import { settingsRegistry } from '../../../settings/server'; +import './email'; +import { MessageTypesValues } from '../../lib/MessageTypes'; + +// Insert server unique id if it doesn't exist +settingsRegistry.add('uniqueID', process.env.DEPLOYMENT_ID || Random.id(), { + public: true, +}); + +// When you define a setting and want to add a description, you don't need to automatically define the i18nDescription +// if you add a node to the i18n.json with the same setting name but with `_Description` it will automatically work. + + +settingsRegistry.addGroup('Accounts', function() { + this.add('Accounts_AllowAnonymousRead', false, { + type: 'boolean', + public: true, + }); + this.add('Accounts_AllowAnonymousWrite', false, { + type: 'boolean', + public: true, + enableQuery: { + _id: 'Accounts_AllowAnonymousRead', + value: true, + }, + }); + this.add('Accounts_AllowDeleteOwnAccount', false, { + type: 'boolean', + public: true, + enableQuery: { + _id: 'Accounts_AllowUserProfileChange', + value: true, + }, + }); + this.add('Accounts_AllowUserProfileChange', true, { + type: 'boolean', + public: true, + }); + this.add('Accounts_AllowUserAvatarChange', true, { + type: 'boolean', + public: true, + }); + this.add('Accounts_AllowRealNameChange', true, { + type: 'boolean', + public: true, + }); + this.add('Accounts_AllowUserStatusMessageChange', true, { + type: 'boolean', + public: true, + }); + this.add('Accounts_AllowUsernameChange', true, { + type: 'boolean', + public: true, + }); + this.add('Accounts_AllowEmailChange', true, { + type: 'boolean', + public: true, + }); + this.add('Accounts_AllowPasswordChange', true, { + type: 'boolean', + public: true, + }); + this.add('Accounts_AllowPasswordChangeForOAuthUsers', true, { + type: 'boolean', + public: true, + }); + this.add('Accounts_AllowEmailNotifications', true, { + type: 'boolean', + public: true, + }); + this.add('Accounts_CustomFieldsToShowInUserInfo', '', { + type: 'string', + public: true, + }); + this.add('Accounts_LoginExpiration', 90, { + type: 'int', + public: true, + }); + this.add('Accounts_ShowFormLogin', true, { + type: 'boolean', + public: true, + }); + this.add('Accounts_EmailOrUsernamePlaceholder', '', { + type: 'string', + public: true, + i18nLabel: 'Placeholder_for_email_or_username_login_field', + }); + this.add('Accounts_PasswordPlaceholder', '', { + type: 'string', + public: true, + i18nLabel: 'Placeholder_for_password_login_field', + }); + + this.add('Accounts_ConfirmPasswordPlaceholder', '', { + type: 'string', + public: true, + i18nLabel: 'Placeholder_for_password_login_confirm_field', + }); + this.add('Accounts_ForgetUserSessionOnWindowClose', false, { + type: 'boolean', + public: true, + }); + this.add('Accounts_SearchFields', 'username, name, bio, nickname', { + type: 'string', + }); + this.add('Accounts_Directory_DefaultView', 'channels', { + type: 'select', + values: [ + { + key: 'channels', + i18nLabel: 'Channels', + }, + { + key: 'users', + i18nLabel: 'Users', + }, + ], + public: true, + }); + this.add('Accounts_AllowInvisibleStatusOption', true, { + type: 'boolean', + public: true, + i18nLabel: 'Accounts_AllowInvisibleStatusOption', + }); + + this.section('Registration', function() { + this.add('Accounts_Send_Email_When_Activating', true, { + type: 'boolean', + }); + this.add('Accounts_Send_Email_When_Deactivating', true, { + type: 'boolean', + }); + this.add('Accounts_DefaultUsernamePrefixSuggestion', 'user', { + type: 'string', + }); + this.add('Accounts_RequireNameForSignUp', true, { // TODO rename to Accounts_RequireFullName + type: 'boolean', + public: true, + }); + this.add('Accounts_RequirePasswordConfirmation', true, { + type: 'boolean', + public: true, + }); + this.add('Accounts_EmailVerification', false, { + type: 'boolean', + public: true, + enableQuery: { + _id: 'SMTP_Host', + value: { + $exists: true, + $ne: '', + }, + }, + }); + this.add('Accounts_Verify_Email_For_External_Accounts', true, { + type: 'boolean', + }); + this.add('Accounts_ManuallyApproveNewUsers', false, { + public: true, + type: 'boolean', + }); + this.add('Accounts_AllowedDomainsList', '', { + type: 'string', + public: true, + }); + this.add('Accounts_BlockedDomainsList', '', { + type: 'string', + }); + this.add('Accounts_BlockedUsernameList', '', { + type: 'string', + }); + this.add('Accounts_SystemBlockedUsernameList', 'admin,administrator,system,user', { + type: 'string', + hidden: true, + }); + this.add('Accounts_UseDefaultBlockedDomainsList', true, { + type: 'boolean', + }); + this.add('Accounts_UseDNSDomainCheck', false, { + type: 'boolean', + }); + this.add('Accounts_RegistrationForm', 'Public', { + type: 'select', + public: true, + values: [ + { + key: 'Public', + i18nLabel: 'Accounts_RegistrationForm_Public', + }, { + key: 'Disabled', + i18nLabel: 'Accounts_RegistrationForm_Disabled', + }, { + key: 'Secret URL', + i18nLabel: 'Accounts_RegistrationForm_Secret_URL', + }, + ], + }); + this.add('Accounts_RegistrationForm_SecretURL', Random.id(), { + type: 'string', + secret: true, + }); + this.add('Accounts_Registration_InviteUrlType', 'proxy', { + type: 'select', + values: [ + { + key: 'direct', + i18nLabel: 'Accounts_Registration_InviteUrlType_Direct', + }, { + key: 'proxy', + i18nLabel: 'Accounts_Registration_InviteUrlType_Proxy', + }, + ], + }); + + this.add('Accounts_RegistrationForm_LinkReplacementText', 'New user registration is currently disabled', { + type: 'string', + public: true, + }); + this.add('Accounts_Registration_AuthenticationServices_Enabled', true, { + type: 'boolean', + public: true, + }); + this.add('Accounts_Registration_AuthenticationServices_Default_Roles', 'user', { + type: 'string', + enableQuery: { + _id: 'Accounts_Registration_AuthenticationServices_Enabled', + value: true, + }, + }); + this.add('Accounts_Registration_Users_Default_Roles', 'user', { + type: 'string', + }); + this.add('Accounts_PasswordReset', true, { + type: 'boolean', + public: true, + }); + this.add('Accounts_CustomFields', '', { + type: 'code', + public: true, + i18nLabel: 'Custom_Fields', + }); + }); + + this.section('Accounts_Default_User_Preferences', function() { + this.add('Accounts_Default_User_Preferences_enableAutoAway', true, { + type: 'boolean', + public: true, + i18nLabel: 'Enable_Auto_Away', + }); + this.add('Accounts_Default_User_Preferences_idleTimeLimit', 300, { + type: 'int', + public: true, + i18nLabel: 'Idle_Time_Limit', + }); + this.add('Accounts_Default_User_Preferences_desktopNotificationRequireInteraction', false, { + type: 'boolean', + public: true, + i18nLabel: 'Notification_RequireInteraction', + i18nDescription: 'Notification_RequireInteraction_Description', + }); + this.add('Accounts_Default_User_Preferences_desktopNotifications', 'all', { + type: 'select', + values: [ + { + key: 'all', + i18nLabel: 'All_messages', + }, + { + key: 'mentions', + i18nLabel: 'Mentions', + }, + { + key: 'nothing', + i18nLabel: 'Nothing', + }, + ], + public: true, + }); + this.add('Accounts_Default_User_Preferences_pushNotifications', 'all', { + type: 'select', + values: [ + { + key: 'all', + i18nLabel: 'All_messages', + }, + { + key: 'mentions', + i18nLabel: 'Mentions', + }, + { + key: 'nothing', + i18nLabel: 'Nothing', + }, + ], + public: true, + }); + this.add('Accounts_Default_User_Preferences_unreadAlert', true, { + type: 'boolean', + public: true, + i18nLabel: 'Unread_Tray_Icon_Alert', + }); + this.add('Accounts_Default_User_Preferences_useEmojis', true, { + type: 'boolean', + public: true, + i18nLabel: 'Use_Emojis', + }); + this.add('Accounts_Default_User_Preferences_convertAsciiEmoji', true, { + type: 'boolean', + public: true, + i18nLabel: 'Convert_Ascii_Emojis', + }); + this.add('Accounts_Default_User_Preferences_autoImageLoad', true, { + type: 'boolean', + public: true, + i18nLabel: 'Auto_Load_Images', + }); + this.add('Accounts_Default_User_Preferences_saveMobileBandwidth', true, { + type: 'boolean', + public: true, + i18nLabel: 'Save_Mobile_Bandwidth', + }); + this.add('Accounts_Default_User_Preferences_collapseMediaByDefault', false, { + type: 'boolean', + public: true, + i18nLabel: 'Collapse_Embedded_Media_By_Default', + }); + this.add('Accounts_Default_User_Preferences_hideUsernames', false, { + type: 'boolean', + public: true, + i18nLabel: 'Hide_usernames', + }); + this.add('Accounts_Default_User_Preferences_hideRoles', false, { + type: 'boolean', + public: true, + i18nLabel: 'Hide_roles', + }); + this.add('Accounts_Default_User_Preferences_hideFlexTab', false, { + type: 'boolean', + public: true, + i18nLabel: 'Hide_flextab', + }); + this.add('Accounts_Default_User_Preferences_displayAvatars', true, { + type: 'boolean', + public: true, + i18nLabel: 'Display_avatars', + }); + this.add('Accounts_Default_User_Preferences_sidebarGroupByType', true, { + type: 'boolean', + public: true, + i18nLabel: 'Group_by_Type', + }); + this.add('Accounts_Default_User_Preferences_sidebarViewMode', 'medium', { + type: 'select', + values: [ + { + key: 'extended', + i18nLabel: 'Extended', + }, + { + key: 'medium', + i18nLabel: 'Medium', + }, + { + key: 'condensed', + i18nLabel: 'Condensed', + }, + ], + public: true, + i18nLabel: 'Sidebar_list_mode', + }); + this.add('Accounts_Default_User_Preferences_sidebarDisplayAvatar', true, { + type: 'boolean', + public: true, + i18nLabel: 'Display_Avatars_Sidebar', + }); + + this.add('Accounts_Default_User_Preferences_sidebarShowUnread', false, { + type: 'boolean', + public: true, + i18nLabel: 'Unread_on_top', + }); + + this.add('Accounts_Default_User_Preferences_sidebarSortby', 'activity', { + type: 'select', + values: [ + { + key: 'activity', + i18nLabel: 'Activity', + }, + { + key: 'alphabetical', + i18nLabel: 'Alphabetical', + }, + ], + public: true, + i18nLabel: 'Sort_By', + }); + + this.add('Accounts_Default_User_Preferences_showMessageInMainThread', false, { + type: 'boolean', + public: true, + i18nLabel: 'Show_Message_In_Main_Thread', + }); + + this.add('Accounts_Default_User_Preferences_sidebarShowFavorites', true, { + type: 'boolean', + public: true, + i18nLabel: 'Group_favorites', + }); + + this.add('Accounts_Default_User_Preferences_sendOnEnter', 'normal', { + type: 'select', + values: [ + { + key: 'normal', + i18nLabel: 'Enter_Normal', + }, + { + key: 'alternative', + i18nLabel: 'Enter_Alternative', + }, + { + key: 'desktop', + i18nLabel: 'Only_On_Desktop', + }, + ], + public: true, + i18nLabel: 'Enter_Behaviour', + }); + this.add('Accounts_Default_User_Preferences_messageViewMode', 0, { + type: 'select', + values: [ + { + key: 0, + i18nLabel: 'Normal', + }, + { + key: 1, + i18nLabel: 'Cozy', + }, + { + key: 2, + i18nLabel: 'Compact', + }, + ], + public: true, + i18nLabel: 'MessageBox_view_mode', + }); + this.add('Accounts_Default_User_Preferences_emailNotificationMode', 'mentions', { + type: 'select', + values: [ + { + key: 'nothing', + i18nLabel: 'Email_Notification_Mode_Disabled', + }, + { + key: 'mentions', + i18nLabel: 'Email_Notification_Mode_All', + }, + ], + public: true, + i18nLabel: 'Email_Notification_Mode', + }); + this.add('Accounts_Default_User_Preferences_newRoomNotification', 'door', { + type: 'select', + values: [ + { + key: 'none', + i18nLabel: 'None', + }, + { + key: 'door', + i18nLabel: 'Default', + }, + ], + public: true, + i18nLabel: 'New_Room_Notification', + }); + this.add('Accounts_Default_User_Preferences_newMessageNotification', 'chime', { + type: 'select', + values: [ + { + key: 'none', + i18nLabel: 'None', + }, + { + key: 'chime', + i18nLabel: 'Default', + }, + ], + public: true, + i18nLabel: 'New_Message_Notification', + }); + + this.add('Accounts_Default_User_Preferences_muteFocusedConversations', true, { + type: 'boolean', + public: true, + i18nLabel: 'Mute_Focused_Conversations', + }); + + this.add('Accounts_Default_User_Preferences_notificationsSoundVolume', 100, { + type: 'int', + public: true, + i18nLabel: 'Notifications_Sound_Volume', + }); + + this.add('Accounts_Default_User_Preferences_enableMessageParserEarlyAdoption', false, { + type: 'boolean', + public: true, + i18nLabel: 'Enable_message_parser_early_adoption', + alert: 'Enable_message_parser_early_adoption_alert', + }); + }); + + this.section('Avatar', function() { + this.add('Accounts_AvatarResize', true, { + type: 'boolean', + }); + this.add('Accounts_AvatarSize', 200, { + type: 'int', + enableQuery: { + _id: 'Accounts_AvatarResize', + value: true, + }, + }); + + this.add('Accounts_AvatarExternalProviderUrl', '', { + type: 'string', + public: true, + }); + + this.add('Accounts_RoomAvatarExternalProviderUrl', '', { + type: 'string', + public: true, + }); + + this.add('Accounts_AvatarCacheTime', 3600, { + type: 'int', + i18nDescription: 'Accounts_AvatarCacheTime_description', + }); + + this.add('Accounts_AvatarBlockUnauthenticatedAccess', false, { + type: 'boolean', + public: true, + }); + + return this.add('Accounts_SetDefaultAvatar', true, { + type: 'boolean', + }); + }); + + this.section('Password_Policy', function() { + this.add('Accounts_Password_Policy_Enabled', false, { + type: 'boolean', + }); + + const enableQuery = { + _id: 'Accounts_Password_Policy_Enabled', + value: true, + }; + + this.add('Accounts_Password_Policy_MinLength', 7, { + type: 'int', + enableQuery, + }); + + this.add('Accounts_Password_Policy_MaxLength', -1, { + type: 'int', + enableQuery, + }); + + this.add('Accounts_Password_Policy_ForbidRepeatingCharacters', true, { + type: 'boolean', + enableQuery, + }); + + this.add('Accounts_Password_Policy_ForbidRepeatingCharactersCount', 3, { + type: 'int', + enableQuery, + }); + + this.add('Accounts_Password_Policy_AtLeastOneLowercase', true, { + type: 'boolean', + enableQuery, + }); + + this.add('Accounts_Password_Policy_AtLeastOneUppercase', true, { + type: 'boolean', + enableQuery, + }); + + this.add('Accounts_Password_Policy_AtLeastOneNumber', true, { + type: 'boolean', + enableQuery, + }); + + this.add('Accounts_Password_Policy_AtLeastOneSpecialCharacter', true, { + type: 'boolean', + enableQuery, + }); + }); + + this.section('Password_History', function() { + this.add('Accounts_Password_History_Enabled', false, { + type: 'boolean', + i18nLabel: 'Enable_Password_History', + i18nDescription: 'Enable_Password_History_Description', + }); + + const enableQuery = { + _id: 'Accounts_Password_History_Enabled', + value: true, + }; + + this.add('Accounts_Password_History_Amount', 5, { + type: 'int', + enableQuery, + i18nLabel: 'Password_History_Amount', + i18nDescription: 'Password_History_Amount_Description', + }); + }); +}); + +settingsRegistry.addGroup('OAuth', function() { + this.section('Facebook', function() { + const enableQuery = { + _id: 'Accounts_OAuth_Facebook', + value: true, + }; + this.add('Accounts_OAuth_Facebook', false, { + type: 'boolean', + public: true, + }); + this.add('Accounts_OAuth_Facebook_id', '', { + type: 'string', + enableQuery, + }); + this.add('Accounts_OAuth_Facebook_secret', '', { + type: 'string', + enableQuery, + secret: true, + }); + return this.add('Accounts_OAuth_Facebook_callback_url', '_oauth/facebook', { + type: 'relativeUrl', + readonly: true, + enableQuery, + }); + }); + this.section('Google', function() { + const enableQuery = { + _id: 'Accounts_OAuth_Google', + value: true, + }; + this.add('Accounts_OAuth_Google', false, { + type: 'boolean', + public: true, + }); + this.add('Accounts_OAuth_Google_id', '', { + type: 'string', + enableQuery, + }); + this.add('Accounts_OAuth_Google_secret', '', { + type: 'string', + enableQuery, + secret: true, + }); + return this.add('Accounts_OAuth_Google_callback_url', '_oauth/google', { + type: 'relativeUrl', + readonly: true, + enableQuery, + }); + }); + this.section('GitHub', function() { + const enableQuery = { + _id: 'Accounts_OAuth_Github', + value: true, + }; + this.add('Accounts_OAuth_Github', false, { + type: 'boolean', + public: true, + }); + this.add('Accounts_OAuth_Github_id', '', { + type: 'string', + enableQuery, + }); + this.add('Accounts_OAuth_Github_secret', '', { + type: 'string', + enableQuery, + secret: true, + }); + return this.add('Accounts_OAuth_Github_callback_url', '_oauth/github', { + type: 'relativeUrl', + readonly: true, + enableQuery, + }); + }); + this.section('Linkedin', function() { + const enableQuery = { + _id: 'Accounts_OAuth_Linkedin', + value: true, + }; + this.add('Accounts_OAuth_Linkedin', false, { + type: 'boolean', + public: true, + }); + this.add('Accounts_OAuth_Linkedin_id', '', { + type: 'string', + enableQuery, + }); + this.add('Accounts_OAuth_Linkedin_secret', '', { + type: 'string', + enableQuery, + secret: true, + }); + return this.add('Accounts_OAuth_Linkedin_callback_url', '_oauth/linkedin', { + type: 'relativeUrl', + readonly: true, + enableQuery, + }); + }); + this.section('Meteor', function() { + const enableQuery = { + _id: 'Accounts_OAuth_Meteor', + value: true, + }; + this.add('Accounts_OAuth_Meteor', false, { + type: 'boolean', + public: true, + }); + this.add('Accounts_OAuth_Meteor_id', '', { + type: 'string', + enableQuery, + }); + this.add('Accounts_OAuth_Meteor_secret', '', { + type: 'string', + enableQuery, + secret: true, + }); + return this.add('Accounts_OAuth_Meteor_callback_url', '_oauth/meteor', { + type: 'relativeUrl', + readonly: true, + enableQuery, + }); + }); + this.section('Twitter', function() { + const enableQuery = { + _id: 'Accounts_OAuth_Twitter', + value: true, + }; + this.add('Accounts_OAuth_Twitter', false, { + type: 'boolean', + public: true, + }); + this.add('Accounts_OAuth_Twitter_id', '', { + type: 'string', + enableQuery, + }); + this.add('Accounts_OAuth_Twitter_secret', '', { + type: 'string', + enableQuery, + secret: true, + }); + return this.add('Accounts_OAuth_Twitter_callback_url', '_oauth/twitter', { + type: 'relativeUrl', + readonly: true, + enableQuery, + }); + }); + return this.section('Proxy', function() { + this.add('Accounts_OAuth_Proxy_host', 'https://oauth-proxy.rocket.chat', { + type: 'string', + public: true, + }); + return this.add('Accounts_OAuth_Proxy_services', '', { + type: 'string', + public: true, + }); + }); +}); + +settingsRegistry.addGroup('General', function() { + this.add('Show_Setup_Wizard', 'pending', { + type: 'select', + public: true, + readonly: true, + values: [ + { + key: 'pending', + i18nLabel: 'Pending', + }, { + key: 'in_progress', + i18nLabel: 'In_progress', + }, { + key: 'completed', + i18nLabel: 'Completed', + }, + ], + }); + + // eslint-disable-next-line @typescript-eslint/camelcase + this.add('Site_Url', typeof (global as any).__meteor_runtime_config__ !== 'undefined' && (global as any).__meteor_runtime_config__ !== null ? (global as any).__meteor_runtime_config__.ROOT_URL : null, { + type: 'string', + i18nDescription: 'Site_Url_Description', + public: true, + }); + this.add('Site_Name', 'Rocket.Chat', { + type: 'string', + public: true, + wizard: { + step: 3, + order: 0, + }, + }); + this.add('Document_Domain', '', { + type: 'string', + public: true, + }); + this.add('Language', '', { + type: 'language', + public: true, + wizard: { + step: 3, + order: 1, + }, + }); + this.add('Allow_Invalid_SelfSigned_Certs', false, { + type: 'boolean', + secret: true, + }); + + this.add('Enable_CSP', true, { + type: 'boolean', + }); + + this.add('Iframe_Restrict_Access', true, { + type: 'boolean', + secret: true, + }); + this.add('Iframe_X_Frame_Options', 'sameorigin', { + type: 'string', + secret: true, + enableQuery: { + _id: 'Iframe_Restrict_Access', + value: true, + }, + }); + this.add('Favorite_Rooms', true, { + type: 'boolean', + public: true, + }); + this.add('First_Channel_After_Login', '', { + type: 'string', + public: true, + }); + this.add('Unread_Count', 'user_and_group_mentions_only', { + type: 'select', + values: [ + { + key: 'all_messages', + i18nLabel: 'All_messages', + }, { + key: 'user_mentions_only', + i18nLabel: 'User_mentions_only', + }, { + key: 'group_mentions_only', + i18nLabel: 'Group_mentions_only', + }, { + key: 'user_and_group_mentions_only', + i18nLabel: 'User_and_group_mentions_only', + }, + ], + public: true, + }); + this.add('Unread_Count_DM', 'all_messages', { + type: 'select', + values: [ + { + key: 'all_messages', + i18nLabel: 'All_messages', + }, { + key: 'mentions_only', + i18nLabel: 'Mentions_only', + }, + ], + public: true, + }); + + this.add('DeepLink_Url', 'https://go.rocket.chat', { + type: 'string', + public: true, + }); + + this.add('CDN_PREFIX', '', { + type: 'string', + public: true, + }); + this.add('CDN_PREFIX_ALL', true, { + type: 'boolean', + public: true, + }); + this.add('CDN_JSCSS_PREFIX', '', { + type: 'string', + public: true, + enableQuery: { + _id: 'CDN_PREFIX_ALL', + value: false, + }, + }); + this.add('Force_SSL', false, { + type: 'boolean', + public: true, + }); + + this.add('GoogleTagManager_id', '', { + type: 'string', + public: true, + secret: true, + }); + this.add('Bugsnag_api_key', '', { + type: 'string', + public: false, + secret: true, + }); + this.add('Restart', 'restart_server', { + type: 'action', + actionText: 'Restart_the_server', + }); + this.add('Store_Last_Message', true, { + type: 'boolean', + public: true, + i18nDescription: 'Store_Last_Message_Sent_per_Room', + }); + this.add('Robot_Instructions_File_Content', 'User-agent: *\nDisallow: /', { + type: 'string', + public: true, + multiline: true, + }); + this.add('Default_Referrer_Policy', 'same-origin', { + type: 'select', + values: [ + { + key: 'no-referrer', + i18nLabel: 'No_Referrer', + }, { + key: 'no-referrer-when-downgrade', + i18nLabel: 'No_Referrer_When_Downgrade', + }, { + key: 'origin', + i18nLabel: 'Origin', + }, { + key: 'origin-when-cross-origin', + i18nLabel: 'Origin_When_Cross_Origin', + }, { + key: 'same-origin', + i18nLabel: 'Same_Origin', + }, { + key: 'strict-origin', + i18nLabel: 'Strict_Origin', + }, { + key: 'strict-origin-when-cross-origin', + i18nLabel: 'Strict_Origin_When_Cross_Origin', + }, { + key: 'unsafe-url', + i18nLabel: 'Unsafe_Url', + }, + ], + public: true, + }); + this.add('ECDH_Enabled', false, { + type: 'boolean', + alert: 'This_feature_is_currently_in_alpha', + }); + this.section('UTF8', function() { + this.add('UTF8_User_Names_Validation', '[0-9a-zA-Z-_.]+', { + type: 'string', + public: true, + i18nDescription: 'UTF8_User_Names_Validation_Description', + }); + this.add('UTF8_Channel_Names_Validation', '[0-9a-zA-Z-_.]+', { + type: 'string', + public: true, + i18nDescription: 'UTF8_Channel_Names_Validation_Description', + }); + return this.add('UTF8_Names_Slugify', true, { + type: 'boolean', + public: true, + }); + }); + this.section('Reporting', function() { + return this.add('Statistics_reporting', true, { + type: 'boolean', + }); + }); + this.section('Notifications', function() { + this.add('Notifications_Max_Room_Members', 100, { + type: 'int', + public: true, + i18nDescription: 'Notifications_Max_Room_Members_Description', + }); + }); + this.section('REST API', function() { + return this.add('API_User_Limit', 500, { + type: 'int', + public: true, + i18nDescription: 'API_User_Limit', + }); + }); + this.section('Iframe_Integration', function() { + this.add('Iframe_Integration_send_enable', false, { + type: 'boolean', + public: true, + }); + this.add('Iframe_Integration_send_target_origin', '*', { + type: 'string', + public: true, + enableQuery: { + _id: 'Iframe_Integration_send_enable', + value: true, + }, + }); + this.add('Iframe_Integration_receive_enable', false, { + type: 'boolean', + public: true, + }); + return this.add('Iframe_Integration_receive_origin', '*', { + type: 'string', + public: true, + enableQuery: { + _id: 'Iframe_Integration_receive_enable', + value: true, + }, + }); + }); + this.section('Translations', function() { + return this.add('Custom_Translations', '', { + type: 'code', + public: true, + }); + }); + this.section('Stream_Cast', function() { + return this.add('Stream_Cast_Address', '', { + type: 'string', + }); + }); + this.section('NPS', function() { + this.add('NPS_survey_enabled', true, { + type: 'boolean', + }); + }); + this.section('Timezone', function() { + this.add('Default_Timezone_For_Reporting', 'server', { + type: 'select', + values: [{ + key: 'server', + i18nLabel: 'Default_Server_Timezone', + }, { + key: 'custom', + i18nLabel: 'Default_Custom_Timezone', + }, { + key: 'user', + i18nLabel: 'Default_User_Timezone', + }], + }); + this.add('Default_Custom_Timezone', '', { + type: 'timezone', + enableQuery: { + _id: 'Default_Timezone_For_Reporting', + value: 'custom', + }, + }); + }); +}); + +settingsRegistry.addGroup('Message', function() { + this.section('Message_Attachments', function() { + this.add('Message_Attachments_GroupAttach', false, { + type: 'boolean', + public: true, + i18nDescription: 'Message_Attachments_GroupAttachDescription', + }); + + this.add('Message_Attachments_Thumbnails_Enabled', true, { + type: 'boolean', + public: true, + i18nDescription: 'Message_Attachments_Thumbnails_EnabledDesc', + }); + + this.add('Message_Attachments_Thumbnails_Width', 480, { + type: 'int', + public: true, + enableQuery: [ + { + _id: 'Message_Attachments_Thumbnails_Enabled', + value: true, + }, + ], + }); + + this.add('Message_Attachments_Thumbnails_Height', 360, { + type: 'int', + public: true, + enableQuery: [ + { + _id: 'Message_Attachments_Thumbnails_Enabled', + value: true, + }, + ], + }); + + this.add('Message_Attachments_Strip_Exif', false, { + type: 'boolean', + public: true, + i18nDescription: 'Message_Attachments_Strip_ExifDescription', + }); + }); + this.section('Message_Audio', function() { + this.add('Message_AudioRecorderEnabled', true, { + type: 'boolean', + public: true, + i18nDescription: 'Message_AudioRecorderEnabledDescription', + }); + this.add('Message_Audio_bitRate', 32, { + type: 'int', + public: true, + }); + }); + this.add('Message_AllowEditing', true, { + type: 'boolean', + public: true, + }); + this.add('Message_AllowEditing_BlockEditInMinutes', 0, { + type: 'int', + public: true, + i18nDescription: 'Message_AllowEditing_BlockEditInMinutesDescription', + }); + this.add('Message_AllowDeleting', true, { + type: 'boolean', + public: true, + }); + this.add('Message_AllowDeleting_BlockDeleteInMinutes', 0, { + type: 'int', + public: true, + i18nDescription: 'Message_AllowDeleting_BlockDeleteInMinutes', + }); + this.add('Message_AllowUnrecognizedSlashCommand', false, { + type: 'boolean', + public: true, + }); + this.add('Message_AllowDirectMessagesToYourself', true, { + type: 'boolean', + public: true, + }); + this.add('Message_AlwaysSearchRegExp', false, { + type: 'boolean', + }); + this.add('Message_ShowEditedStatus', true, { + type: 'boolean', + public: true, + }); + this.add('Message_ShowDeletedStatus', false, { + type: 'boolean', + public: true, + }); + this.add('Message_AllowBadWordsFilter', false, { + type: 'boolean', + public: true, + }); + this.add('Message_BadWordsFilterList', '', { + type: 'string', + public: true, + }); + this.add('Message_BadWordsWhitelist', '', { + type: 'string', + public: true, + }); + this.add('Message_KeepHistory', false, { + type: 'boolean', + public: true, + }); + this.add('Message_MaxAll', 0, { + type: 'int', + public: true, + }); + this.add('Message_MaxAllowedSize', 5000, { + type: 'int', + public: true, + }); + this.add('Message_AllowConvertLongMessagesToAttachment', true, { + type: 'boolean', + public: true, + }); + this.add('Message_ShowFormattingTips', true, { + type: 'boolean', + public: true, + }); + this.add('Message_GroupingPeriod', 300, { + type: 'int', + public: true, + i18nDescription: 'Message_GroupingPeriodDescription', + }); + this.add('API_Embed', true, { + type: 'boolean', + public: true, + }); + this.add('API_Embed_UserAgent', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36', { + type: 'string', + public: true, + }); + this.add('API_EmbedCacheExpirationDays', 30, { + type: 'int', + public: false, + }); + this.add('API_Embed_clear_cache_now', 'OEmbedCacheCleanup', { + type: 'action', + actionText: 'clear', + i18nLabel: 'clear_cache_now', + }); + this.add('API_EmbedDisabledFor', '', { + type: 'string', + public: true, + i18nDescription: 'API_EmbedDisabledFor_Description', + }); + this.add('API_EmbedIgnoredHosts', 'localhost, 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16', { + type: 'string', + i18nDescription: 'API_EmbedIgnoredHosts_Description', + }); + this.add('API_EmbedSafePorts', '80, 443', { + type: 'string', + }); + this.add('Message_TimeFormat', 'LT', { + type: 'string', + public: true, + i18nDescription: 'Message_TimeFormat_Description', + }); + this.add('Message_DateFormat', 'LL', { + type: 'string', + public: true, + i18nDescription: 'Message_DateFormat_Description', + }); + this.add('Message_TimeAndDateFormat', 'LLL', { + type: 'string', + public: true, + i18nDescription: 'Message_TimeAndDateFormat_Description', + }); + this.add('Message_QuoteChainLimit', 2, { + type: 'int', + public: true, + }); + + + this.add('Hide_System_Messages', [], { + type: 'multiSelect', + public: true, + values: MessageTypesValues, + }); + + this.add('DirectMesssage_maxUsers', 8, { + type: 'int', + public: true, + }); + + this.add('Message_ErasureType', 'Delete', { + type: 'select', + public: true, + values: [ + { + key: 'Keep', + i18nLabel: 'Message_ErasureType_Keep', + }, { + key: 'Delete', + i18nLabel: 'Message_ErasureType_Delete', + }, { + key: 'Unlink', + i18nLabel: 'Message_ErasureType_Unlink', + }, + ], + }); + + this.add('Message_Code_highlight', 'javascript,css,markdown,dockerfile,json,go,rust,clean,bash,plaintext,powershell,scss,shell,yaml,vim', { + type: 'string', + public: true, + }); +}); + +settingsRegistry.addGroup('Meta', function() { + this.add('Meta_language', '', { + type: 'string', + }); + this.add('Meta_fb_app_id', '', { + type: 'string', + secret: true, + }); + this.add('Meta_robots', 'INDEX,FOLLOW', { + type: 'string', + }); + this.add('Meta_google-site-verification', '', { + type: 'string', + secret: true, + }); + this.add('Meta_msvalidate01', '', { + type: 'string', + secret: true, + }); + return this.add('Meta_custom', '', { + type: 'code', + code: 'text/html', + multiline: true, + }); +}); + +settingsRegistry.addGroup('Mobile', function() { + this.add('Allow_Save_Media_to_Gallery', true, { + type: 'boolean', + public: true, + }); + this.section('Screen_Lock', function() { + this.add('Force_Screen_Lock', false, { type: 'boolean', i18nDescription: 'Force_Screen_Lock_description', public: true }); + this.add('Force_Screen_Lock_After', 1800, { type: 'int', i18nDescription: 'Force_Screen_Lock_After_description', enableQuery: { _id: 'Force_Screen_Lock', value: true }, public: true }); + }); +}); + +const pushEnabledWithoutGateway = [ + { + _id: 'Push_enable', + value: true, + }, { + _id: 'Push_enable_gateway', + value: false, + }, +]; + +settingsRegistry.addGroup('Push', function() { + this.add('Push_enable', true, { + type: 'boolean', + public: true, + alert: 'Push_Setting_Requires_Restart_Alert', + }); + + this.add('Push_enable_gateway', true, { + type: 'boolean', + alert: 'Push_Setting_Requires_Restart_Alert', + enableQuery: [ + { + _id: 'Push_enable', + value: true, + }, + { + _id: 'Register_Server', + value: true, + }, + { + _id: 'Cloud_Service_Agree_PrivacyTerms', + value: true, + }, + ], + }); + this.add('Push_gateway', 'https://gateway.rocket.chat', { + type: 'string', + i18nDescription: 'Push_gateway_description', + alert: 'Push_Setting_Requires_Restart_Alert', + multiline: true, + enableQuery: [ + { + _id: 'Push_enable', + value: true, + }, { + _id: 'Push_enable_gateway', + value: true, + }, + ], + }); + this.add('Push_production', true, { + type: 'boolean', + public: true, + alert: 'Push_Setting_Requires_Restart_Alert', + enableQuery: pushEnabledWithoutGateway, + }); + this.add('Push_test_push', 'push_test', { + type: 'action', + actionText: 'Send_a_test_push_to_my_user', + enableQuery: { + _id: 'Push_enable', + value: true, + }, + }); + this.section('Certificates_and_Keys', function() { + this.add('Push_apn_passphrase', '', { + type: 'string', + enableQuery: [], + secret: true, + }); + this.add('Push_apn_key', '', { + type: 'string', + multiline: true, + enableQuery: [], + secret: true, + }); + this.add('Push_apn_cert', '', { + type: 'string', + multiline: true, + enableQuery: [], + secret: true, + }); + this.add('Push_apn_dev_passphrase', '', { + type: 'string', + enableQuery: [], + secret: true, + }); + this.add('Push_apn_dev_key', '', { + type: 'string', + multiline: true, + enableQuery: [], + secret: true, + }); + this.add('Push_apn_dev_cert', '', { + type: 'string', + multiline: true, + enableQuery: [], + secret: true, + }); + this.add('Push_gcm_api_key', '', { + type: 'string', + enableQuery: [], + secret: true, + }); + return this.add('Push_gcm_project_number', '', { + type: 'string', + public: true, + enableQuery: [], + secret: true, + }); + }); + return this.section('Privacy', function() { + this.add('Push_show_username_room', true, { + type: 'boolean', + public: true, + }); + this.add('Push_show_message', true, { + type: 'boolean', + public: true, + }); + this.add('Push_request_content_from_server', true, { + type: 'boolean', + enterprise: true, + invalidValue: false, + modules: [ + 'push-privacy', + ], + }); + }); +}); + +settingsRegistry.addGroup('Layout', function() { + this.section('Content', function() { + this.add('Layout_Home_Title', 'Home', { + type: 'string', + public: true, + }); + this.add('Layout_Show_Home_Button', true, { + type: 'boolean', + public: true, + }); + this.add('Layout_Home_Body', '

Welcome to Rocket.Chat!

\n

The Rocket.Chat desktops apps for Windows, macOS and Linux are available to download here.

The native mobile app, Rocket.Chat,\n for Android and iOS is available from Google Play and the App Store.

\n

For further help, please consult the documentation.

\n

If you\'re an admin, feel free to change this content via AdministrationLayoutHome Body. Or clicking here.

', { + type: 'code', + code: 'text/html', + multiline: true, + public: true, + }); + this.add('Layout_Terms_of_Service', 'Terms of Service
Go to APP SETTINGS → Layout to customize this page.', { + type: 'code', + code: 'text/html', + multiline: true, + public: true, + }); + this.add('Layout_Login_Terms', 'By proceeding you are agreeing to our Terms of Service, Privacy Policy and Legal Notice.', { + type: 'string', + multiline: true, + public: true, + }); + this.add('Layout_Privacy_Policy', 'Privacy Policy
Go to APP SETTINGS → Layout to customize this page.', { + type: 'code', + code: 'text/html', + multiline: true, + public: true, + }); + this.add('Layout_Legal_Notice', 'Legal Notice
Go to APP SETTINGS -> Layout to customize this page.', { + type: 'code', + code: 'text/html', + multiline: true, + public: true, + }); + return this.add('Layout_Sidenav_Footer', 'Home', { + type: 'code', + code: 'text/html', + public: true, + i18nDescription: 'Layout_Sidenav_Footer_description', + }); + }); + this.section('Custom_Scripts', function() { + this.add('Custom_Script_On_Logout', '//Add your script', { + type: 'code', + multiline: true, + public: true, + }); + this.add('Custom_Script_Logged_Out', '//Add your script', { + type: 'code', + multiline: true, + public: true, + }); + return this.add('Custom_Script_Logged_In', '//Add your script', { + type: 'code', + multiline: true, + public: true, + }); + }); + return this.section('User_Interface', function() { + this.add('UI_DisplayRoles', true, { + type: 'boolean', + public: true, + }); + this.add('UI_Group_Channels_By_Type', true, { + type: 'boolean', + public: false, + }); + this.add('UI_Use_Name_Avatar', false, { + type: 'boolean', + public: true, + }); + this.add('UI_Use_Real_Name', false, { + type: 'boolean', + public: true, + }); + this.add('UI_Click_Direct_Message', false, { + type: 'boolean', + public: true, + }); + + this.add('Number_of_users_autocomplete_suggestions', 5, { + type: 'int', + public: true, + }); + + this.add('UI_Unread_Counter_Style', 'Different_Style_For_User_Mentions', { + type: 'select', + values: [ + { + key: 'Same_Style_For_Mentions', + i18nLabel: 'Same_Style_For_Mentions', + }, { + key: 'Different_Style_For_User_Mentions', + i18nLabel: 'Different_Style_For_User_Mentions', + }, + ], + public: true, + }); + this.add('UI_Allow_room_names_with_special_chars', false, { + type: 'boolean', + public: true, + }); + return this.add('UI_Show_top_navbar_embedded_layout', false, { + type: 'boolean', + public: true, + }); + }); +}); + +settingsRegistry.addGroup('Logs', function() { + this.add('Log_Level', '0', { + type: 'select', + values: [ + { + key: '0', + i18nLabel: '0_Errors_Only', + }, { + key: '1', + i18nLabel: '1_Errors_and_Information', + }, { + key: '2', + i18nLabel: '2_Erros_Information_and_Debug', + }, + ], + public: true, + }); + this.add('Log_View_Limit', 1000, { + type: 'int', + }); + + this.add('Log_Trace_Methods', false, { + type: 'boolean', + }); + + this.add('Log_Trace_Methods_Filter', '', { + type: 'string', + enableQuery: { + _id: 'Log_Trace_Methods', + value: true, + }, + }); + + this.add('Log_Trace_Subscriptions', false, { + type: 'boolean', + }); + + this.add('Log_Trace_Subscriptions_Filter', '', { + type: 'string', + enableQuery: { + _id: 'Log_Trace_Subscriptions', + value: true, + }, + }); + + this.section('Prometheus', function() { + this.add('Prometheus_Enabled', false, { + type: 'boolean', + i18nLabel: 'Enabled', + }); + // See the default port allocation at https://github.com/prometheus/prometheus/wiki/Default-port-allocations + this.add('Prometheus_Port', 9458, { + type: 'int', + i18nLabel: 'Port', + }); + this.add('Prometheus_Reset_Interval', 0, { + type: 'int', + }); + this.add('Prometheus_Garbage_Collector', false, { + type: 'boolean', + alert: 'Prometheus_Garbage_Collector_Alert', + }); + this.add('Prometheus_API_User_Agent', false, { + type: 'boolean', + }); + }); +}); + +settingsRegistry.addGroup('Setup_Wizard', function() { + this.section('Organization_Info', function() { + this.add('Organization_Type', '', { + type: 'select', + values: [ + { + key: 'community', + i18nLabel: 'Community', + }, + { + key: 'enterprise', + i18nLabel: 'Enterprise', + }, + { + key: 'government', + i18nLabel: 'Government', + }, + { + key: 'nonprofit', + i18nLabel: 'Nonprofit', + }, + ], + wizard: { + step: 2, + order: 0, + }, + }); + this.add('Organization_Name', '', { + type: 'string', + wizard: { + step: 2, + order: 1, + }, + }); + this.add('Industry', '', { + type: 'select', + values: [ + { + key: 'aerospaceDefense', + i18nLabel: 'Aerospace_and_Defense', + }, + { + key: 'consulting', + i18nLabel: 'Consulting', + }, + { + key: 'consumerGoods', + i18nLabel: 'Consumer_Packaged_Goods', + }, + { + key: 'contactCenter', + i18nLabel: 'Contact_Center', + }, + { + key: 'education', + i18nLabel: 'Education', + }, + { + key: 'entertainment', + i18nLabel: 'Entertainment', + }, + { + key: 'financialServices', + i18nLabel: 'Financial_Services', + }, + { + key: 'gaming', + i18nLabel: 'Gaming', + }, + { + key: 'healthcare', + i18nLabel: 'Healthcare', + }, + { + key: 'hospitalityBusinness', + i18nLabel: 'Hospitality_Businness', + }, + { + key: 'insurance', + i18nLabel: 'Insurance', + }, + { + key: 'itSecurity', + i18nLabel: 'It_Security', + }, + { + key: 'logistics', + i18nLabel: 'Logistics', + }, + { + key: 'manufacturing', + i18nLabel: 'Manufacturing', + }, + { + key: 'media', + i18nLabel: 'Media', + }, + { + key: 'pharmaceutical', + i18nLabel: 'Pharmaceutical', + }, + { + key: 'realEstate', + i18nLabel: 'Real_Estate', + }, + { + key: 'religious', + i18nLabel: 'Religious', + }, + { + key: 'retail', + i18nLabel: 'Retail', + }, + { + key: 'socialNetwork', + i18nLabel: 'Social_Network', + }, + { + key: 'technologyProvider', + i18nLabel: 'Technology_Provider', + }, + { + key: 'technologyServices', + i18nLabel: 'Technology_Services', + }, + { + key: 'telecom', + i18nLabel: 'Telecom', + }, + { + key: 'utilities', + i18nLabel: 'Utilities', + }, + { + key: 'other', + i18nLabel: 'Other', + }, + ], + wizard: { + step: 2, + order: 2, + }, + }); + this.add('Size', '', { + type: 'select', + values: [ + { + key: '0', + i18nLabel: '1-10 people', + }, + { + key: '1', + i18nLabel: '11-50 people', + }, + { + key: '2', + i18nLabel: '51-100 people', + }, + { + key: '3', + i18nLabel: '101-250 people', + }, + { + key: '4', + i18nLabel: '251-500 people', + }, + { + key: '5', + i18nLabel: '501-1000 people', + }, + { + key: '6', + i18nLabel: '1001-4000 people', + }, + { + key: '7', + i18nLabel: '4000 or more people', + }, + ], + wizard: { + step: 2, + order: 3, + }, + }); + this.add('Country', '', { + type: 'select', + values: [ + { + key: 'afghanistan', + i18nLabel: 'Country_Afghanistan', + }, + { + key: 'albania', + i18nLabel: 'Country_Albania', + }, + { + key: 'algeria', + i18nLabel: 'Country_Algeria', + }, + { + key: 'americanSamoa', + i18nLabel: 'Country_American_Samoa', + }, + { + key: 'andorra', + i18nLabel: 'Country_Andorra', + }, + { + key: 'angola', + i18nLabel: 'Country_Angola', + }, + { + key: 'anguilla', + i18nLabel: 'Country_Anguilla', + }, + { + key: 'antarctica', + i18nLabel: 'Country_Antarctica', + }, + { + key: 'antiguaAndBarbuda', + i18nLabel: 'Country_Antigua_and_Barbuda', + }, + { + key: 'argentina', + i18nLabel: 'Country_Argentina', + }, + { + key: 'armenia', + i18nLabel: 'Country_Armenia', + }, + { + key: 'aruba', + i18nLabel: 'Country_Aruba', + }, + { + key: 'australia', + i18nLabel: 'Country_Australia', + }, + { + key: 'austria', + i18nLabel: 'Country_Austria', + }, + { + key: 'azerbaijan', + i18nLabel: 'Country_Azerbaijan', + }, + { + key: 'bahamas', + i18nLabel: 'Country_Bahamas', + }, + { + key: 'bahrain', + i18nLabel: 'Country_Bahrain', + }, + { + key: 'bangladesh', + i18nLabel: 'Country_Bangladesh', + }, + { + key: 'barbados', + i18nLabel: 'Country_Barbados', + }, + { + key: 'belarus', + i18nLabel: 'Country_Belarus', + }, + { + key: 'belgium', + i18nLabel: 'Country_Belgium', + }, + { + key: 'belize', + i18nLabel: 'Country_Belize', + }, + { + key: 'benin', + i18nLabel: 'Country_Benin', + }, + { + key: 'bermuda', + i18nLabel: 'Country_Bermuda', + }, + { + key: 'bhutan', + i18nLabel: 'Country_Bhutan', + }, + { + key: 'bolivia', + i18nLabel: 'Country_Bolivia', + }, + { + key: 'bosniaAndHerzegovina', + i18nLabel: 'Country_Bosnia_and_Herzegovina', + }, + { + key: 'botswana', + i18nLabel: 'Country_Botswana', + }, + { + key: 'bouvetIsland', + i18nLabel: 'Country_Bouvet_Island', + }, + { + key: 'brazil', + i18nLabel: 'Country_Brazil', + }, + { + key: 'britishIndianOceanTerritory', + i18nLabel: 'Country_British_Indian_Ocean_Territory', + }, + { + key: 'bruneiDarussalam', + i18nLabel: 'Country_Brunei_Darussalam', + }, + { + key: 'bulgaria', + i18nLabel: 'Country_Bulgaria', + }, + { + key: 'burkinaFaso', + i18nLabel: 'Country_Burkina_Faso', + }, + { + key: 'burundi', + i18nLabel: 'Country_Burundi', + }, + { + key: 'cambodia', + i18nLabel: 'Country_Cambodia', + }, + { + key: 'cameroon', + i18nLabel: 'Country_Cameroon', + }, + { + key: 'canada', + i18nLabel: 'Country_Canada', + }, + { + key: 'capeVerde', + i18nLabel: 'Country_Cape_Verde', + }, + { + key: 'caymanIslands', + i18nLabel: 'Country_Cayman_Islands', + }, + { + key: 'centralAfricanRepublic', + i18nLabel: 'Country_Central_African_Republic', + }, + { + key: 'chad', + i18nLabel: 'Country_Chad', + }, + { + key: 'chile', + i18nLabel: 'Country_Chile', + }, + { + key: 'china', + i18nLabel: 'Country_China', + }, + { + key: 'christmasIsland', + i18nLabel: 'Country_Christmas_Island', + }, + { + key: 'cocosKeelingIslands', + i18nLabel: 'Country_Cocos_Keeling_Islands', + }, + { + key: 'colombia', + i18nLabel: 'Country_Colombia', + }, + { + key: 'comoros', + i18nLabel: 'Country_Comoros', + }, + { + key: 'congo', + i18nLabel: 'Country_Congo', + }, + { + key: 'congoTheDemocraticRepublicOfThe', + i18nLabel: 'Country_Congo_The_Democratic_Republic_of_The', + }, + { + key: 'cookIslands', + i18nLabel: 'Country_Cook_Islands', + }, + { + key: 'costaRica', + i18nLabel: 'Country_Costa_Rica', + }, + { + key: 'coteDivoire', + i18nLabel: 'Country_Cote_Divoire', + }, + { + key: 'croatia', + i18nLabel: 'Country_Croatia', + }, + { + key: 'cuba', + i18nLabel: 'Country_Cuba', + }, + { + key: 'cyprus', + i18nLabel: 'Country_Cyprus', + }, + { + key: 'czechRepublic', + i18nLabel: 'Country_Czech_Republic', + }, + { + key: 'denmark', + i18nLabel: 'Country_Denmark', + }, + { + key: 'djibouti', + i18nLabel: 'Country_Djibouti', + }, + { + key: 'dominica', + i18nLabel: 'Country_Dominica', + }, + { + key: 'dominicanRepublic', + i18nLabel: 'Country_Dominican_Republic', + }, + { + key: 'ecuador', + i18nLabel: 'Country_Ecuador', + }, + { + key: 'egypt', + i18nLabel: 'Country_Egypt', + }, + { + key: 'elSalvador', + i18nLabel: 'Country_El_Salvador', + }, + { + key: 'equatorialGuinea', + i18nLabel: 'Country_Equatorial_Guinea', + }, + { + key: 'eritrea', + i18nLabel: 'Country_Eritrea', + }, + { + key: 'estonia', + i18nLabel: 'Country_Estonia', + }, + { + key: 'ethiopia', + i18nLabel: 'Country_Ethiopia', + }, + { + key: 'falklandIslandsMalvinas', + i18nLabel: 'Country_Falkland_Islands_Malvinas', + }, + { + key: 'faroeIslands', + i18nLabel: 'Country_Faroe_Islands', + }, + { + key: 'fiji', + i18nLabel: 'Country_Fiji', + }, + { + key: 'finland', + i18nLabel: 'Country_Finland', + }, + { + key: 'france', + i18nLabel: 'Country_France', + }, + { + key: 'frenchGuiana', + i18nLabel: 'Country_French_Guiana', + }, + { + key: 'frenchPolynesia', + i18nLabel: 'Country_French_Polynesia', + }, + { + key: 'frenchSouthernTerritories', + i18nLabel: 'Country_French_Southern_Territories', + }, + { + key: 'gabon', + i18nLabel: 'Country_Gabon', + }, + { + key: 'gambia', + i18nLabel: 'Country_Gambia', + }, + { + key: 'georgia', + i18nLabel: 'Country_Georgia', + }, + { + key: 'germany', + i18nLabel: 'Country_Germany', + }, + { + key: 'ghana', + i18nLabel: 'Country_Ghana', + }, + { + key: 'gibraltar', + i18nLabel: 'Country_Gibraltar', + }, + { + key: 'greece', + i18nLabel: 'Country_Greece', + }, + { + key: 'greenland', + i18nLabel: 'Country_Greenland', + }, + { + key: 'grenada', + i18nLabel: 'Country_Grenada', + }, + { + key: 'guadeloupe', + i18nLabel: 'Country_Guadeloupe', + }, + { + key: 'guam', + i18nLabel: 'Country_Guam', + }, + { + key: 'guatemala', + i18nLabel: 'Country_Guatemala', + }, + { + key: 'guinea', + i18nLabel: 'Country_Guinea', + }, + { + key: 'guineaBissau', + i18nLabel: 'Country_Guinea_bissau', + }, + { + key: 'guyana', + i18nLabel: 'Country_Guyana', + }, + { + key: 'haiti', + i18nLabel: 'Country_Haiti', + }, + { + key: 'heardIslandAndMcdonaldIslands', + i18nLabel: 'Country_Heard_Island_and_Mcdonald_Islands', + }, + { + key: 'holySeeVaticanCityState', + i18nLabel: 'Country_Holy_See_Vatican_City_State', + }, + { + key: 'honduras', + i18nLabel: 'Country_Honduras', + }, + { + key: 'hongKong', + i18nLabel: 'Country_Hong_Kong', + }, + { + key: 'hungary', + i18nLabel: 'Country_Hungary', + }, + { + key: 'iceland', + i18nLabel: 'Country_Iceland', + }, + { + key: 'india', + i18nLabel: 'Country_India', + }, + { + key: 'indonesia', + i18nLabel: 'Country_Indonesia', + }, + { + key: 'iranIslamicRepublicOf', + i18nLabel: 'Country_Iran_Islamic_Republic_of', + }, + { + key: 'iraq', + i18nLabel: 'Country_Iraq', + }, + { + key: 'ireland', + i18nLabel: 'Country_Ireland', + }, + { + key: 'israel', + i18nLabel: 'Country_Israel', + }, + { + key: 'italy', + i18nLabel: 'Country_Italy', + }, + { + key: 'jamaica', + i18nLabel: 'Country_Jamaica', + }, + { + key: 'japan', + i18nLabel: 'Country_Japan', + }, + { + key: 'jordan', + i18nLabel: 'Country_Jordan', + }, + { + key: 'kazakhstan', + i18nLabel: 'Country_Kazakhstan', + }, + { + key: 'kenya', + i18nLabel: 'Country_Kenya', + }, + { + key: 'kiribati', + i18nLabel: 'Country_Kiribati', + }, + { + key: 'koreaDemocraticPeoplesRepublicOf', + i18nLabel: 'Country_Korea_Democratic_Peoples_Republic_of', + }, + { + key: 'koreaRepublicOf', + i18nLabel: 'Country_Korea_Republic_of', + }, + { + key: 'kuwait', + i18nLabel: 'Country_Kuwait', + }, + { + key: 'kyrgyzstan', + i18nLabel: 'Country_Kyrgyzstan', + }, + { + key: 'laoPeoplesDemocraticRepublic', + i18nLabel: 'Country_Lao_Peoples_Democratic_Republic', + }, + { + key: 'latvia', + i18nLabel: 'Country_Latvia', + }, + { + key: 'lebanon', + i18nLabel: 'Country_Lebanon', + }, + { + key: 'lesotho', + i18nLabel: 'Country_Lesotho', + }, + { + key: 'liberia', + i18nLabel: 'Country_Liberia', + }, + { + key: 'libyanArabJamahiriya', + i18nLabel: 'Country_Libyan_Arab_Jamahiriya', + }, + { + key: 'liechtenstein', + i18nLabel: 'Country_Liechtenstein', + }, + { + key: 'lithuania', + i18nLabel: 'Country_Lithuania', + }, + { + key: 'luxembourg', + i18nLabel: 'Country_Luxembourg', + }, + { + key: 'macao', + i18nLabel: 'Country_Macao', + }, + { + key: 'macedoniaTheFormerYugoslavRepublicOf', + i18nLabel: 'Country_Macedonia_The_Former_Yugoslav_Republic_of', + }, + { + key: 'madagascar', + i18nLabel: 'Country_Madagascar', + }, + { + key: 'malawi', + i18nLabel: 'Country_Malawi', + }, + { + key: 'malaysia', + i18nLabel: 'Country_Malaysia', + }, + { + key: 'maldives', + i18nLabel: 'Country_Maldives', + }, + { + key: 'mali', + i18nLabel: 'Country_Mali', + }, + { + key: 'malta', + i18nLabel: 'Country_Malta', + }, + { + key: 'marshallIslands', + i18nLabel: 'Country_Marshall_Islands', + }, + { + key: 'martinique', + i18nLabel: 'Country_Martinique', + }, + { + key: 'mauritania', + i18nLabel: 'Country_Mauritania', + }, + { + key: 'mauritius', + i18nLabel: 'Country_Mauritius', + }, + { + key: 'mayotte', + i18nLabel: 'Country_Mayotte', + }, + { + key: 'mexico', + i18nLabel: 'Country_Mexico', + }, + { + key: 'micronesiaFederatedStatesOf', + i18nLabel: 'Country_Micronesia_Federated_States_of', + }, + { + key: 'moldovaRepublicOf', + i18nLabel: 'Country_Moldova_Republic_of', + }, + { + key: 'monaco', + i18nLabel: 'Country_Monaco', + }, + { + key: 'mongolia', + i18nLabel: 'Country_Mongolia', + }, + { + key: 'montserrat', + i18nLabel: 'Country_Montserrat', + }, + { + key: 'morocco', + i18nLabel: 'Country_Morocco', + }, + { + key: 'mozambique', + i18nLabel: 'Country_Mozambique', + }, + { + key: 'myanmar', + i18nLabel: 'Country_Myanmar', + }, + { + key: 'namibia', + i18nLabel: 'Country_Namibia', + }, + { + key: 'nauru', + i18nLabel: 'Country_Nauru', + }, + { + key: 'nepal', + i18nLabel: 'Country_Nepal', + }, + { + key: 'netherlands', + i18nLabel: 'Country_Netherlands', + }, + { + key: 'netherlandsAntilles', + i18nLabel: 'Country_Netherlands_Antilles', + }, + { + key: 'newCaledonia', + i18nLabel: 'Country_New_Caledonia', + }, + { + key: 'newZealand', + i18nLabel: 'Country_New_Zealand', + }, + { + key: 'nicaragua', + i18nLabel: 'Country_Nicaragua', + }, + { + key: 'niger', + i18nLabel: 'Country_Niger', + }, + { + key: 'nigeria', + i18nLabel: 'Country_Nigeria', + }, + { + key: 'niue', + i18nLabel: 'Country_Niue', + }, + { + key: 'norfolkIsland', + i18nLabel: 'Country_Norfolk_Island', + }, + { + key: 'northernMarianaIslands', + i18nLabel: 'Country_Northern_Mariana_Islands', + }, + { + key: 'norway', + i18nLabel: 'Country_Norway', + }, + { + key: 'oman', + i18nLabel: 'Country_Oman', + }, + { + key: 'pakistan', + i18nLabel: 'Country_Pakistan', + }, + { + key: 'palau', + i18nLabel: 'Country_Palau', + }, + { + key: 'palestinianTerritoryOccupied', + i18nLabel: 'Country_Palestinian_Territory_Occupied', + }, + { + key: 'panama', + i18nLabel: 'Country_Panama', + }, + { + key: 'papuaNewGuinea', + i18nLabel: 'Country_Papua_New_Guinea', + }, + { + key: 'paraguay', + i18nLabel: 'Country_Paraguay', + }, + { + key: 'peru', + i18nLabel: 'Country_Peru', + }, + { + key: 'philippines', + i18nLabel: 'Country_Philippines', + }, + { + key: 'pitcairn', + i18nLabel: 'Country_Pitcairn', + }, + { + key: 'poland', + i18nLabel: 'Country_Poland', + }, + { + key: 'portugal', + i18nLabel: 'Country_Portugal', + }, + { + key: 'puertoRico', + i18nLabel: 'Country_Puerto_Rico', + }, + { + key: 'qatar', + i18nLabel: 'Country_Qatar', + }, + { + key: 'reunion', + i18nLabel: 'Country_Reunion', + }, + { + key: 'romania', + i18nLabel: 'Country_Romania', + }, + { + key: 'russianFederation', + i18nLabel: 'Country_Russian_Federation', + }, + { + key: 'rwanda', + i18nLabel: 'Country_Rwanda', + }, + { + key: 'saintHelena', + i18nLabel: 'Country_Saint_Helena', + }, + { + key: 'saintKittsAndNevis', + i18nLabel: 'Country_Saint_Kitts_and_Nevis', + }, + { + key: 'saintLucia', + i18nLabel: 'Country_Saint_Lucia', + }, + { + key: 'saintPierreAndMiquelon', + i18nLabel: 'Country_Saint_Pierre_and_Miquelon', + }, + { + key: 'saintVincentAndTheGrenadines', + i18nLabel: 'Country_Saint_Vincent_and_The_Grenadines', + }, + { + key: 'samoa', + i18nLabel: 'Country_Samoa', + }, + { + key: 'sanMarino', + i18nLabel: 'Country_San_Marino', + }, + { + key: 'saoTomeAndPrincipe', + i18nLabel: 'Country_Sao_Tome_and_Principe', + }, + { + key: 'saudiArabia', + i18nLabel: 'Country_Saudi_Arabia', + }, + { + key: 'senegal', + i18nLabel: 'Country_Senegal', + }, + { + key: 'serbiaAndMontenegro', + i18nLabel: 'Country_Serbia_and_Montenegro', + }, + { + key: 'seychelles', + i18nLabel: 'Country_Seychelles', + }, + { + key: 'sierraLeone', + i18nLabel: 'Country_Sierra_Leone', + }, + { + key: 'singapore', + i18nLabel: 'Country_Singapore', + }, + { + key: 'slovakia', + i18nLabel: 'Country_Slovakia', + }, + { + key: 'slovenia', + i18nLabel: 'Country_Slovenia', + }, + { + key: 'solomonIslands', + i18nLabel: 'Country_Solomon_Islands', + }, + { + key: 'somalia', + i18nLabel: 'Country_Somalia', + }, + { + key: 'southAfrica', + i18nLabel: 'Country_South_Africa', + }, + { + key: 'southGeorgiaAndTheSouthSandwichIslands', + i18nLabel: 'Country_South_Georgia_and_The_South_Sandwich_Islands', + }, + { + key: 'spain', + i18nLabel: 'Country_Spain', + }, + { + key: 'sriLanka', + i18nLabel: 'Country_Sri_Lanka', + }, + { + key: 'sudan', + i18nLabel: 'Country_Sudan', + }, + { + key: 'suriname', + i18nLabel: 'Country_Suriname', + }, + { + key: 'svalbardAndJanMayen', + i18nLabel: 'Country_Svalbard_and_Jan_Mayen', + }, + { + key: 'swaziland', + i18nLabel: 'Country_Swaziland', + }, + { + key: 'sweden', + i18nLabel: 'Country_Sweden', + }, + { + key: 'switzerland', + i18nLabel: 'Country_Switzerland', + }, + { + key: 'syrianArabRepublic', + i18nLabel: 'Country_Syrian_Arab_Republic', + }, + { + key: 'taiwanProvinceOfChina', + i18nLabel: 'Country_Taiwan_Province_of_China', + }, + { + key: 'tajikistan', + i18nLabel: 'Country_Tajikistan', + }, + { + key: 'tanzaniaUnitedRepublicOf', + i18nLabel: 'Country_Tanzania_United_Republic_of', + }, + { + key: 'thailand', + i18nLabel: 'Country_Thailand', + }, + { + key: 'timorLeste', + i18nLabel: 'Country_Timor_leste', + }, + { + key: 'togo', + i18nLabel: 'Country_Togo', + }, + { + key: 'tokelau', + i18nLabel: 'Country_Tokelau', + }, + { + key: 'tonga', + i18nLabel: 'Country_Tonga', + }, + { + key: 'trinidadAndTobago', + i18nLabel: 'Country_Trinidad_and_Tobago', + }, + { + key: 'tunisia', + i18nLabel: 'Country_Tunisia', + }, + { + key: 'turkey', + i18nLabel: 'Country_Turkey', + }, + { + key: 'turkmenistan', + i18nLabel: 'Country_Turkmenistan', + }, + { + key: 'turksAndCaicosIslands', + i18nLabel: 'Country_Turks_and_Caicos_Islands', + }, + { + key: 'tuvalu', + i18nLabel: 'Country_Tuvalu', + }, + { + key: 'uganda', + i18nLabel: 'Country_Uganda', + }, + { + key: 'ukraine', + i18nLabel: 'Country_Ukraine', + }, + { + key: 'unitedArabEmirates', + i18nLabel: 'Country_United_Arab_Emirates', + }, + { + key: 'unitedKingdom', + i18nLabel: 'Country_United_Kingdom', + }, + { + key: 'unitedStates', + i18nLabel: 'Country_United_States', + }, + { + key: 'unitedStatesMinorOutlyingIslands', + i18nLabel: 'Country_United_States_Minor_Outlying_Islands', + }, + { + key: 'uruguay', + i18nLabel: 'Country_Uruguay', + }, + { + key: 'uzbekistan', + i18nLabel: 'Country_Uzbekistan', + }, + { + key: 'vanuatu', + i18nLabel: 'Country_Vanuatu', + }, + { + key: 'venezuela', + i18nLabel: 'Country_Venezuela', + }, + { + key: 'vietNam', + i18nLabel: 'Country_Viet_Nam', + }, + { + key: 'virginIslandsBritish', + i18nLabel: 'Country_Virgin_Islands_British', + }, + { + key: 'virginIslandsUS', + i18nLabel: 'Country_Virgin_Islands_US', + }, + { + key: 'wallisAndFutuna', + i18nLabel: 'Country_Wallis_and_Futuna', + }, + { + key: 'westernSahara', + i18nLabel: 'Country_Western_Sahara', + }, + { + key: 'yemen', + i18nLabel: 'Country_Yemen', + }, + { + key: 'zambia', + i18nLabel: 'Country_Zambia', + }, + { + key: 'zimbabwe', + i18nLabel: 'Country_Zimbabwe', + }, + { + key: 'worldwide', + i18nLabel: 'Worldwide', + }, + ], + wizard: { + step: 2, + order: 4, + }, + }); + this.add('Website', '', { + type: 'string', + wizard: { + step: 2, + order: 5, + }, + }); + this.add('Server_Type', '', { + type: 'select', + values: [ + { + key: 'privateTeam', + i18nLabel: 'Private_Team', + }, + { + key: 'publicCommunity', + i18nLabel: 'Public_Community', + }, + ], + wizard: { + step: 3, + order: 2, + }, + }); + this.add('Allow_Marketing_Emails', true, { + type: 'boolean', + }); + this.add('Register_Server', false, { + type: 'boolean', + }); + this.add('Organization_Email', '', { + type: 'string', + }); + }); + + this.section('Cloud_Info', function() { + this.add('Nps_Url', 'https://nps.rocket.chat', { + type: 'string', + hidden: true, + readonly: true, + enableQuery: { + _id: 'Register_Server', + value: true, + }, + secret: true, + }); + + this.add('Cloud_Url', 'https://cloud.rocket.chat', { + type: 'string', + hidden: true, + readonly: true, + enableQuery: { + _id: 'Register_Server', + value: true, + }, + secret: true, + }); + + this.add('Cloud_Service_Agree_PrivacyTerms', false, { + type: 'boolean', + }); + + this.add('Cloud_Workspace_Id', '', { + type: 'string', + hidden: true, + readonly: true, + enableQuery: { + _id: 'Register_Server', + value: true, + }, + secret: true, + }); + + this.add('Cloud_Workspace_Name', '', { + type: 'string', + hidden: true, + readonly: true, + enableQuery: { + _id: 'Register_Server', + value: true, + }, + secret: true, + }); + + this.add('Cloud_Workspace_Client_Id', '', { + type: 'string', + hidden: true, + readonly: true, + enableQuery: { + _id: 'Register_Server', + value: true, + }, + secret: true, + }); + + this.add('Cloud_Workspace_Client_Secret', '', { + type: 'string', + hidden: true, + readonly: true, + enableQuery: { + _id: 'Register_Server', + value: true, + }, + secret: true, + }); + + this.add('Cloud_Workspace_Client_Secret_Expires_At', '', { + type: 'int', + hidden: true, + readonly: true, + enableQuery: { + _id: 'Register_Server', + value: true, + }, + secret: true, + }); + + this.add('Cloud_Workspace_Registration_Client_Uri', '', { + type: 'string', + hidden: true, + readonly: true, + enableQuery: { + _id: 'Register_Server', + value: true, + }, + secret: true, + }); + + this.add('Cloud_Workspace_PublicKey', '', { + type: 'string', + hidden: true, + readonly: true, + enableQuery: { + _id: 'Register_Server', + value: true, + }, + secret: true, + }); + + this.add('Cloud_Workspace_License', '', { + type: 'string', + hidden: true, + readonly: true, + enableQuery: { + _id: 'Register_Server', + value: true, + }, + secret: true, + }); + + this.add('Cloud_Workspace_Access_Token', '', { + type: 'string', + hidden: true, + readonly: true, + enableQuery: { + _id: 'Register_Server', + value: true, + }, + secret: true, + }); + + this.add('Cloud_Workspace_Access_Token_Expires_At', new Date(0), { + type: 'date', + hidden: true, + readonly: true, + enableQuery: { + _id: 'Register_Server', + value: true, + }, + secret: true, + }); + + this.add('Cloud_Workspace_Registration_State', '', { + type: 'string', + hidden: true, + readonly: true, + enableQuery: { + _id: 'Register_Server', + value: true, + }, + secret: true, + }); + }); +}); + +settingsRegistry.addGroup('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 } }); + + this.add('DDP_Rate_Limit_User_Enabled', true, { type: 'boolean' }); + this.add('DDP_Rate_Limit_User_Requests_Allowed', 1200, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_User_Enabled', value: true } }); + this.add('DDP_Rate_Limit_User_Interval_Time', 60000, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_User_Enabled', value: true } }); + + this.add('DDP_Rate_Limit_Connection_Enabled', true, { type: 'boolean' }); + this.add('DDP_Rate_Limit_Connection_Requests_Allowed', 600, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_Connection_Enabled', value: true } }); + this.add('DDP_Rate_Limit_Connection_Interval_Time', 60000, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_Connection_Enabled', value: true } }); + + this.add('DDP_Rate_Limit_User_By_Method_Enabled', true, { type: 'boolean' }); + this.add('DDP_Rate_Limit_User_By_Method_Requests_Allowed', 20, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_User_By_Method_Enabled', value: true } }); + this.add('DDP_Rate_Limit_User_By_Method_Interval_Time', 10000, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_User_By_Method_Enabled', value: true } }); + + this.add('DDP_Rate_Limit_Connection_By_Method_Enabled', true, { type: 'boolean' }); + this.add('DDP_Rate_Limit_Connection_By_Method_Requests_Allowed', 10, { type: 'int', enableQuery: { _id: 'DDP_Rate_Limit_Connection_By_Method_Enabled', value: true } }); + 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.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() { + this.add('Troubleshoot_Disable_Notifications', false, { + type: 'boolean', + alert: 'Troubleshoot_Disable_Notifications_Alert', + }); + this.add('Troubleshoot_Disable_Presence_Broadcast', false, { + type: 'boolean', + alert: 'Troubleshoot_Disable_Presence_Broadcast_Alert', + }); + this.add('Troubleshoot_Disable_Instance_Broadcast', false, { + type: 'boolean', + alert: 'Troubleshoot_Disable_Instance_Broadcast_Alert', + }); + this.add('Troubleshoot_Disable_Sessions_Monitor', false, { + type: 'boolean', + alert: 'Troubleshoot_Disable_Sessions_Monitor_Alert', + }); + this.add('Troubleshoot_Disable_Livechat_Activity_Monitor', false, { + type: 'boolean', + alert: 'Troubleshoot_Disable_Livechat_Activity_Monitor_Alert', + }); + this.add('Troubleshoot_Disable_Statistics_Generator', false, { + type: 'boolean', + alert: 'Troubleshoot_Disable_Statistics_Generator_Alert', + }); + this.add('Troubleshoot_Disable_Data_Exporter_Processor', false, { + type: 'boolean', + alert: 'Troubleshoot_Disable_Data_Exporter_Processor_Alert', + }); + this.add('Troubleshoot_Disable_Workspace_Sync', false, { + type: 'boolean', + alert: 'Troubleshoot_Disable_Workspace_Sync_Alert', + }); +}); diff --git a/app/lib/server/startup/settingsOnLoadCdnPrefix.js b/app/lib/server/startup/settingsOnLoadCdnPrefix.js index 3c3e62bb356e8..84d307dbc375c 100644 --- a/app/lib/server/startup/settingsOnLoadCdnPrefix.js +++ b/app/lib/server/startup/settingsOnLoadCdnPrefix.js @@ -2,19 +2,19 @@ import { Meteor } from 'meteor/meteor'; import { WebAppInternals } from 'meteor/webapp'; import _ from 'underscore'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; function testWebAppInternals(fn) { typeof WebAppInternals !== 'undefined' && fn(WebAppInternals); } -settings.onload('CDN_PREFIX', function(key, value) { +settings.change('CDN_PREFIX', function(value) { const useForAll = settings.get('CDN_PREFIX_ALL'); if (_.isString(value) && value.trim() && useForAll) { return testWebAppInternals((WebAppInternals) => WebAppInternals.setBundledJsCssPrefix(value)); } }); -settings.onload('CDN_JSCSS_PREFIX', function(key, value) { +settings.change('CDN_JSCSS_PREFIX', function(value) { const useForAll = settings.get('CDN_PREFIX_ALL'); if (_.isString(value) && value.trim() && !useForAll) { return testWebAppInternals((WebAppInternals) => WebAppInternals.setBundledJsCssPrefix(value)); diff --git a/app/lib/server/startup/settingsOnLoadDirectReply.js b/app/lib/server/startup/settingsOnLoadDirectReply.js index 73675065cb5bf..e85f4b6000ab3 100644 --- a/app/lib/server/startup/settingsOnLoadDirectReply.js +++ b/app/lib/server/startup/settingsOnLoadDirectReply.js @@ -1,37 +1,39 @@ import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; -import { settings } from '../../../settings'; +import { Logger } from '../../../logger/server'; +import { settings } from '../../../settings/server'; import { IMAPIntercepter, POP3Helper, POP3 } from '../lib/interceptDirectReplyEmails.js'; +const logger = new Logger('Email Intercepter'); + let IMAP; let _POP3Helper; -const startEmailIntercepter = _.debounce(Meteor.bindEnvironment(function() { - console.log('Starting Email Intercepter...'); +settings.watchMultiple(['Direct_Reply_Enable', 'Direct_Reply_Protocol', 'Direct_Reply_Host', 'Direct_Reply_Port', 'Direct_Reply_Username', 'Direct_Reply_Password', 'Direct_Reply_Protocol', 'Direct_Reply_Protocol'], function() { + logger.debug('Starting Email Intercepter...'); if (settings.get('Direct_Reply_Enable') && settings.get('Direct_Reply_Protocol') && settings.get('Direct_Reply_Host') && settings.get('Direct_Reply_Port') && settings.get('Direct_Reply_Username') && settings.get('Direct_Reply_Password')) { if (settings.get('Direct_Reply_Protocol') === 'IMAP') { // stop already running IMAP instance if (IMAP && IMAP.isActive()) { - console.log('Disconnecting already running IMAP instance...'); + logger.debug('Disconnecting already running IMAP instance...'); IMAP.stop(Meteor.bindEnvironment(function() { - console.log('Starting new IMAP instance......'); + logger.debug('Starting new IMAP instance......'); IMAP = new IMAPIntercepter(); IMAP.start(); return true; })); } else if (POP3 && _POP3Helper && _POP3Helper.isActive()) { - console.log('Disconnecting already running POP instance...'); + logger.debug('Disconnecting already running POP instance...'); _POP3Helper.stop(Meteor.bindEnvironment(function() { - console.log('Starting new IMAP instance......'); + logger.debug('Starting new IMAP instance......'); IMAP = new IMAPIntercepter(); IMAP.start(); return true; })); } else { - console.log('Starting new IMAP instance......'); + logger.debug('Starting new IMAP instance......'); IMAP = new IMAPIntercepter(); IMAP.start(); return true; @@ -39,23 +41,23 @@ const startEmailIntercepter = _.debounce(Meteor.bindEnvironment(function() { } else if (settings.get('Direct_Reply_Protocol') === 'POP') { // stop already running POP instance if (POP3 && _POP3Helper && _POP3Helper.isActive()) { - console.log('Disconnecting already running POP instance...'); + logger.debug('Disconnecting already running POP instance...'); _POP3Helper.stop(Meteor.bindEnvironment(function() { - console.log('Starting new POP instance......'); + logger.debug('Starting new POP instance......'); _POP3Helper = new POP3Helper(); _POP3Helper.start(); return true; })); } else if (IMAP && IMAP.isActive()) { - console.log('Disconnecting already running IMAP instance...'); + logger.debug('Disconnecting already running IMAP instance...'); IMAP.stop(Meteor.bindEnvironment(function() { - console.log('Starting new POP instance......'); + logger.debug('Starting new POP instance......'); _POP3Helper = new POP3Helper(); _POP3Helper.start(); return true; })); } else { - console.log('Starting new POP instance......'); + logger.debug('Starting new POP instance......'); _POP3Helper = new POP3Helper(); _POP3Helper.start(); return true; @@ -68,6 +70,4 @@ const startEmailIntercepter = _.debounce(Meteor.bindEnvironment(function() { // stop POP3 instance _POP3Helper.stop(); } -}), 1000); - -settings.onload(/^Direct_Reply_.+/, startEmailIntercepter); +}); diff --git a/app/lib/server/startup/settingsOnLoadSMTP.js b/app/lib/server/startup/settingsOnLoadSMTP.js deleted file mode 100644 index 4ad9b806933d5..0000000000000 --- a/app/lib/server/startup/settingsOnLoadSMTP.js +++ /dev/null @@ -1,68 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; - -import { settings } from '../../../settings'; - -const buildMailURL = _.debounce(function() { - console.log('Updating process.env.MAIL_URL'); - - if (settings.get('SMTP_Host')) { - process.env.MAIL_URL = `${ settings.get('SMTP_Protocol') }://`; - - if (settings.get('SMTP_Username') && settings.get('SMTP_Password')) { - process.env.MAIL_URL += `${ encodeURIComponent(settings.get('SMTP_Username')) }:${ encodeURIComponent(settings.get('SMTP_Password')) }@`; - } - - process.env.MAIL_URL += encodeURIComponent(settings.get('SMTP_Host')); - - if (settings.get('SMTP_Port')) { - process.env.MAIL_URL += `:${ parseInt(settings.get('SMTP_Port')) }`; - } - - process.env.MAIL_URL += `?pool=${ settings.get('SMTP_Pool') }`; - - if (settings.get('SMTP_Protocol') === 'smtp' && settings.get('SMTP_IgnoreTLS')) { - process.env.MAIL_URL += '&secure=false&ignoreTLS=true'; - } - - return process.env.MAIL_URL; - } -}, 500); - -settings.onload('SMTP_Host', function(key, value) { - if (_.isString(value)) { - return buildMailURL(); - } -}); - -settings.onload('SMTP_Port', function() { - return buildMailURL(); -}); - -settings.onload('SMTP_Username', function(key, value) { - if (_.isString(value)) { - return buildMailURL(); - } -}); - -settings.onload('SMTP_Password', function(key, value) { - if (_.isString(value)) { - return buildMailURL(); - } -}); - -settings.onload('SMTP_Protocol', function() { - return buildMailURL(); -}); - -settings.onload('SMTP_Pool', function() { - return buildMailURL(); -}); - -settings.onload('SMTP_IgnoreTLS', function() { - return buildMailURL(); -}); - -Meteor.startup(function() { - return buildMailURL(); -}); diff --git a/app/lib/server/startup/settingsOnLoadSMTP.ts b/app/lib/server/startup/settingsOnLoadSMTP.ts new file mode 100644 index 0000000000000..6d1b9340d9b83 --- /dev/null +++ b/app/lib/server/startup/settingsOnLoadSMTP.ts @@ -0,0 +1,34 @@ +import { settings } from '../../../settings/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; + +settings.watchMultiple(['SMTP_Host', + 'SMTP_Port', + 'SMTP_Username', + 'SMTP_Password', + 'SMTP_Protocol', + 'SMTP_Pool', + 'SMTP_IgnoreTLS'], function() { + SystemLogger.info('Updating process.env.MAIL_URL'); + + if (settings.get('SMTP_Host')) { + process.env.MAIL_URL = `${ settings.get('SMTP_Protocol') }://`; + + if (settings.get('SMTP_Username') && settings.get('SMTP_Password')) { + process.env.MAIL_URL += `${ encodeURIComponent(settings.get('SMTP_Username')) }:${ encodeURIComponent(settings.get('SMTP_Password')) }@`; + } + + process.env.MAIL_URL += encodeURIComponent(settings.get('SMTP_Host')); + + if (settings.get('SMTP_Port')) { + process.env.MAIL_URL += `:${ parseInt(settings.get('SMTP_Port')) }`; + } + + process.env.MAIL_URL += `?pool=${ settings.get('SMTP_Pool') }`; + + if (settings.get('SMTP_Protocol') === 'smtp' && settings.get('SMTP_IgnoreTLS')) { + process.env.MAIL_URL += '&secure=false&ignoreTLS=true'; + } + + return process.env.MAIL_URL; + } +}); diff --git a/app/lib/server/startup/settingsOnLoadSiteUrl.ts b/app/lib/server/startup/settingsOnLoadSiteUrl.ts new file mode 100644 index 0000000000000..fecadde870619 --- /dev/null +++ b/app/lib/server/startup/settingsOnLoadSiteUrl.ts @@ -0,0 +1,31 @@ +import { Meteor } from 'meteor/meteor'; +import { WebAppInternals } from 'meteor/webapp'; + +import { settings } from '../../../settings/server'; + +export let hostname: string; + +settings.watch('Site_Url', function(value) { + if (value == null || value.trim() === '') { + return; + } + let host = value.replace(/\/$/, ''); + // let prefix = ''; + const match = value.match(/([^\/]+\/{2}[^\/]+)(\/.+)/); + if (match != null) { + host = match[1]; + // prefix = match[2].replace(/\/$/, ''); + } + (global as any).__meteor_runtime_config__.ROOT_URL = value; + + if (Meteor.absoluteUrl.defaultOptions && Meteor.absoluteUrl.defaultOptions.rootUrl) { + Meteor.absoluteUrl.defaultOptions.rootUrl = value; + } + + hostname = host.replace(/^https?:\/\//, ''); + process.env.MOBILE_ROOT_URL = host; + process.env.MOBILE_DDP_URL = host; + if (typeof WebAppInternals !== 'undefined' && WebAppInternals.generateBoilerplate) { + return WebAppInternals.generateBoilerplate(); + } +}); diff --git a/app/lib/startup/index.js b/app/lib/startup/index.js deleted file mode 100644 index 06021c7418326..0000000000000 --- a/app/lib/startup/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import * as Mailer from '../../mailer'; -import { settings } from '../../settings'; - -Mailer.setSettings(settings); diff --git a/app/lib/tests/server.tests.js b/app/lib/tests/server.tests.js index cc6a4de04b1af..a606cff94901a 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 7af6388409b1d..f99571f0e66d0 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/lib/dateHandler.js b/app/livechat/client/lib/dateHandler.js index ac7d4167aa53f..8639280b2ee62 100644 --- a/app/livechat/client/lib/dateHandler.js +++ b/app/livechat/client/lib/dateHandler.js @@ -1,7 +1,6 @@ import moment from 'moment'; -import { handleError } from '../../../utils'; - +import { handleError } from '../../../../client/lib/utils/handleError'; /** * Check if given daterange matches any of pre-defined options diff --git a/app/livechat/client/lib/stream/queueManager.js b/app/livechat/client/lib/stream/queueManager.js index 526c5158cf4f9..97e9bcf0fb681 100644 --- a/app/livechat/client/lib/stream/queueManager.js +++ b/app/livechat/client/lib/stream/queueManager.js @@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { APIClient } from '../../../../utils/client'; import { LivechatInquiry } from '../../collections/LivechatInquiry'; import { inquiryDataStream } from './inquiry'; -import { call } from '../../../../ui-utils/client'; +import { callWithErrorHandling } from '../../../../../client/lib/utils/callWithErrorHandling'; import { getUserPreference } from '../../../../utils'; import { CustomSounds } from '../../../../custom-sounds/client/lib/CustomSounds'; @@ -13,9 +13,8 @@ const newInquirySound = () => { const userId = Meteor.userId(); const audioVolume = getUserPreference(userId, 'notificationsSoundVolume'); const newRoomNotification = getUserPreference(userId, 'newRoomNotification'); - const audioNotificationValue = getUserPreference(userId, 'audioNotifications'); - if (audioNotificationValue !== 'none') { + if (newRoomNotification !== 'none') { CustomSounds.play(newRoomNotification, { volume: Number((audioVolume / 100).toPrecision(2)), }); @@ -80,7 +79,7 @@ const addGlobalListener = () => { const subscribe = async (userId) => { - const config = await call('livechat:getRoutingConfig'); + const config = await callWithErrorHandling('livechat:getRoutingConfig'); if (config && config.autoAssignAgent) { return; } diff --git a/app/livechat/client/views/app/dialog/closeRoom.js b/app/livechat/client/views/app/dialog/closeRoom.js index 2dac3bc7956af..42a9f1f512849 100644 --- a/app/livechat/client/views/app/dialog/closeRoom.js +++ b/app/livechat/client/views/app/dialog/closeRoom.js @@ -5,9 +5,10 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { settings } from '../../../../../settings'; import { modal } from '../../../../../ui-utils/client'; -import { APIClient, handleError, t } from '../../../../../utils'; +import { APIClient, t } from '../../../../../utils'; import { hasRole } from '../../../../../authorization'; import './closeRoom.html'; +import { handleError } from '../../../../../../client/lib/utils/handleError'; const validateRoomComment = (comment) => { if (!settings.get('Livechat_request_comment_when_closing_conversation')) { diff --git a/app/livechat/client/views/app/livechatReadOnly.js b/app/livechat/client/views/app/livechatReadOnly.js index 765088722c269..74dce229f25cb 100644 --- a/app/livechat/client/views/app/livechatReadOnly.js +++ b/app/livechat/client/views/app/livechatReadOnly.js @@ -4,7 +4,7 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { ChatRoom, CachedChatRoom } from '../../../../models'; -import { call } from '../../../../ui-utils/client'; +import { callWithErrorHandling } from '../../../../../client/lib/utils/callWithErrorHandling'; import './livechatReadOnly.html'; import { APIClient } from '../../../../utils/client'; import { inquiryDataStream } from '../../lib/stream/inquiry'; @@ -42,7 +42,7 @@ Template.livechatReadOnly.events({ const inquiry = instance.inquiry.get(); const { _id } = inquiry; - await call('livechat:takeInquiry', _id, { clientAction: true }); + await callWithErrorHandling('livechat:takeInquiry', _id, { clientAction: true }); instance.loadInquiry(inquiry.rid); }, @@ -52,7 +52,7 @@ Template.livechatReadOnly.events({ const room = instance.room.get(); - await call('livechat:resumeOnHold', room._id, { clientAction: true }); + await callWithErrorHandling('livechat:resumeOnHold', room._id, { clientAction: true }); }, }); @@ -64,7 +64,7 @@ Template.livechatReadOnly.onCreated(function() { this.preparing = new ReactiveVar(true); this.updateInquiry = async ({ clientAction, ...inquiry }) => { - if (clientAction === 'removed' || !await call('canAccessRoom', inquiry.rid, Meteor.userId())) { + if (clientAction === 'removed') { // this will force to refresh the room // since the client wont get notified of room changes when chats are on queue (no one assigned) // a better approach should be performed when refactoring these templates to use react diff --git a/app/livechat/client/views/app/tabbar/agentEdit.js b/app/livechat/client/views/app/tabbar/agentEdit.js index e46c66e376082..b079aba9da777 100644 --- a/app/livechat/client/views/app/tabbar/agentEdit.js +++ b/app/livechat/client/views/app/tabbar/agentEdit.js @@ -1,12 +1,13 @@ import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; -import toastr from 'toastr'; import { getCustomFormTemplate } from '../customTemplates/register'; import './agentEdit.html'; import { hasPermission } from '../../../../../authorization'; -import { t, handleError, APIClient } from '../../../../../utils/client'; +import { t, APIClient } from '../../../../../utils/client'; +import { handleError } from '../../../../../../client/lib/utils/handleError'; +import { dispatchToastMessage } from '../../../../../../client/lib/toast'; Template.agentEdit.helpers({ canEditDepartment() { @@ -82,7 +83,7 @@ Template.agentEdit.events({ return handleError(error); } - toastr.success(t('Saved')); + dispatchToastMessage({ type: 'success', message: t('Saved') }); return this.back && this.back(_id); }); }, diff --git a/app/livechat/client/views/app/tabbar/agentInfo.js b/app/livechat/client/views/app/tabbar/agentInfo.js index 6894586f32dac..c90de337a12e9 100644 --- a/app/livechat/client/views/app/tabbar/agentInfo.js +++ b/app/livechat/client/views/app/tabbar/agentInfo.js @@ -9,8 +9,9 @@ import s from 'underscore.string'; import { getCustomFormTemplate } from '../customTemplates/register'; import './agentInfo.html'; import { modal } from '../../../../../ui-utils'; -import { t, handleError, APIClient } from '../../../../../utils/client'; +import { t, APIClient } from '../../../../../utils/client'; import { hasPermission } from '../../../../../authorization'; +import { handleError } from '../../../../../../client/lib/utils/handleError'; const customFieldsTemplate = () => getCustomFormTemplate('livechatAgentInfoForm'); diff --git a/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.html b/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.html index 1b5e7ec2b104e..7b83a9db8de7f 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}} -
-
    - {{# with messageContext}} - {{#each msg in messages}}{{> message msg=msg room=room subscription=subscription settings=settings u=u}}{{/each}} - {{/with}} + {{#if hasError}} +
    +
    +

    {{_ "Not_found_or_not_allowed"}}

    +
    +
    + {{else}} +
    +
      + {{# with messageContext}} + {{#each msg in messages}}{{> message msg=msg room=room subscription=subscription settings=settings u=u}}{{/each}} + {{/with}} - {{#if isLoading}} - {{> loading}} - {{/if}} -
    -
    + {{#if isLoading}} + {{> loading}} + {{/if}} +
+
+ {{/if}} {{/if}} diff --git a/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.js b/app/livechat/client/views/app/tabbar/contactChatHistoryMessages.js index c73136c07a8f6..cb3171f67c65b 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/client/views/app/tabbar/visitorEdit.js b/app/livechat/client/views/app/tabbar/visitorEdit.js index 536d0b77e275d..cdacf8046d37f 100644 --- a/app/livechat/client/views/app/tabbar/visitorEdit.js +++ b/app/livechat/client/views/app/tabbar/visitorEdit.js @@ -1,13 +1,13 @@ import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; -import toastr from 'toastr'; import { t } from '../../../../../utils'; -import { hasAtLeastOnePermission, hasPermission, hasRole } from '../../../../../authorization'; +import { hasAtLeastOnePermission, hasPermission, hasRole } from '../../../../../authorization/client'; import './visitorEdit.html'; import { APIClient } from '../../../../../utils/client'; import { getCustomFormTemplate } from '../customTemplates/register'; +import { dispatchToastMessage } from '../../../../../../client/lib/toast'; const CUSTOM_FIELDS_COUNT = 100; @@ -177,9 +177,15 @@ Template.visitorEdit.events({ Meteor.call('livechat:saveInfo', userData, roomData, (err) => { if (err) { - toastr.error(t(err.error)); + dispatchToastMessage({ + type: 'error', + message: t(err.error), + }); } else { - toastr.success(t('Saved')); + dispatchToastMessage({ + type: 'success', + message: t('Saved'), + }); this.save(); } }); diff --git a/app/livechat/client/views/app/tabbar/visitorForward.js b/app/livechat/client/views/app/tabbar/visitorForward.js index 871f6448f31a4..d4aac56a8357e 100644 --- a/app/livechat/client/views/app/tabbar/visitorForward.js +++ b/app/livechat/client/views/app/tabbar/visitorForward.js @@ -2,12 +2,12 @@ import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { Template } from 'meteor/templating'; -import toastr from 'toastr'; import { ChatRoom } from '../../../../../models'; import { t } from '../../../../../utils'; import './visitorForward.html'; import { APIClient } from '../../../../../utils/client'; +import { dispatchToastMessage } from '../../../../../../client/lib/toast'; Template.visitorForward.helpers({ visitor() { @@ -128,13 +128,22 @@ Template.visitorForward.events({ Meteor.call('livechat:transfer', transferData, (error, result) => { if (error) { - toastr.error(t(error.error)); + dispatchToastMessage({ + type: 'error', + message: t(error.error), + }); } else if (result) { this.save(); - toastr.success(t('Transferred')); + dispatchToastMessage({ + type: 'success', + message: t('Transferred'), + }); FlowRouter.go('/'); } else { - toastr.warning(t('No_available_agents_to_transfer')); + dispatchToastMessage({ + type: 'warning', + message: t('No_available_agents_to_transfer'), + }); } }); }, diff --git a/app/livechat/client/views/app/tabbar/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index 56de7d9275c0b..a58246dbd1641 100644 --- a/app/livechat/client/views/app/tabbar/visitorInfo.js +++ b/app/livechat/client/views/app/tabbar/visitorInfo.js @@ -11,14 +11,15 @@ import UAParser from 'ua-parser-js'; import { modal } from '../../../../../ui-utils'; import { Subscriptions } from '../../../../../models'; import { settings } from '../../../../../settings'; -import { t, handleError, roomTypes } from '../../../../../utils'; +import { t, roomTypes } from '../../../../../utils'; import { hasRole, hasPermission, hasAtLeastOnePermission } from '../../../../../authorization'; import './visitorInfo.html'; import { APIClient } from '../../../../../utils/client'; import { RoomManager } from '../../../../../ui-utils/client'; -import { DateFormat } from '../../../../../lib/client'; import { getCustomFormTemplate } from '../customTemplates/register'; import { Markdown } from '../../../../../markdown/client'; +import { handleError } from '../../../../../../client/lib/utils/handleError'; +import { formatDateAndTime } from '../../../../../../client/lib/utils/formatDateAndTime'; const isSubscribedToRoom = () => { const data = Template.currentData(); @@ -213,7 +214,7 @@ Template.visitorInfo.helpers({ roomClosedDateTime() { const { closedAt } = this; - return DateFormat.formatDateAndTime(closedAt); + return formatDateAndTime(closedAt); }, roomClosedBy() { @@ -249,7 +250,7 @@ Template.visitorInfo.helpers({ transcriptRequestedDateTime() { const { requestedAt } = this; - return DateFormat.formatDateAndTime(requestedAt); + return formatDateAndTime(requestedAt); }, markdown(text) { diff --git a/app/livechat/client/views/app/tabbar/visitorTranscript.js b/app/livechat/client/views/app/tabbar/visitorTranscript.js index ae06c775aff2b..d99c02c82aa55 100644 --- a/app/livechat/client/views/app/tabbar/visitorTranscript.js +++ b/app/livechat/client/views/app/tabbar/visitorTranscript.js @@ -1,9 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; -import toastr from 'toastr'; -import { t, isEmail, handleError, roomTypes } from '../../../../../utils'; +import { dispatchToastMessage } from '../../../../../../client/lib/toast'; +import { handleError } from '../../../../../../client/lib/utils/handleError'; +import { t, roomTypes } from '../../../../../utils'; +import { isEmail } from '../../../../../../lib/utils/isEmail'; import { APIClient } from '../../../../../utils/client'; import './visitorTranscript.html'; @@ -99,7 +101,7 @@ Template.visitorTranscript.events({ return handleError(err); } - toastr.success(t('Your_email_has_been_queued_for_sending')); + dispatchToastMessage({ type: 'success', message: t('Your_email_has_been_queued_for_sending') }); this.save(); }); }, @@ -122,7 +124,7 @@ Template.visitorTranscript.events({ return handleError(err); } - toastr.success(t('Livechat_transcript_has_been_requested')); + dispatchToastMessage({ type: 'success', message: t('Livechat_transcript_has_been_requested') }); this.save(); }); }, @@ -138,7 +140,7 @@ Template.visitorTranscript.events({ return handleError(err); } - toastr.success(t('Livechat_transcript_request_has_been_canceled')); + dispatchToastMessage({ type: 'success', message: t('Livechat_transcript_request_has_been_canceled') }); this.save(); }); }, diff --git a/app/livechat/imports/server/rest/facebook.js b/app/livechat/imports/server/rest/facebook.js index b4b8efa550349..cb9f19afc86c4 100644 --- a/app/livechat/imports/server/rest/facebook.js +++ b/app/livechat/imports/server/rest/facebook.js @@ -90,7 +90,7 @@ API.v1.addRoute('livechat/facebook', { message: Livechat.sendMessage(sendMessage), }; } catch (e) { - console.error('Error using Facebook ->', e); + Livechat.logger.error('Error using Facebook ->', e); } }, }); diff --git a/app/livechat/imports/server/rest/rooms.js b/app/livechat/imports/server/rest/rooms.js index d2f4c6e20d63f..9680b8baffce1 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 fe627ba6d817a..ac180f4c652ea 100644 --- a/app/livechat/imports/server/rest/sms.js +++ b/app/livechat/imports/server/rest/sms.js @@ -7,6 +7,7 @@ import { LivechatRooms, LivechatVisitors, LivechatDepartment } from '../../../.. import { API } from '../../../../api/server'; import { SMS } from '../../../../sms'; import { Livechat } from '../../../server/lib/Livechat'; +import { OmnichannelSourceType } from '../../../../../definition/IRoom'; const getUploadFile = (details, fileUrl) => { const response = HTTP.get(fileUrl, { npmRequestOptions: { encoding: null } }); @@ -29,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(), @@ -44,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); @@ -69,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(); @@ -83,6 +88,10 @@ API.v1.addRoute('livechat/sms-incoming/:service', { sms: { from: sms.to, }, + source: { + type: OmnichannelSourceType.SMS, + alias: this.urlParams.service, + }, }, }; @@ -132,7 +141,7 @@ API.v1.addRoute('livechat/sms-incoming/:service', { attachment.video_size = file.size; } } catch (e) { - console.error(`Attachment upload failed: ${ e.message }`); + Livechat.logger.error(`Attachment upload failed: ${ e.message }`); attachment = { fields: [{ title: 'User upload failed', diff --git a/app/livechat/imports/server/rest/upload.js b/app/livechat/imports/server/rest/upload.js index b51e9318103c0..f200edd6e523d 100644 --- a/app/livechat/imports/server/rest/upload.js +++ b/app/livechat/imports/server/rest/upload.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import filesize from 'filesize'; -import { settings } from '../../../../settings'; +import { settings } from '../../../../settings/server'; import { Settings, LivechatRooms, LivechatVisitors } from '../../../../models'; import { fileUploadIsValidContentType } from '../../../../utils/server'; import { FileUpload } from '../../../../file-upload'; @@ -10,7 +10,7 @@ import { getUploadFormData } from '../../../../api/server/lib/getUploadFormData' let maxFileSize; -settings.get('FileUpload_MaxFileSize', function(key, value) { +settings.watch('FileUpload_MaxFileSize', function(value) { try { maxFileSize = parseInt(value); } catch (e) { @@ -72,6 +72,6 @@ API.v1.addRoute('livechat/upload/:rid', { uploadedFile.description = fields.description; delete fields.description; - API.v1.success(Meteor.call('sendFileLivechatMessage', this.urlParams.rid, visitorToken, uploadedFile, fields)); + return API.v1.success(Meteor.call('sendFileLivechatMessage', this.urlParams.rid, visitorToken, uploadedFile, fields)); }, }); diff --git a/app/livechat/imports/server/rest/visitors.ts b/app/livechat/imports/server/rest/visitors.ts new file mode 100644 index 0000000000000..e75d4a955a2ad --- /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 bde52192cbe9d..fb6fa4c10160f 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/agentStatus.js b/app/livechat/server/agentStatus.js index 7ee9e84827b5e..a15d67f90eb71 100644 --- a/app/livechat/server/agentStatus.js +++ b/app/livechat/server/agentStatus.js @@ -1,10 +1,10 @@ import { UserPresenceMonitor } from 'meteor/konecty:user-presence'; import { Livechat } from './lib/Livechat'; -import { hasRole } from '../../authorization'; +import { hasAnyRole } from '../../authorization/server/functions/hasRole'; UserPresenceMonitor.onSetUserStatus((user, status) => { - if (hasRole(user._id, 'livechat-manager') || hasRole(user._id, 'livechat-monitor') || hasRole(user._id, 'livechat-agent')) { + if (hasAnyRole(user._id, ['livechat-manager', 'livechat-monitor', 'livechat-agent'])) { Livechat.notifyAgentStatusChanged(user._id, status); } }); diff --git a/app/livechat/server/api.js b/app/livechat/server/api.js index 6a13dddc86bf8..7aa0ee39c4a37 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 0a70d1b6fca44..1e70a709444f2 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/inquiries.js b/app/livechat/server/api/lib/inquiries.js index e56392d69b145..ecfa3b902939c 100644 --- a/app/livechat/server/api/lib/inquiries.js +++ b/app/livechat/server/api/lib/inquiries.js @@ -1,6 +1,6 @@ import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { LivechatDepartmentAgents, LivechatDepartment, LivechatInquiry } from '../../../../models/server/raw'; -import { hasRoleAsync } from '../../../../authorization/server/functions/hasRole'; +import { hasAnyRoleAsync } from '../../../../authorization/server/functions/hasRole'; const agentDepartments = async (userId) => { const agentDepartments = (await LivechatDepartmentAgents.findByAgentId(userId).toArray()).map(({ departmentId }) => departmentId); @@ -8,7 +8,7 @@ const agentDepartments = async (userId) => { }; const applyDepartmentRestrictions = async (userId, filterDepartment) => { - if (await hasRoleAsync(userId, 'livechat-manager')) { + if (await hasAnyRoleAsync(userId, ['livechat-manager'])) { return filterDepartment; } diff --git a/app/livechat/server/api/lib/livechat.js b/app/livechat/server/api/lib/livechat.js index a7a29598250f3..a8abb9115851c 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, @@ -108,18 +111,25 @@ export function settings() { forceAcceptDataProcessingConsent: initSettings.Livechat_force_accept_data_processing_consent, showConnecting: initSettings.Livechat_Show_Connecting, agentHiddenInfo: initSettings.Livechat_show_agent_info === false, + clearLocalStorageWhenChatEnded: initSettings.Livechat_clear_local_storage_when_chat_ended, limitTextLength: initSettings.Livechat_enable_message_character_limit - && (initSettings.Livechat_message_character_limit || initSettings.Message_MaxAllowedSize), + && (initSettings.Livechat_message_character_limit || initSettings.Message_MaxAllowedSize), }, theme: { title: initSettings.Livechat_title, 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 72d84803de153..957911737f0aa 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 c0366bf1ac694..d03566d6da998 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/rest.js b/app/livechat/server/api/rest.js index 1991a9f2fce9c..7273c565aac91 100644 --- a/app/livechat/server/api/rest.js +++ b/app/livechat/server/api/rest.js @@ -1,5 +1,5 @@ import './v1/config.js'; -import './v1/visitor.js'; +import './v1/visitor'; import './v1/transcript.js'; import './v1/offlineMessage.js'; import './v1/pageVisited.js'; diff --git a/app/livechat/server/api/v1/config.js b/app/livechat/server/api/v1/config.js index e43509d4ba3ca..f1a49c1ed3955 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 df8ac26a07f7a..0fd39bc5d0867 100644 --- a/app/livechat/server/api/v1/message.js +++ b/app/livechat/server/api/v1/message.js @@ -10,6 +10,7 @@ import { findGuest, findRoom, normalizeHttpHeaderData } from '../lib/livechat'; import { Livechat } from '../../lib/Livechat'; import { normalizeMessageFileUpload } from '../../../../utils/server/functions/normalizeMessageFileUpload'; import { settings } from '../../../../settings/server'; +import { OmnichannelSourceType } from '../../../../../definition/IRoom'; API.v1.addRoute('livechat/message', { post() { @@ -56,6 +57,11 @@ API.v1.addRoute('livechat/message', { token, }, agent, + roomInfo: { + source: { + type: this.isWidget() ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, + }, + }, }; const result = Promise.await(Livechat.sendMessage(sendMessage)); @@ -102,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 }); @@ -145,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 }); @@ -185,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: { @@ -245,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); @@ -305,6 +311,11 @@ API.v1.addRoute('livechat/messages', { authRequired: true }, { token: visitorToken, msg: message.msg, }, + roomInfo: { + source: { + type: this.isWidget() ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, + }, + }, }; const sentMessage = Promise.await(Livechat.sendMessage(sendMessage)); return { diff --git a/app/livechat/server/api/v1/room.js b/app/livechat/server/api/v1/room.js index ed5c28f13ab08..88c9e47171105 100644 --- a/app/livechat/server/api/v1/room.js +++ b/app/livechat/server/api/v1/room.js @@ -10,7 +10,7 @@ import { findGuest, findRoom, getRoom, settings, findAgent, onCheckRoomParams } import { Livechat } from '../../lib/Livechat'; import { normalizeTransferredByData } from '../../lib/Helper'; import { findVisitorInfo } from '../lib/visitors'; - +import { OmnichannelSourceType } from '../../../../../definition/IRoom'; API.v1.addRoute('livechat/room', { get() { @@ -46,7 +46,13 @@ API.v1.addRoute('livechat/room', { } const rid = Random.id(); - room = Promise.await(getRoom({ guest, rid, agent, extraParams })); + const roomInfo = { + source: { + type: this.isWidget() ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, + }, + }; + + room = Promise.await(getRoom({ guest, rid, agent, roomInfo, extraParams })); return API.v1.success(room); } @@ -160,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 4f7b5cfd524f9..6aef0c49537e2 100644 --- a/app/livechat/server/api/v1/videoCall.js +++ b/app/livechat/server/api/v1/videoCall.js @@ -1,11 +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() { @@ -26,15 +30,21 @@ API.v1.addRoute('livechat/video.call/:token', { } const rid = this.queryParams.rid || Random.id(); - const roomInfo = { jitsiTimeout: new Date(Date.now() + 3600 * 1000) }; + const roomInfo = { + jitsiTimeout: new Date(Date.now() + 3600 * 1000), + source: { + type: OmnichannelSourceType.API, + alias: 'video-call', + }, + }; 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')) { @@ -50,9 +60,108 @@ API.v1.addRoute('livechat/video.call/:token', { timeout: new Date(Date.now() + 3600 * 1000), }; + return API.v1.success(this.deprecationWarning({ videoCall })); + } catch (e) { + return API.v1.failure(e); + } + }, +}); + +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 deleted file mode 100644 index 98007540876c5..0000000000000 --- a/app/livechat/server/api/v1/visitor.js +++ /dev/null @@ -1,153 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; - -import { LivechatRooms, LivechatVisitors, LivechatCustomField } from '../../../../models'; -import { hasPermission } from '../../../../authorization'; -import { API } from '../../../../api/server'; -import { findGuest, normalizeHttpHeaderData } from '../lib/livechat'; -import { Livechat } from '../../lib/Livechat'; - -API.v1.addRoute('livechat/visitor', { - post() { - try { - check(this.bodyParams, { - visitor: Match.ObjectIncluding({ - token: String, - name: Match.Maybe(String), - email: Match.Maybe(String), - department: Match.Maybe(String), - phone: Match.Maybe(String), - username: Match.Maybe(String), - customFields: Match.Maybe([ - Match.ObjectIncluding({ - key: String, - value: String, - overwrite: Boolean, - }), - ]), - }), - }); - - const { token, customFields } = this.bodyParams.visitor; - const guest = this.bodyParams.visitor; - - if (this.bodyParams.visitor.phone) { - guest.phone = { number: this.bodyParams.visitor.phone }; - } - - guest.connectionData = normalizeHttpHeaderData(this.request.headers); - const visitorId = Livechat.registerGuest(guest); - - let visitor = LivechatVisitors.getVisitorByToken(token); - // If it's updating an existing visitor, it must also update the roomInfo - const cursor = LivechatRooms.findOpenByVisitorToken(token); - cursor.forEach((room) => { - Livechat.saveRoomInfo(room, visitor); - }); - - if (customFields && customFields instanceof Array) { - customFields.forEach((field) => { - const customField = LivechatCustomField.findOneById(field.key); - if (!customField) { - return; - } - const { key, value, overwrite } = field; - if (customField.scope === 'visitor' && !LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite)) { - return API.v1.failure(); - } - }); - } - - visitor = LivechatVisitors.findOneById(visitorId); - return API.v1.success({ visitor }); - } catch (e) { - return API.v1.failure(e); - } - }, -}); - -API.v1.addRoute('livechat/visitor/:token', { - get() { - try { - check(this.urlParams, { - token: String, - }); - - const visitor = LivechatVisitors.getVisitorByToken(this.urlParams.token); - return API.v1.success({ visitor }); - } catch (e) { - return API.v1.failure(e.error); - } - }, - delete() { - try { - check(this.urlParams, { - token: String, - }); - - const visitor = LivechatVisitors.getVisitorByToken(this.urlParams.token); - if (!visitor) { - throw new Meteor.Error('invalid-token'); - } - - const { _id } = visitor; - const result = Livechat.removeGuest(_id); - if (result) { - return API.v1.success({ - visitor: { - _id, - ts: new Date().toISOString(), - }, - }); - } - - return API.v1.failure(); - } catch (e) { - return API.v1.failure(e.error); - } - }, -}); - -API.v1.addRoute('livechat/visitor/:token/room', { authRequired: true }, { - get() { - if (!hasPermission(this.userId, 'view-livechat-manager')) { - return API.v1.unauthorized(); - } - - const rooms = LivechatRooms.findOpenByVisitorToken(this.urlParams.token, { - fields: { - name: 1, - t: 1, - cl: 1, - u: 1, - usernames: 1, - servedBy: 1, - }, - }).fetch(); - return API.v1.success({ rooms }); - }, -}); - -API.v1.addRoute('livechat/visitor.status', { - post() { - try { - check(this.bodyParams, { - token: String, - status: String, - }); - - const { token, status } = this.bodyParams; - - const guest = findGuest(token); - if (!guest) { - throw new Meteor.Error('invalid-token'); - } - - Livechat.notifyGuestStatusChanged(token, status); - - return API.v1.success({ token, status }); - } catch (e) { - return API.v1.failure(e); - } - }, -}); diff --git a/app/livechat/server/api/v1/visitor.ts b/app/livechat/server/api/v1/visitor.ts new file mode 100644 index 0000000000000..dccc30e2c116a --- /dev/null +++ b/app/livechat/server/api/v1/visitor.ts @@ -0,0 +1,179 @@ +import { Meteor } from 'meteor/meteor'; +import { Match, check } from 'meteor/check'; + +import { LivechatRooms, LivechatVisitors, LivechatCustomField } from '../../../../models/server'; +import { LivechatVisitors as VisitorsRaw } from '../../../../models/server/raw'; +import { API } from '../../../../api/server'; +import { findGuest, normalizeHttpHeaderData } from '../lib/livechat'; +import { Livechat } from '../../lib/Livechat'; +import { ILivechatVisitorDTO } from '../../../../../definition/ILivechatVisitor'; +import { IRoom } from '../../../../../definition/IRoom'; + +API.v1.addRoute('livechat/visitor', { + async post() { + check(this.bodyParams, { + visitor: Match.ObjectIncluding({ + token: String, + name: Match.Maybe(String), + email: Match.Maybe(String), + department: Match.Maybe(String), + phone: Match.Maybe(String), + username: Match.Maybe(String), + customFields: Match.Maybe([ + Match.ObjectIncluding({ + key: String, + value: String, + overwrite: Boolean, + }), + ]), + }), + }); + + const { token, customFields } = this.bodyParams.visitor; + const guest: ILivechatVisitorDTO = { ...this.bodyParams.visitor }; + + if (this.bodyParams.visitor.phone) { + guest.phone = { number: this.bodyParams.visitor.phone as string }; + } + + guest.connectionData = normalizeHttpHeaderData(this.request.headers); + const visitorId = Livechat.registerGuest(guest); + + let visitor = await VisitorsRaw.getVisitorByToken(token, {}); + // If it's updating an existing visitor, it must also update the roomInfo + const cursor = LivechatRooms.findOpenByVisitorToken(token); + cursor.forEach((room: IRoom) => { + Livechat.saveRoomInfo(room, visitor); + }); + + if (customFields && customFields instanceof Array) { + customFields.forEach((field) => { + const customField = LivechatCustomField.findOneById(field.key); + if (!customField) { + return; + } + const { key, value, overwrite } = field; + if (customField.scope === 'visitor' && !LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite)) { + return API.v1.failure(); + } + }); + + visitor = await VisitorsRaw.findOneById(visitorId, {}); + } + + if (!visitor) { + throw new Meteor.Error('error-saving-visitor', 'An error ocurred while saving visitor'); + } + + return API.v1.success({ visitor }); + }, +}); + +API.v1.addRoute('livechat/visitor/:token', { + async get() { + check(this.urlParams, { + token: String, + }); + + const visitor = await VisitorsRaw.getVisitorByToken(this.urlParams.token, {}); + + if (!visitor) { + throw new Meteor.Error('invalid-token'); + } + + return API.v1.success({ visitor }); + }, + async delete() { + check(this.urlParams, { + token: String, + }); + + const visitor = await VisitorsRaw.getVisitorByToken(this.urlParams.token, {}); + if (!visitor) { + throw new Meteor.Error('invalid-token'); + } + + const rooms = LivechatRooms.findOpenByVisitorToken(this.urlParams.token, { + fields: { + name: 1, + t: 1, + cl: 1, + u: 1, + usernames: 1, + servedBy: 1, + }, + }).fetch(); + + if (rooms && rooms.length) { + throw new Meteor.Error('visitor-has-open-rooms', 'Cannot remove visitors with opened rooms'); + } + + const { _id } = visitor; + const result = Livechat.removeGuest(_id); + if (!result) { + throw new Meteor.Error('error-removing-visitor', 'An error ocurred while deleting visitor'); + } + + return API.v1.success({ + visitor: { + _id, + ts: new Date().toISOString(), + }, + }); + }, +}); + +API.v1.addRoute('livechat/visitor/:token/room', { authRequired: true, permissionsRequired: ['view-livechat-manager'] }, { + async get() { + const rooms = LivechatRooms.findOpenByVisitorToken(this.urlParams.token, { + fields: { + name: 1, + t: 1, + cl: 1, + u: 1, + usernames: 1, + servedBy: 1, + }, + }).fetch(); + return API.v1.success({ rooms }); + }, +}); + +API.v1.addRoute('livechat/visitor.callStatus', { + async post() { + 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'); + } + Livechat.updateCallStatus(callId, rid, callStatus, guest); + return API.v1.success({ token, callStatus }); + }, +}); + +API.v1.addRoute('livechat/visitor.status', { + async post() { + check(this.bodyParams, { + token: String, + status: String, + }); + + const { token, status } = this.bodyParams; + + const guest = findGuest(token); + if (!guest) { + throw new Meteor.Error('invalid-token'); + } + + Livechat.notifyGuestStatusChanged(token, status); + + return API.v1.success({ token, status }); + }, +}); diff --git a/app/livechat/server/business-hour/AbstractBusinessHour.ts b/app/livechat/server/business-hour/AbstractBusinessHour.ts index b9da24988ef38..01768b5240d09 100644 --- a/app/livechat/server/business-hour/AbstractBusinessHour.ts +++ b/app/livechat/server/business-hour/AbstractBusinessHour.ts @@ -19,6 +19,7 @@ export interface IBusinessHourBehavior { onStartBusinessHours(): Promise; afterSaveBusinessHours(businessHourData: ILivechatBusinessHour): Promise; allowAgentChangeServiceStatus(agentId: string): Promise; + changeAgentActiveStatus(agentId: string, status: string): Promise; } export interface IBusinessHourType { @@ -44,6 +45,10 @@ export abstract class AbstractBusinessHourBehavior { async allowAgentChangeServiceStatus(agentId: string): Promise { return this.UsersRepository.isAgentWithinBusinessHours(agentId); } + + async changeAgentActiveStatus(agentId: string, status: string): Promise { + return this.UsersRepository.setLivechatStatusIf(agentId, status, { livechatStatusSystemModified: true }, { livechatStatusSystemModified: true }); + } } export abstract class AbstractBusinessHourType { diff --git a/app/livechat/server/business-hour/BusinessHourManager.ts b/app/livechat/server/business-hour/BusinessHourManager.ts index cc849aa62e5d4..04505776f32bd 100644 --- a/app/livechat/server/business-hour/BusinessHourManager.ts +++ b/app/livechat/server/business-hour/BusinessHourManager.ts @@ -1,10 +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, @@ -86,6 +87,14 @@ export class BusinessHourManager { await this.createCronJobsForWorkHours(); } + async onLogin(agentId: string): Promise { + if (!settings.get('Livechat_enable_business_hours')) { + return this.behavior.changeAgentActiveStatus(agentId, 'available'); + } + + return Users.setLivechatStatusActiveBasedOnBusinessHours(agentId); + } + private setupCallbacks(): void { callbacks.add('livechat.removeAgentDepartment', this.behavior.onRemoveAgentFromDepartment.bind(this), callbacks.priority.HIGH, 'business-hour-livechat-on-remove-agent-department'); callbacks.add('livechat.afterRemoveDepartment', this.behavior.onRemoveDepartment.bind(this), callbacks.priority.HIGH, 'business-hour-livechat-after-remove-department'); diff --git a/app/livechat/server/business-hour/index.ts b/app/livechat/server/business-hour/index.ts index 6d6b96b6da7ae..fa25f40bf5036 100644 --- a/app/livechat/server/business-hour/index.ts +++ b/app/livechat/server/business-hour/index.ts @@ -1,4 +1,5 @@ import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; import { BusinessHourManager } from './BusinessHourManager'; import { SingleBusinessHourBehavior } from './Single'; @@ -12,4 +13,6 @@ Meteor.startup(() => { const { BusinessHourBehaviorClass } = callbacks.run('on-business-hour-start', { BusinessHourBehaviorClass: SingleBusinessHourBehavior }); businessHourManager.registerBusinessHourBehavior(new BusinessHourBehaviorClass()); businessHourManager.registerBusinessHourType(new DefaultBusinessHour()); + + Accounts.onLogin(async ({ user }: { user: any }) => user?.roles?.includes('livechat-agent') && !user?.roles?.includes('bot') && businessHourManager.onLogin(user._id)); }); diff --git a/app/livechat/server/config.js b/app/livechat/server/config.js deleted file mode 100644 index 45f59a8b26a90..0000000000000 --- a/app/livechat/server/config.js +++ /dev/null @@ -1,558 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(function() { - settings.addGroup('Omnichannel'); - - settings.add('Livechat_enabled', true, { - type: 'boolean', - group: 'Omnichannel', - public: true, - }); - - settings.add('Livechat_title', 'Rocket.Chat', { - type: 'string', - group: 'Omnichannel', - section: 'Livechat', - public: true, - }); - - settings.add('Livechat_title_color', '#C1272D', { - type: 'color', - editor: 'color', - allowedTypes: ['color', 'expression'], - group: 'Omnichannel', - section: 'Livechat', - public: true, - }); - - settings.add('Livechat_enable_message_character_limit', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'Livechat', - public: true, - }); - - settings.add('Livechat_message_character_limit', 0, { - type: 'int', - group: 'Omnichannel', - section: 'Livechat', - public: true, - }); - - settings.add('Livechat_display_offline_form', true, { - type: 'boolean', - group: 'Omnichannel', - public: true, - section: 'Livechat', - i18nLabel: 'Display_offline_form', - }); - - settings.add('Livechat_validate_offline_email', true, { - type: 'boolean', - group: 'Omnichannel', - public: true, - section: 'Livechat', - i18nLabel: 'Validate_email_address', - }); - - settings.add('Livechat_offline_form_unavailable', '', { - type: 'string', - group: 'Omnichannel', - public: true, - section: 'Livechat', - i18nLabel: 'Offline_form_unavailable_message', - }); - - settings.add('Livechat_offline_title', 'Leave a message', { - type: 'string', - group: 'Omnichannel', - public: true, - section: 'Livechat', - i18nLabel: 'Title', - }); - settings.add('Livechat_offline_title_color', '#666666', { - type: 'color', - editor: 'color', - allowedTypes: ['color', 'expression'], - group: 'Omnichannel', - public: true, - section: 'Livechat', - i18nLabel: 'Color', - }); - settings.add('Livechat_offline_message', '', { - type: 'string', - group: 'Omnichannel', - public: true, - section: 'Livechat', - i18nLabel: 'Instructions', - i18nDescription: 'Instructions_to_your_visitor_fill_the_form_to_send_a_message', - }); - settings.add('Livechat_offline_email', '', { - type: 'string', - group: 'Omnichannel', - i18nLabel: 'Email_address_to_send_offline_messages', - section: 'Livechat', - }); - settings.add('Livechat_offline_success_message', '', { - type: 'string', - group: 'Omnichannel', - public: true, - section: 'Livechat', - i18nLabel: 'Offline_success_message', - }); - - settings.add('Livechat_allow_switching_departments', true, { - type: 'boolean', - group: 'Omnichannel', - public: true, - section: 'Livechat', - i18nLabel: 'Allow_switching_departments', - }); - - settings.add('Livechat_show_agent_info', true, { - type: 'boolean', - group: 'Omnichannel', - section: 'Livechat', - public: true, - i18nLabel: 'Show_agent_info', - }); - - settings.add('Livechat_show_agent_email', true, { - type: 'boolean', - group: 'Omnichannel', - section: 'Livechat', - public: true, - enableQuery: { _id: 'Livechat_show_agent_info', value: true }, - i18nLabel: 'Show_agent_email', - }); - - settings.add('Livechat_request_comment_when_closing_conversation', true, { - type: 'boolean', - group: 'Omnichannel', - public: true, - i18nLabel: 'Request_comment_when_closing_conversation', - i18nDescription: 'Request_comment_when_closing_conversation_description', - }); - - settings.add('Livechat_conversation_finished_message', '', { - type: 'string', - group: 'Omnichannel', - section: 'Livechat', - public: true, - i18nLabel: 'Conversation_finished_message', - }); - - settings.add('Livechat_conversation_finished_text', '', { - type: 'string', - multiline: true, - group: 'Omnichannel', - section: 'Livechat', - public: true, - i18nLabel: 'Conversation_finished_text', - }); - - settings.add('Livechat_registration_form', true, { - type: 'boolean', - group: 'Omnichannel', - section: 'Livechat', - public: true, - i18nLabel: 'Show_preregistration_form', - }); - - settings.add('Livechat_name_field_registration_form', true, { - type: 'boolean', - group: 'Omnichannel', - section: 'Livechat', - public: true, - i18nLabel: 'Show_name_field', - }); - - settings.add('Livechat_email_field_registration_form', true, { - type: 'boolean', - group: 'Omnichannel', - section: 'Livechat', - public: true, - i18nLabel: 'Show_email_field', - }); - - settings.add('Livechat_guest_count', 1, { type: 'int', group: 'Omnichannel' }); - - settings.add('Livechat_Room_Count', 1, { - type: 'int', - group: 'Omnichannel', - i18nLabel: 'Livechat_room_count', - }); - - settings.add('Livechat_enabled_when_agent_idle', true, { - type: 'boolean', - group: 'Omnichannel', - i18nLabel: 'Accept_new_livechats_when_agent_is_idle', - }); - - settings.add('Livechat_webhookUrl', '', { - type: 'string', - group: 'Omnichannel', - section: 'CRM_Integration', - i18nLabel: 'Webhook_URL', - }); - - settings.add('Livechat_secret_token', '', { - type: 'string', - group: 'Omnichannel', - section: 'CRM_Integration', - i18nLabel: 'Secret_token', - secret: true, - }); - - settings.add('Livechat_webhook_on_start', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'CRM_Integration', - i18nLabel: 'Send_request_on_chat_start', - }); - - settings.add('Livechat_webhook_on_close', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'CRM_Integration', - i18nLabel: 'Send_request_on_chat_close', - }); - - settings.add('Livechat_webhook_on_chat_taken', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'CRM_Integration', - i18nLabel: 'Send_request_on_chat_taken', - }); - - settings.add('Livechat_webhook_on_chat_queued', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'CRM_Integration', - i18nLabel: 'Send_request_on_chat_queued', - }); - - settings.add('Livechat_webhook_on_forward', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'CRM_Integration', - i18nLabel: 'Send_request_on_forwarding', - }); - - settings.add('Livechat_webhook_on_offline_msg', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'CRM_Integration', - i18nLabel: 'Send_request_on_offline_messages', - }); - - settings.add('Livechat_webhook_on_visitor_message', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'CRM_Integration', - i18nLabel: 'Send_request_on_visitor_message', - }); - - settings.add('Livechat_webhook_on_agent_message', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'CRM_Integration', - i18nLabel: 'Send_request_on_agent_message', - }); - - settings.add('Send_visitor_navigation_history_livechat_webhook_request', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'CRM_Integration', - i18nLabel: 'Send_visitor_navigation_history_on_request', - i18nDescription: 'Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled', - enableQuery: { _id: 'Livechat_Visitor_navigation_as_a_message', value: true }, - }); - - settings.add('Livechat_webhook_on_capture', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'CRM_Integration', - i18nLabel: 'Send_request_on_lead_capture', - }); - - settings.add('Livechat_lead_email_regex', '\\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\\.)+[A-Z]{2,4}\\b', { - type: 'string', - group: 'Omnichannel', - section: 'CRM_Integration', - i18nLabel: 'Lead_capture_email_regex', - }); - - settings.add('Livechat_lead_phone_regex', '((?:\\([0-9]{1,3}\\)|[0-9]{2})[ \\-]*?[0-9]{4,5}(?:[\\-\\s\\_]{1,2})?[0-9]{4}(?:(?=[^0-9])|$)|[0-9]{4,5}(?:[\\-\\s\\_]{1,2})?[0-9]{4}(?:(?=[^0-9])|$))', { - type: 'string', - group: 'Omnichannel', - section: 'CRM_Integration', - i18nLabel: 'Lead_capture_phone_regex', - }); - - settings.add('Livechat_history_monitor_type', 'url', { - type: 'select', - group: 'Omnichannel', - section: 'Livechat', - i18nLabel: 'Monitor_history_for_changes_on', - values: [ - { key: 'url', i18nLabel: 'Page_URL' }, - { key: 'title', i18nLabel: 'Page_title' }, - ], - }); - - settings.add('Livechat_Visitor_navigation_as_a_message', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'Livechat', - public: true, - i18nLabel: 'Send_Visitor_navigation_history_as_a_message', - }); - - settings.addGroup('Omnichannel', function() { - this.section('Business_Hours', function() { - this.add('Livechat_enable_business_hours', false, { - type: 'boolean', - group: 'Omnichannel', - public: true, - i18nLabel: 'Business_hours_enabled', - }); - }); - }); - - settings.add('Livechat_continuous_sound_notification_new_livechat_room', false, { - type: 'boolean', - group: 'Omnichannel', - public: true, - i18nLabel: 'Continuous_sound_notifications_for_new_livechat_room', - }); - - settings.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 }, - }); - - settings.add('Livechat_fileupload_enabled', true, { - type: 'boolean', - group: 'Omnichannel', - public: true, - i18nLabel: 'FileUpload_Enabled', - enableQuery: { _id: 'FileUpload_Enabled', value: true }, - }); - - settings.add('Livechat_enable_transcript', false, { - type: 'boolean', - group: 'Omnichannel', - public: true, - i18nLabel: 'Transcript_Enabled', - }); - - settings.add('Livechat_transcript_message', '', { - type: 'string', - group: 'Omnichannel', - public: true, - i18nLabel: 'Transcript_message', - enableQuery: { _id: 'Livechat_enable_transcript', value: true }, - }); - - settings.add('Livechat_registration_form_message', '', { - type: 'string', - group: 'Omnichannel', - section: 'Livechat', - public: true, - i18nLabel: 'Livechat_registration_form_message', - }); - - settings.add('Livechat_AllowedDomainsList', '', { - type: 'string', - group: 'Omnichannel', - section: 'Livechat', - public: true, - i18nLabel: 'Livechat_AllowedDomainsList', - i18nDescription: 'Domains_allowed_to_embed_the_livechat_widget', - }); - - settings.add('Livechat_OfflineMessageToChannel_enabled', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'Livechat', - public: true, - }); - - settings.add('Livechat_OfflineMessageToChannel_channel_name', '', { - type: 'string', - group: 'Omnichannel', - section: 'Livechat', - public: true, - enableQuery: { _id: 'Livechat_OfflineMessageToChannel_enabled', value: true }, - i18nLabel: 'Channel_name', - }); - - settings.add('Livechat_Facebook_Enabled', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'Facebook', - }); - - settings.add('Livechat_Facebook_API_Key', '', { - type: 'string', - group: 'Omnichannel', - section: 'Facebook', - i18nDescription: 'If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours', - }); - - settings.add('Livechat_Facebook_API_Secret', '', { - type: 'string', - group: 'Omnichannel', - section: 'Facebook', - i18nDescription: 'If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours', - }); - - settings.add('Livechat_RDStation_Token', '', { - type: 'string', - group: 'Omnichannel', - public: false, - section: 'RD Station', - i18nLabel: 'RDStation_Token', - }); - - settings.add('Livechat_Routing_Method', 'Auto_Selection', { - type: 'select', - group: 'Omnichannel', - public: true, - section: 'Routing', - values: [ - { key: 'External', i18nLabel: 'External_Service' }, - { key: 'Auto_Selection', i18nLabel: 'Auto_Selection' }, - { key: 'Manual_Selection', i18nLabel: 'Manual_Selection' }, - ], - }); - - settings.add('Livechat_accept_chats_with_no_agents', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'Routing', - i18nLabel: 'Accept_with_no_online_agents', - i18nDescription: 'Accept_incoming_livechat_requests_even_if_there_are_no_online_agents', - }); - - settings.add('Livechat_assign_new_conversation_to_bot', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'Routing', - i18nLabel: 'Assign_new_conversations_to_bot_agent', - i18nDescription: 'Assign_new_conversations_to_bot_agent_description', - }); - - settings.add('Livechat_guest_pool_max_number_incoming_livechats_displayed', 0, { - type: 'int', - group: 'Omnichannel', - section: 'Routing', - public: true, - i18nLabel: 'Max_number_incoming_livechats_displayed', - i18nDescription: 'Max_number_incoming_livechats_displayed_description', - enableQuery: { _id: 'Livechat_Routing_Method', value: 'Manual_Selection' }, - }); - - settings.add('Livechat_show_queue_list_link', false, { - type: 'boolean', - group: 'Omnichannel', - public: true, - section: 'Routing', - i18nLabel: 'Show_queue_list_to_all_agents', - enableQuery: { _id: 'Livechat_Routing_Method', value: { $ne: 'External' } }, - }); - - settings.add('Livechat_External_Queue_URL', '', { - type: 'string', - group: 'Omnichannel', - public: false, - section: 'Routing', - i18nLabel: 'External_Queue_Service_URL', - i18nDescription: 'For_more_details_please_check_our_docs', - enableQuery: { _id: 'Livechat_Routing_Method', value: 'External' }, - }); - - settings.add('Livechat_External_Queue_Token', '', { - type: 'string', - group: 'Omnichannel', - public: false, - section: 'Routing', - i18nLabel: 'Secret_token', - enableQuery: { _id: 'Livechat_Routing_Method', value: 'External' }, - }); - - settings.add('Livechat_Allow_collect_and_store_HTTP_header_informations', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'GDPR', - public: true, - i18nLabel: 'Allow_collect_and_store_HTTP_header_informations', - i18nDescription: 'Allow_collect_and_store_HTTP_header_informations_description', - }); - - settings.add('Livechat_force_accept_data_processing_consent', false, { - type: 'boolean', - group: 'Omnichannel', - section: 'GDPR', - public: true, - alert: 'Force_visitor_to_accept_data_processing_consent_enabled_alert', - i18nLabel: 'Force_visitor_to_accept_data_processing_consent', - i18nDescription: 'Force_visitor_to_accept_data_processing_consent_description', - }); - - settings.add('Livechat_data_processing_consent_text', '', { - type: 'string', - multiline: true, - group: 'Omnichannel', - section: 'GDPR', - public: true, - i18nLabel: 'Data_processing_consent_text', - i18nDescription: 'Data_processing_consent_text_description', - enableQuery: { _id: 'Livechat_force_accept_data_processing_consent', value: true }, - }); - - settings.add('Livechat_agent_leave_action', 'none', { - type: 'select', - group: 'Omnichannel', - section: 'Sessions', - values: [ - { key: 'none', i18nLabel: 'None' }, - { key: 'forward', i18nLabel: 'Forward' }, - { key: 'close', i18nLabel: 'Close' }, - ], - i18nLabel: 'How_to_handle_open_sessions_when_agent_goes_offline', - }); - - settings.add('Livechat_agent_leave_action_timeout', 60, { - type: 'int', - group: 'Omnichannel', - section: 'Sessions', - enableQuery: { _id: 'Livechat_agent_leave_action', value: { $ne: 'none' } }, - i18nLabel: 'How_long_to_wait_after_agent_goes_offline', - i18nDescription: 'Time_in_seconds', - }); - - settings.add('Livechat_agent_leave_comment', '', { - type: 'string', - group: 'Omnichannel', - section: 'Sessions', - enableQuery: { _id: 'Livechat_agent_leave_action', value: 'close' }, - i18nLabel: 'Comment_to_leave_on_closing_session', - }); - - settings.add('Livechat_visitor_inactivity_timeout', 3600, { - type: 'int', - group: 'Omnichannel', - section: 'Sessions', - i18nLabel: 'How_long_to_wait_to_consider_visitor_abandonment', - i18nDescription: 'Time_in_seconds', - }); -}); diff --git a/app/livechat/server/config.ts b/app/livechat/server/config.ts new file mode 100644 index 0000000000000..be6ac87f51b98 --- /dev/null +++ b/app/livechat/server/config.ts @@ -0,0 +1,635 @@ +import { Meteor } from 'meteor/meteor'; + +import { SettingEditor } from '../../../definition/ISetting'; +import { settingsRegistry } from '../../settings/server'; + +const omnichannelEnabledQuery = { _id: 'Livechat_enabled', value: true }; + +Meteor.startup(function() { + settingsRegistry.addGroup('Omnichannel', function() { + this.add('Livechat_enabled', true, { + type: 'boolean', + group: 'Omnichannel', + public: true, + }); + + this.add('Livechat_title', 'Rocket.Chat', { + type: 'string', + group: 'Omnichannel', + section: 'Livechat', + public: true, + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_title_color', '#C1272D', { + type: 'color', + editor: SettingEditor.COLOR, + // allowedTypes: ['color', 'expression'], + group: 'Omnichannel', + section: 'Livechat', + public: true, + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_enable_message_character_limit', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'Livechat', + public: true, + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_message_character_limit', 0, { + type: 'int', + group: 'Omnichannel', + section: 'Livechat', + public: true, + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_display_offline_form', true, { + type: 'boolean', + group: 'Omnichannel', + public: true, + section: 'Livechat', + i18nLabel: 'Display_offline_form', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_clear_local_storage_when_chat_ended', false, { + type: 'boolean', + group: 'Omnichannel', + public: true, + section: 'Livechat', + i18nLabel: 'Clear_livechat_session_when_chat_ended', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_validate_offline_email', true, { + type: 'boolean', + group: 'Omnichannel', + public: true, + section: 'Livechat', + i18nLabel: 'Validate_email_address', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_offline_form_unavailable', '', { + type: 'string', + group: 'Omnichannel', + public: true, + section: 'Livechat', + i18nLabel: 'Offline_form_unavailable_message', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_offline_title', 'Leave a message', { + type: 'string', + group: 'Omnichannel', + public: true, + section: 'Livechat', + i18nLabel: 'Title', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_offline_title_color', '#666666', { + type: 'color', + editor: SettingEditor.COLOR, + // allowedTypes: ['color', 'expression'], + group: 'Omnichannel', + public: true, + section: 'Livechat', + i18nLabel: 'Color', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_offline_message', '', { + type: 'string', + group: 'Omnichannel', + public: true, + section: 'Livechat', + i18nLabel: 'Instructions', + i18nDescription: 'Instructions_to_your_visitor_fill_the_form_to_send_a_message', + enableQuery: omnichannelEnabledQuery, + multiline: true, + }); + + this.add('Livechat_offline_email', '', { + type: 'string', + group: 'Omnichannel', + i18nLabel: 'Email_address_to_send_offline_messages', + section: 'Livechat', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_offline_success_message', '', { + type: 'string', + group: 'Omnichannel', + public: true, + section: 'Livechat', + i18nLabel: 'Offline_success_message', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_allow_switching_departments', true, { + type: 'boolean', + group: 'Omnichannel', + public: true, + section: 'Livechat', + i18nLabel: 'Allow_switching_departments', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_show_agent_info', true, { + type: 'boolean', + group: 'Omnichannel', + section: 'Livechat', + public: true, + i18nLabel: 'Show_agent_info', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_show_agent_email', true, { + type: 'boolean', + group: 'Omnichannel', + section: 'Livechat', + public: true, + enableQuery: [{ _id: 'Livechat_show_agent_info', value: true }, omnichannelEnabledQuery], + i18nLabel: 'Show_agent_email', + }); + + this.add('Livechat_request_comment_when_closing_conversation', true, { + type: 'boolean', + group: 'Omnichannel', + public: true, + i18nLabel: 'Request_comment_when_closing_conversation', + i18nDescription: 'Request_comment_when_closing_conversation_description', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_conversation_finished_message', '', { + type: 'string', + group: 'Omnichannel', + section: 'Livechat', + public: true, + i18nLabel: 'Conversation_finished_message', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_conversation_finished_text', '', { + type: 'string', + multiline: true, + group: 'Omnichannel', + section: 'Livechat', + public: true, + i18nLabel: 'Conversation_finished_text', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_registration_form', true, { + type: 'boolean', + group: 'Omnichannel', + section: 'Livechat', + public: true, + i18nLabel: 'Show_preregistration_form', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_name_field_registration_form', true, { + type: 'boolean', + group: 'Omnichannel', + section: 'Livechat', + public: true, + i18nLabel: 'Show_name_field', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_email_field_registration_form', true, { + type: 'boolean', + group: 'Omnichannel', + section: 'Livechat', + public: true, + i18nLabel: 'Show_email_field', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_guest_count', 1, { type: 'int', group: 'Omnichannel', hidden: true }); + + this.add('Livechat_Room_Count', 1, { + type: 'int', + group: 'Omnichannel', + i18nLabel: 'Livechat_room_count', + hidden: true, + }); + + this.add('Livechat_enabled_when_agent_idle', true, { + type: 'boolean', + group: 'Omnichannel', + i18nLabel: 'Accept_new_livechats_when_agent_is_idle', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_webhookUrl', '', { + type: 'string', + group: 'Omnichannel', + section: 'CRM_Integration', + i18nLabel: 'Webhook_URL', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_secret_token', '', { + type: 'string', + group: 'Omnichannel', + section: 'CRM_Integration', + i18nLabel: 'Secret_token', + secret: true, + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_webhook_on_start', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'CRM_Integration', + i18nLabel: 'Send_request_on_chat_start', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_webhook_on_close', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'CRM_Integration', + i18nLabel: 'Send_request_on_chat_close', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_webhook_on_chat_taken', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'CRM_Integration', + i18nLabel: 'Send_request_on_chat_taken', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_webhook_on_chat_queued', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'CRM_Integration', + i18nLabel: 'Send_request_on_chat_queued', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_webhook_on_forward', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'CRM_Integration', + i18nLabel: 'Send_request_on_forwarding', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_webhook_on_offline_msg', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'CRM_Integration', + i18nLabel: 'Send_request_on_offline_messages', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_webhook_on_visitor_message', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'CRM_Integration', + i18nLabel: 'Send_request_on_visitor_message', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_webhook_on_agent_message', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'CRM_Integration', + i18nLabel: 'Send_request_on_agent_message', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Send_visitor_navigation_history_livechat_webhook_request', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'CRM_Integration', + i18nLabel: 'Send_visitor_navigation_history_on_request', + i18nDescription: 'Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled', + enableQuery: [{ _id: 'Livechat_Visitor_navigation_as_a_message', value: true }, omnichannelEnabledQuery], + }); + + this.add('Livechat_webhook_on_capture', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'CRM_Integration', + i18nLabel: 'Send_request_on_lead_capture', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_lead_email_regex', '\\b[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\\.)+[A-Z]{2,4}\\b', { + type: 'string', + group: 'Omnichannel', + section: 'CRM_Integration', + i18nLabel: 'Lead_capture_email_regex', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_lead_phone_regex', '((?:\\([0-9]{1,3}\\)|[0-9]{2})[ \\-]*?[0-9]{4,5}(?:[\\-\\s\\_]{1,2})?[0-9]{4}(?:(?=[^0-9])|$)|[0-9]{4,5}(?:[\\-\\s\\_]{1,2})?[0-9]{4}(?:(?=[^0-9])|$))', { + type: 'string', + group: 'Omnichannel', + section: 'CRM_Integration', + i18nLabel: 'Lead_capture_phone_regex', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_history_monitor_type', 'url', { + type: 'select', + group: 'Omnichannel', + section: 'Livechat', + i18nLabel: 'Monitor_history_for_changes_on', + values: [ + { key: 'url', i18nLabel: 'Page_URL' }, + { key: 'title', i18nLabel: 'Page_title' }, + ], + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_Visitor_navigation_as_a_message', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'Livechat', + public: true, + i18nLabel: 'Send_Visitor_navigation_history_as_a_message', + enableQuery: omnichannelEnabledQuery, + }); + + settingsRegistry.addGroup('Omnichannel', function() { + this.section('Business_Hours', function() { + this.add('Livechat_enable_business_hours', false, { + type: 'boolean', + group: 'Omnichannel', + public: true, + i18nLabel: 'Business_hours_enabled', + enableQuery: omnichannelEnabledQuery, + }); + }); + }); + + this.add('Livechat_continuous_sound_notification_new_livechat_room', false, { + type: 'boolean', + group: 'Omnichannel', + public: true, + i18nLabel: 'Continuous_sound_notifications_for_new_livechat_room', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_fileupload_enabled', true, { + type: 'boolean', + group: 'Omnichannel', + public: true, + i18nLabel: 'FileUpload_Enabled', + enableQuery: [{ _id: 'FileUpload_Enabled', value: true }, omnichannelEnabledQuery], + }); + + this.add('Livechat_enable_transcript', false, { + type: 'boolean', + group: 'Omnichannel', + public: true, + i18nLabel: 'Transcript_Enabled', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_transcript_message', '', { + type: 'string', + group: 'Omnichannel', + public: true, + i18nLabel: 'Transcript_message', + enableQuery: [{ _id: 'Livechat_enable_transcript', value: true }, omnichannelEnabledQuery], + }); + + this.add('Livechat_registration_form_message', '', { + type: 'string', + group: 'Omnichannel', + section: 'Livechat', + public: true, + i18nLabel: 'Livechat_registration_form_message', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_AllowedDomainsList', '', { + type: 'string', + group: 'Omnichannel', + section: 'Livechat', + public: true, + i18nLabel: 'Livechat_AllowedDomainsList', + i18nDescription: 'Domains_allowed_to_embed_the_livechat_widget', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_OfflineMessageToChannel_enabled', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'Livechat', + public: true, + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_OfflineMessageToChannel_channel_name', '', { + type: 'string', + group: 'Omnichannel', + section: 'Livechat', + public: true, + enableQuery: [{ _id: 'Livechat_OfflineMessageToChannel_enabled', value: true }, omnichannelEnabledQuery], + i18nLabel: 'Channel_name', + }); + + this.add('Livechat_Facebook_Enabled', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'Facebook', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_Facebook_API_Key', '', { + type: 'string', + group: 'Omnichannel', + section: 'Facebook', + i18nDescription: 'If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_Facebook_API_Secret', '', { + type: 'string', + group: 'Omnichannel', + section: 'Facebook', + i18nDescription: 'If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_RDStation_Token', '', { + type: 'string', + group: 'Omnichannel', + public: false, + section: 'RD Station', + i18nLabel: 'RDStation_Token', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_Routing_Method', 'Auto_Selection', { + type: 'select', + group: 'Omnichannel', + public: true, + section: 'Routing', + values: [ + { key: 'External', i18nLabel: 'External_Service' }, + { key: 'Auto_Selection', i18nLabel: 'Auto_Selection' }, + { key: 'Manual_Selection', i18nLabel: 'Manual_Selection' }, + ], + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_accept_chats_with_no_agents', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'Routing', + i18nLabel: 'Accept_with_no_online_agents', + i18nDescription: 'Accept_incoming_livechat_requests_even_if_there_are_no_online_agents', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_assign_new_conversation_to_bot', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'Routing', + i18nLabel: 'Assign_new_conversations_to_bot_agent', + i18nDescription: 'Assign_new_conversations_to_bot_agent_description', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_guest_pool_max_number_incoming_livechats_displayed', 0, { + type: 'int', + group: 'Omnichannel', + section: 'Routing', + public: true, + i18nLabel: 'Max_number_incoming_livechats_displayed', + i18nDescription: 'Max_number_incoming_livechats_displayed_description', + enableQuery: [{ _id: 'Livechat_Routing_Method', value: 'Manual_Selection' }, omnichannelEnabledQuery], + }); + + this.add('Livechat_show_queue_list_link', false, { + type: 'boolean', + group: 'Omnichannel', + public: true, + section: 'Routing', + i18nLabel: 'Show_queue_list_to_all_agents', + enableQuery: [{ _id: 'Livechat_Routing_Method', value: { $ne: 'External' } }, omnichannelEnabledQuery], + }); + + this.add('Livechat_External_Queue_URL', '', { + type: 'string', + group: 'Omnichannel', + public: false, + section: 'Routing', + i18nLabel: 'External_Queue_Service_URL', + i18nDescription: 'For_more_details_please_check_our_docs', + enableQuery: [{ _id: 'Livechat_Routing_Method', value: 'External' }, omnichannelEnabledQuery], + }); + + this.add('Livechat_External_Queue_Token', '', { + type: 'string', + group: 'Omnichannel', + public: false, + section: 'Routing', + i18nLabel: 'Secret_token', + enableQuery: [{ _id: 'Livechat_Routing_Method', value: 'External' }, omnichannelEnabledQuery], + }); + + this.add('Livechat_Allow_collect_and_store_HTTP_header_informations', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'GDPR', + public: true, + i18nLabel: 'Allow_collect_and_store_HTTP_header_informations', + i18nDescription: 'Allow_collect_and_store_HTTP_header_informations_description', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_force_accept_data_processing_consent', false, { + type: 'boolean', + group: 'Omnichannel', + section: 'GDPR', + public: true, + alert: 'Force_visitor_to_accept_data_processing_consent_enabled_alert', + i18nLabel: 'Force_visitor_to_accept_data_processing_consent', + i18nDescription: 'Force_visitor_to_accept_data_processing_consent_description', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_data_processing_consent_text', '', { + type: 'string', + multiline: true, + group: 'Omnichannel', + section: 'GDPR', + public: true, + i18nLabel: 'Data_processing_consent_text', + i18nDescription: 'Data_processing_consent_text_description', + enableQuery: [{ _id: 'Livechat_force_accept_data_processing_consent', value: true }, omnichannelEnabledQuery], + }); + + this.add('Livechat_agent_leave_action', 'none', { + type: 'select', + group: 'Omnichannel', + section: 'Sessions', + values: [ + { key: 'none', i18nLabel: 'None' }, + { key: 'forward', i18nLabel: 'Forward' }, + { key: 'close', i18nLabel: 'Close' }, + ], + i18nLabel: 'How_to_handle_open_sessions_when_agent_goes_offline', + enableQuery: omnichannelEnabledQuery, + }); + + this.add('Livechat_agent_leave_action_timeout', 60, { + type: 'int', + group: 'Omnichannel', + section: 'Sessions', + enableQuery: [{ _id: 'Livechat_agent_leave_action', value: { $ne: 'none' } }, omnichannelEnabledQuery], + i18nLabel: 'How_long_to_wait_after_agent_goes_offline', + i18nDescription: 'Time_in_seconds', + }); + + this.add('Livechat_agent_leave_comment', '', { + type: 'string', + group: 'Omnichannel', + section: 'Sessions', + enableQuery: [{ _id: 'Livechat_agent_leave_action', value: 'close' }, omnichannelEnabledQuery], + i18nLabel: 'Comment_to_leave_on_closing_session', + }); + + this.add('Livechat_visitor_inactivity_timeout', 3600, { + type: 'int', + group: 'Omnichannel', + section: 'Sessions', + i18nLabel: 'How_long_to_wait_to_consider_visitor_abandonment', + 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/externalFrame/settings.js b/app/livechat/server/externalFrame/settings.js deleted file mode 100644 index e707e8604de79..0000000000000 --- a/app/livechat/server/externalFrame/settings.js +++ /dev/null @@ -1,38 +0,0 @@ -import { settings } from '../../../settings/server/functions/settings'; - -settings.addGroup('Omnichannel', function() { - this.section('External Frame', function() { - this.add('Omnichannel_External_Frame_Enabled', false, { - type: 'boolean', - public: true, - alert: 'Experimental_Feature_Alert', - }); - - this.add('Omnichannel_External_Frame_URL', '', { - type: 'string', - public: true, - enableQuery: { - _id: 'Omnichannel_External_Frame_Enabled', - value: true, - }, - }); - - this.add('Omnichannel_External_Frame_Encryption_JWK', '', { - type: 'string', - public: true, - enableQuery: { - _id: 'Omnichannel_External_Frame_Enabled', - value: true, - }, - }); - - this.add('Omnichannel_External_Frame_GenerateKey', 'omnichannelExternalFrameGenerateKey', { - type: 'action', - actionText: 'Generate_new_key', - enableQuery: { - _id: 'Omnichannel_External_Frame_Enabled', - value: true, - }, - }); - }); -}); diff --git a/app/livechat/server/externalFrame/settings.ts b/app/livechat/server/externalFrame/settings.ts new file mode 100644 index 0000000000000..367fa3f304b77 --- /dev/null +++ b/app/livechat/server/externalFrame/settings.ts @@ -0,0 +1,38 @@ +import { settingsRegistry } from '../../../settings/server'; + +settingsRegistry.addGroup('Omnichannel', function() { + this.section('External Frame', function() { + this.add('Omnichannel_External_Frame_Enabled', false, { + type: 'boolean', + public: true, + alert: 'Experimental_Feature_Alert', + }); + + this.add('Omnichannel_External_Frame_URL', '', { + type: 'string', + public: true, + enableQuery: { + _id: 'Omnichannel_External_Frame_Enabled', + value: true, + }, + }); + + this.add('Omnichannel_External_Frame_Encryption_JWK', '', { + type: 'string', + public: true, + enableQuery: { + _id: 'Omnichannel_External_Frame_Enabled', + value: true, + }, + }); + + this.add('Omnichannel_External_Frame_GenerateKey', 'omnichannelExternalFrameGenerateKey', { + type: 'action', + actionText: 'Generate_new_key', + enableQuery: { + _id: 'Omnichannel_External_Frame_Enabled', + value: true, + }, + }); + }); +}); diff --git a/app/livechat/server/hooks/RDStation.js b/app/livechat/server/hooks/RDStation.js index bc60b1c1c9efe..b15453995eda4 100644 --- a/app/livechat/server/hooks/RDStation.js +++ b/app/livechat/server/hooks/RDStation.js @@ -3,6 +3,7 @@ import { HTTP } from 'meteor/http'; import { settings } from '../../../settings'; import { callbacks } from '../../../callbacks'; import { Livechat } from '../lib/Livechat'; +import { SystemLogger } from '../../../../server/lib/logger/system'; function sendToRDStation(room) { if (!settings.get('Livechat_RDStation_Token')) { @@ -50,7 +51,7 @@ function sendToRDStation(room) { try { HTTP.call('POST', 'https://www.rdstation.com.br/api/1.3/conversions', options); } catch (e) { - console.error('Error sending lead to RD Station ->', e); + SystemLogger.error('Error sending lead to RD Station ->', e); } return room; diff --git a/app/livechat/server/hooks/saveAnalyticsData.js b/app/livechat/server/hooks/saveAnalyticsData.js index 4ca8832c153dc..96d336c228ca7 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 2b80cd7408e75..94e4cfcd22ebe 100644 --- a/app/livechat/server/hooks/sendToCRM.js +++ b/app/livechat/server/hooks/sendToCRM.js @@ -1,4 +1,4 @@ -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { callbacks } from '../../../callbacks'; import { Messages, LivechatRooms } from '../../../models'; import { Livechat } from '../lib/Livechat'; @@ -8,27 +8,12 @@ import { normalizeMessageFileUpload } from '../../../utils/server/functions/norm const msgNavType = 'livechat_navigation_history'; const msgClosingType = 'livechat-close'; -let sendNavHistoryMessageEnabled = false; -let sendNavHistoryWebhookEnabled = false; -let crmWebhookUrl = ''; -settings.get('Livechat_Visitor_navigation_as_a_message', (key, value) => { - sendNavHistoryMessageEnabled = value; -}); -settings.get('Send_visitor_navigation_history_livechat_webhook_request', (key, value) => { - sendNavHistoryWebhookEnabled = value; -}); -settings.get('Livechat_webhookUrl', (key, value) => { - crmWebhookUrl = value; -}); - -const crmEnabled = () => crmWebhookUrl !== '' && crmWebhookUrl !== undefined; - const sendMessageType = (msgType) => { switch (msgType) { case msgClosingType: return true; case msgNavType: - return sendNavHistoryMessageEnabled && sendNavHistoryWebhookEnabled; + return settings.get('Livechat_Visitor_navigation_as_a_message') && settings.get('Send_visitor_navigation_history_livechat_webhook_request'); default: return false; } @@ -51,7 +36,7 @@ const getAdditionalFieldsByType = (type, room) => { } }; function sendToCRM(type, room, includeMessages = true) { - if (crmEnabled() === false) { + if (!settings.get('Livechat_webhookUrl')) { return room; } @@ -99,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 1af4767e36568..7c1b00f312115 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/index.js b/app/livechat/server/index.js index 9d2c6fdf3b355..ec823937e7cd0 100644 --- a/app/livechat/server/index.js +++ b/app/livechat/server/index.js @@ -1,9 +1,9 @@ import './livechat'; +import './config'; import './startup'; import './visitorStatus'; import './agentStatus'; import '../lib/messageTypes'; -import './config'; import './roomType'; import './hooks/beforeCloseRoom'; import './hooks/beforeDelegateAgent'; @@ -69,7 +69,6 @@ import './methods/setUpConnection'; import './methods/takeInquiry'; import './methods/requestTranscript'; import './methods/returnAsInquiry'; -import './methods/saveOfficeHours'; import './methods/sendTranscript'; import './methods/getFirstRoomMessage'; import './methods/getTagsList'; diff --git a/app/livechat/server/lib/Analytics.js b/app/livechat/server/lib/Analytics.js index c4251fbd1bce1..b4aa71af346f6 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/Helper.js b/app/livechat/server/lib/Helper.js index a582941be58e9..96f3bcd7251ff 100644 --- a/app/livechat/server/lib/Helper.js +++ b/app/livechat/server/lib/Helper.js @@ -3,18 +3,19 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { Match, check } from 'meteor/check'; import { LivechatTransferEventType } from '@rocket.chat/apps-engine/definition/livechat'; -import { hasRole } from '../../../authorization'; +import { hasRole } from '../../../authorization/server'; import { Messages, LivechatRooms, Rooms, Subscriptions, Users, LivechatInquiry, LivechatDepartment, LivechatDepartmentAgents } from '../../../models/server'; import { Livechat } from './Livechat'; import { RoutingManager } from './RoutingManager'; import { callbacks } from '../../../callbacks/server'; -import { Logger } from '../../../logger'; -import { settings } from '../../../settings'; +import { Logger } from '../../../logger/server'; +import { settings } from '../../../settings/server'; import { Apps, AppEvents } from '../../../apps/server'; import notifications from '../../../notifications/server/lib/Notifications'; import { sendNotification } from '../../../lib/server'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; import { queueInquiry, saveQueueInquiry } from './QueueManager'; +import { OmnichannelSourceType } from '../../../../definition/IRoom'; const logger = new Logger('LivechatHelper'); const emailValidationRegex = /^[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])?)*$/; @@ -39,6 +40,7 @@ export const createLivechatRoom = (rid, name, guest, roomInfo = {}, extraData = const extraRoomInfo = callbacks.run('livechat.beforeRoom', roomInfo, extraData); const { _id, username, token, department: departmentId, status = 'online' } = guest; + const newRoomAt = new Date(); logger.debug(`Creating livechat room for visitor ${ _id }`); @@ -46,10 +48,10 @@ export const createLivechatRoom = (rid, name, guest, roomInfo = {}, extraData = _id: rid, msgs: 0, usersCount: 1, - lm: new Date(), + lm: newRoomAt, fname: name, t: 'l', - ts: new Date(), + ts: newRoomAt, departmentId, v: { _id, @@ -60,6 +62,13 @@ export const createLivechatRoom = (rid, name, guest, roomInfo = {}, extraData = cl: false, open: true, waitingResponse: true, + // this should be overriden by extraRoomInfo when provided + // in case it's not provided, we'll use this "default" type + source: { + type: OmnichannelSourceType.OTHER, + alias: 'unknown', + }, + queuedAt: newRoomAt, }, extraRoomInfo); const roomId = Rooms.insert(room); @@ -196,7 +205,7 @@ export const parseAgentCustomFields = (customFields) => { return Object.keys(parseCustomFields) .filter((customFieldKey) => parseCustomFields[customFieldKey].sendToIntegrations === true); } catch (error) { - console.error(error); + Livechat.logger.error(error); return []; } }; @@ -430,7 +439,9 @@ export const forwardRoomToDepartment = async (room, guest, transferData) => { Livechat.saveTransferHistory(room, transferData); if (oldServedBy) { - RoutingManager.removeAllRoomSubscriptions(room, servedBy); + // if chat is queued then we don't ignore the new servedBy agent bcs at this + // point the chat is not assigned to him/her and it is still in the queue + RoutingManager.removeAllRoomSubscriptions(room, !chatQueued && servedBy); } if (!chatQueued && servedBy) { Messages.createUserJoinWithRoomIdAndUser(rid, servedBy); @@ -441,6 +452,8 @@ export const forwardRoomToDepartment = async (room, guest, transferData) => { if (chatQueued) { logger.debug(`Forwarding succesful. Marking inquiry ${ inquiry._id } as ready`); LivechatInquiry.readyInquiry(inquiry._id); + LivechatRooms.removeAgentByRoomId(rid); + dispatchAgentDelegated(rid, null); const newInquiry = LivechatInquiry.findOneById(inquiry._id); await queueInquiry(room, newInquiry); diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index b34b72ed0a0f8..aa1dedb3ee39a 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -29,8 +29,8 @@ import { LivechatVisitors, LivechatInquiry, } from '../../../models/server'; -import { Logger } from '../../../logger'; -import { addUserRoles, hasPermission, hasRole, removeUserFromRoles, canAccessRoom } from '../../../authorization'; +import { Logger } from '../../../logger/server'; +import { addUserRoles, hasPermission, hasRole, removeUserFromRoles, canAccessRoom } from '../../../authorization/server'; import * as Mailer from '../../../mailer'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; import { updateMessage } from '../../../lib/server/functions/updateMessage'; @@ -40,17 +40,18 @@ import { normalizeTransferredByData, parseAgentCustomFields, updateDepartmentAge import { Apps, AppEvents } from '../../../apps/server'; import { businessHourManager } from '../business-hour'; import notifications from '../../../notifications/server/lib/Notifications'; +import { Users as UsersRaw } from '../../../models/server/raw'; + +const logger = new Logger('Livechat'); + +const dnsResolveMx = Meteor.wrapAsync(dns.resolveMx); export const Livechat = { Analytics, historyMonitorType: 'url', - logger: new Logger('Livechat', { - sections: { - webhook: 'Webhook', - }, - }), - + logger, + webhookLogger: logger.section('Webhook'), findGuest(token) { return LivechatVisitors.getVisitorByToken(token, { @@ -81,7 +82,9 @@ export const Livechat = { } } - return Livechat.checkOnlineAgents(department); + const agentsOnline = Livechat.checkOnlineAgents(department); + Livechat.logger.debug(`Are online agents ${ department ? `for department ${ department }` : '' }?: ${ agentsOnline }`); + return agentsOnline; }, getNextAgent(department) { @@ -219,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 })); @@ -236,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; }, @@ -511,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', @@ -526,6 +529,7 @@ export const Livechat = { 'Livechat_force_accept_data_processing_consent', 'Livechat_data_processing_consent_text', 'Livechat_show_agent_info', + 'Livechat_clear_local_storage_when_chat_ended', ]).forEach((setting) => { rcSettings[setting._id] = setting.value; }); @@ -595,7 +599,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 })); }); }, @@ -711,7 +715,7 @@ export const Livechat = { this.saveTransferHistory(room, transferData); RoutingManager.unassignAgent(inquiry, departmentId); } catch (e) { - console.error(e); + this.logger.error(e); throw new Meteor.Error('error-returning-inquiry', 'Error returning inquiry to the queue', { method: 'livechat:returnRoomAsInquiry' }); } @@ -730,11 +734,11 @@ export const Livechat = { try { return HTTP.post(settings.get('Livechat_webhookUrl'), options); } catch (e) { - Livechat.logger.webhook.error(`Response error on ${ 11 - attempts } try ->`, e); + Livechat.webhookLogger.error(`Response error on ${ 11 - attempts } try ->`, e); // try 10 times after 10 seconds each - Livechat.logger.webhook.warn('Will try again in 10 seconds ...'); + (attempts - 1) && Livechat.webhookLogger.warn('Will try again in 10 seconds ...'); setTimeout(Meteor.bindEnvironment(function() { - Livechat.sendRequest(postData, callback, attempts--); + Livechat.sendRequest(postData, callback, attempts - 1); }), 10000); } }, @@ -810,7 +814,7 @@ export const Livechat = { if (addUserRoles(user._id, 'livechat-agent')) { Users.setOperator(user._id, true); - this.setUserStatusLivechat(user._id, 'available'); + this.setUserStatusLivechat(user._id, user.status !== 'offline' ? 'available' : 'not-available'); return user; } @@ -884,6 +888,12 @@ export const Livechat = { return user; }, + setUserStatusLivechatIf(userId, status, condition, fields) { + const user = Promise.await(UsersRaw.setLivechatStatusIf(userId, status, condition, fields)); + callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); + return user; + }, + cleanGuestHistory(_id) { const guest = LivechatVisitors.findOneById(_id); if (!guest) { @@ -1096,6 +1106,20 @@ export const Livechat = { return true; }, + getRoomMessages({ rid }) { + check(rid, String); + + const isLivechat = Promise.await(Rooms.findByTypeInIds('l', [rid])).count(); + + if (!isLivechat) { + throw new Meteor.Error('invalid-room'); + } + + const ignoredMessageTypes = ['livechat_navigation_history', 'livechat_transcript_history', 'command', 'livechat-close', 'livechat-started', 'livechat_video_call']; + + return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, { sort: { ts: 1 } }).fetch(); + }, + requestTranscript({ rid, email, subject, user }) { check(rid, String); check(email, String); @@ -1169,7 +1193,7 @@ export const Livechat = { const emailDomain = email.substr(email.lastIndexOf('@') + 1); try { - Meteor.wrapAsync(dns.resolveMx)(emailDomain); + dnsResolveMx(emailDomain); } catch (e) { throw new Meteor.Error('error-invalid-email-address', 'Invalid email address', { method: 'livechat:sendOfflineMessage' }); } @@ -1255,8 +1279,14 @@ 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.get('Livechat_history_monitor_type', (key, value) => { +settings.watch('Livechat_history_monitor_type', (value) => { Livechat.historyMonitorType = value; }); diff --git a/app/livechat/server/lib/QueueManager.js b/app/livechat/server/lib/QueueManager.js index f8d1540e84bd1..711aa84351f9e 100644 --- a/app/livechat/server/lib/QueueManager.js +++ b/app/livechat/server/lib/QueueManager.js @@ -7,7 +7,7 @@ import { callbacks } from '../../../callbacks/server'; import { Logger } from '../../../logger'; import { RoutingManager } from './RoutingManager'; -const logger = new Logger('QueueMananger'); +const logger = new Logger('QueueManager'); export const saveQueueInquiry = (inquiry) => { LivechatInquiry.queueInquiry(inquiry._id); @@ -16,13 +16,13 @@ export const saveQueueInquiry = (inquiry) => { export const queueInquiry = async (room, inquiry, defaultAgent) => { const inquiryAgent = RoutingManager.delegateAgent(defaultAgent, inquiry); - logger.debug(`Delegating inquiry with id ${ inquiry._id } to agent ${ defaultAgent?._id }`); + logger.debug(`Delegating inquiry with id ${ inquiry._id } to agent ${ defaultAgent?.username }`); await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent); inquiry = LivechatInquiry.findOneById(inquiry._id); if (inquiry.status === 'ready') { - logger.debug(`Inquiry with id ${ inquiry._id } is ready. Delegating to agent ${ inquiryAgent?._id }`); + logger.debug(`Inquiry with id ${ inquiry._id } is ready. Delegating to agent ${ inquiryAgent?.username }`); return RoutingManager.delegateInquiry(inquiry, inquiryAgent); } }; @@ -51,7 +51,7 @@ export const QueueManager = { const room = LivechatRooms.findOneById(createLivechatRoom(rid, name, guest, roomInfo, extraData)); logger.debug(`Room for visitor ${ guest._id } created with id ${ room._id }`); - const inquiry = LivechatInquiry.findOneById(createLivechatInquiry({ rid, name, guest, message, extraData })); + const inquiry = LivechatInquiry.findOneById(createLivechatInquiry({ rid, name, guest, message, extraData: { ...extraData, source: roomInfo.source } })); logger.debug(`Generated inquiry for visitor ${ guest._id } with id ${ inquiry._id } [Not queued]`); LivechatRooms.updateRoomCount(); @@ -63,7 +63,7 @@ export const QueueManager = { }, async unarchiveRoom(archivedRoom = {}) { - const { _id: rid, open, closedAt, fname: name, servedBy, v, departmentId: department, lastMessage: message } = archivedRoom; + const { _id: rid, open, closedAt, fname: name, servedBy, v, departmentId: department, lastMessage: message, source = {} } = archivedRoom; if (!rid || !closedAt || !!open) { return archivedRoom; @@ -89,7 +89,7 @@ export const QueueManager = { LivechatRooms.unarchiveOneById(rid); const room = LivechatRooms.findOneById(rid); - const inquiry = LivechatInquiry.findOneById(createLivechatInquiry({ rid, name, guest, message })); + const inquiry = LivechatInquiry.findOneById(createLivechatInquiry({ rid, name, guest, message, extraData: { source } })); logger.debug(`Generated inquiry for visitor ${ v._id } with id ${ inquiry._id } [Not queued]`); await queueInquiry(room, inquiry, defaultAgent); diff --git a/app/livechat/server/lib/RoutingManager.js b/app/livechat/server/lib/RoutingManager.js index 52f9a0c858e06..bc8e5ed07453e 100644 --- a/app/livechat/server/lib/RoutingManager.js +++ b/app/livechat/server/lib/RoutingManager.js @@ -1,7 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { settings } from '../../../settings/server'; import { createLivechatSubscription, dispatchAgentDelegated, @@ -13,7 +12,7 @@ import { allowAgentSkipQueue, } from './Helper'; import { callbacks } from '../../../callbacks/server'; -import { Logger } from '../../../logger'; +import { Logger } from '../../../../server/lib/logger/Logger'; import { LivechatRooms, Rooms, Messages, Users, LivechatInquiry, Subscriptions } from '../../../models/server'; import { Apps, AppEvents } from '../../../apps/server'; @@ -23,9 +22,24 @@ export const RoutingManager = { methodName: null, methods: {}, - setMethodName(name) { + startQueue() { // todo: move to eventemitter or middleware + // queue shouldn't start on CE + }, + + isMethodSet() { + return !!this.methodName; + }, + + setMethodNameAndStartQueue(name) { logger.debug(`Changing default routing method from ${ this.methodName } to ${ name }`); - this.methodName = name; + if (!this.methods[name]) { + logger.warn(`Cannot change routing method to ${ name }. Selected Routing method does not exists. Defaulting to Manual_Selection`); + this.methodName = 'Manual_Selection'; + } else { + this.methodName = name; + } + + this.startQueue(); }, registerMethod(name, Method) { @@ -45,7 +59,7 @@ export const RoutingManager = { }, async getNextAgent(department, ignoreAgentId) { - logger.debug(`Getting next available agent with method ${ this.name }`); + logger.debug(`Getting next available agent with method ${ this.methodName }`); return this.getMethod().getNextAgent(department, ignoreAgentId); }, @@ -55,6 +69,7 @@ export const RoutingManager = { if (!agent || (agent.username && !Users.findOneOnlineAgentByUserList(agent.username) && !allowAgentSkipQueue(agent))) { logger.debug(`Agent offline or invalid. Using routing method to get next agent for inquiry ${ inquiry._id }`); agent = await this.getNextAgent(department); + logger.debug(`Routing method returned agent ${ agent && agent.agentId } for inquiry ${ inquiry._id }`); } if (!agent) { @@ -62,7 +77,7 @@ export const RoutingManager = { return LivechatRooms.findOneById(rid); } - logger.debug(`Inquiry ${ inquiry._id } will be taken by agent ${ agent._id }`); + logger.debug(`Inquiry ${ inquiry._id } will be taken by agent ${ agent.agentId }`); return this.takeInquiry(inquiry, agent, options); }, @@ -195,7 +210,7 @@ export const RoutingManager = { const defaultAgent = callbacks.run('livechat.beforeDelegateAgent', agent, { department: inquiry?.department }); if (defaultAgent) { - logger.debug(`Delegating Inquiry ${ inquiry._id } to agent ${ defaultAgent._id }`); + logger.debug(`Delegating Inquiry ${ inquiry._id } to agent ${ defaultAgent.username }`); LivechatInquiry.setDefaultAgentById(inquiry._id, defaultAgent); } @@ -216,7 +231,3 @@ export const RoutingManager = { }); }, }; - -settings.get('Livechat_Routing_Method', function(key, value) { - RoutingManager.setMethodName(value); -}); diff --git a/app/livechat/server/lib/analytics/dashboards.js b/app/livechat/server/lib/analytics/dashboards.js index 18bdca6300332..70dc1c7925ff8 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/routing/External.js b/app/livechat/server/lib/routing/External.js index 4b104a9d2df07..5a72c1ef273f3 100644 --- a/app/livechat/server/lib/routing/External.js +++ b/app/livechat/server/lib/routing/External.js @@ -4,6 +4,7 @@ import { HTTP } from 'meteor/http'; import { settings } from '../../../../settings/server'; import { RoutingManager } from '../RoutingManager'; import { Users } from '../../../../models/server'; +import { SystemLogger } from '../../../../../server/lib/logger/system'; class ExternalQueue { constructor() { @@ -45,7 +46,7 @@ class ExternalQueue { } } } catch (e) { - console.error('Error requesting agent from external queue.', e); + SystemLogger.error('Error requesting agent from external queue.', e); break; } } diff --git a/app/livechat/server/lib/stream/agentStatus.ts b/app/livechat/server/lib/stream/agentStatus.ts index c04cd48d5e803..12985a42f58d9 100644 --- a/app/livechat/server/lib/stream/agentStatus.ts +++ b/app/livechat/server/lib/stream/agentStatus.ts @@ -2,25 +2,28 @@ 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; let action = 'none'; let comment = ''; -settings.get('Livechat_agent_leave_action_timeout', (_key, value) => { +settings.watch('Livechat_agent_leave_action_timeout', (value) => { if (typeof value !== 'number') { return; } actionTimeout = value * 1000; }); -settings.get('Livechat_agent_leave_action', (_key, value) => { +settings.watch('Livechat_agent_leave_action', (value) => { monitorAgents = value !== 'none'; action = value as string; }); -settings.get('Livechat_agent_leave_comment', (_key, value) => { +settings.watch('Livechat_agent_leave_comment', (value) => { if (typeof value !== 'string') { return; } @@ -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/facebook.js b/app/livechat/server/methods/facebook.js index 7d17161fe9384..90d27222aa2e3 100644 --- a/app/livechat/server/methods/facebook.js +++ b/app/livechat/server/methods/facebook.js @@ -1,8 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings'; import OmniChannel from '../lib/OmniChannel'; +import { Settings } from '../../../models/server'; Meteor.methods({ 'livechat:facebook'(options) { @@ -26,13 +28,13 @@ Meteor.methods({ return result; } - return settings.updateById('Livechat_Facebook_Enabled', true); + return Settings.updateValueById('Livechat_Facebook_Enabled', true); } case 'disable': { OmniChannel.disable(); - return settings.updateById('Livechat_Facebook_Enabled', false); + return Settings.updateValueById('Livechat_Facebook_Enabled', false); } case 'list-pages': { @@ -59,7 +61,7 @@ Meteor.methods({ throw new Meteor.Error('integration-error', e.response.data.error.message); } } - console.error('Error contacting omni.rocket.chat:', e); + SystemLogger.error('Error contacting omni.rocket.chat:', e); throw new Meteor.Error('integration-error', e.error); } }, diff --git a/app/livechat/server/methods/getAgentOverviewData.js b/app/livechat/server/methods/getAgentOverviewData.js index f6cc509f1441f..d60d369575301 100644 --- a/app/livechat/server/methods/getAgentOverviewData.js +++ b/app/livechat/server/methods/getAgentOverviewData.js @@ -14,7 +14,7 @@ Meteor.methods({ } if (!(options.chartOptions && options.chartOptions.name)) { - console.log('Incorrect analytics options'); + Livechat.logger.warn('Incorrect analytics options'); return; } diff --git a/app/livechat/server/methods/getAnalyticsChartData.js b/app/livechat/server/methods/getAnalyticsChartData.js index 42b92ac31a4bc..caa86c6508992 100644 --- a/app/livechat/server/methods/getAnalyticsChartData.js +++ b/app/livechat/server/methods/getAnalyticsChartData.js @@ -14,7 +14,7 @@ Meteor.methods({ } if (!(options.chartOptions && options.chartOptions.name)) { - console.log('Incorrect chart options'); + Livechat.logger.warn('Incorrect chart options'); return; } diff --git a/app/livechat/server/methods/getAnalyticsOverviewData.js b/app/livechat/server/methods/getAnalyticsOverviewData.js index 741e26ccee536..30fdec7835d89 100644 --- a/app/livechat/server/methods/getAnalyticsOverviewData.js +++ b/app/livechat/server/methods/getAnalyticsOverviewData.js @@ -15,7 +15,7 @@ Meteor.methods({ } if (!(options.analyticsOptions && options.analyticsOptions.name)) { - console.error('Incorrect analytics options'); + Livechat.logger.error('Incorrect analytics options'); return; } diff --git a/app/livechat/server/methods/getInitialData.js b/app/livechat/server/methods/getInitialData.js index 9b7a2f22a7055..1243a3360ec24 100644 --- a/app/livechat/server/methods/getInitialData.js +++ b/app/livechat/server/methods/getInitialData.js @@ -3,6 +3,7 @@ import _ from 'underscore'; import { LivechatRooms, Users, LivechatDepartment, LivechatTrigger, LivechatVisitors } from '../../../models'; import { Livechat } from '../lib/Livechat'; +import { deprecationWarning } from '../../../api/server/helpers/deprecationWarning'; Meteor.methods({ 'livechat:getInitialData'(visitorToken, departmentId) { @@ -75,7 +76,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; @@ -98,6 +99,7 @@ Meteor.methods({ info.allowSwitchingDepartments = initSettings.Livechat_allow_switching_departments; info.online = Users.findOnlineAgents().count() > 0; - return info; + + return deprecationWarning({ endpoint: 'livechat:getInitialData', versionWillBeRemoved: '5.0', response: info }); }, }); diff --git a/app/livechat/server/methods/saveAgentInfo.js b/app/livechat/server/methods/saveAgentInfo.js index 9b12a428219f7..28b7a5d7dbe0c 100644 --- a/app/livechat/server/methods/saveAgentInfo.js +++ b/app/livechat/server/methods/saveAgentInfo.js @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission, hasRole } from '../../../authorization'; +import { hasPermission, hasRole } from '../../../authorization/server'; import { Livechat } from '../lib/Livechat'; -import { Users } from '../../../models'; +import { Users } from '../../../models/server'; Meteor.methods({ 'livechat:saveAgentInfo'(_id, agentData, agentDepartments) { diff --git a/app/livechat/server/methods/saveAppearance.js b/app/livechat/server/methods/saveAppearance.js index 44a3749d81b43..473695317a070 100644 --- a/app/livechat/server/methods/saveAppearance.js +++ b/app/livechat/server/methods/saveAppearance.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; -import { settings as rcSettings } from '../../../settings'; +import { Settings } from '../../../models/server'; Meteor.methods({ 'livechat:saveAppearance'(settings) { @@ -38,7 +38,7 @@ Meteor.methods({ } settings.forEach((setting) => { - rcSettings.updateById(setting._id, setting.value); + Settings.updateValueById(setting._id, setting.value); }); }, }); diff --git a/app/livechat/server/methods/saveIntegration.js b/app/livechat/server/methods/saveIntegration.js index b28ab334256f3..d9585643783eb 100644 --- a/app/livechat/server/methods/saveIntegration.js +++ b/app/livechat/server/methods/saveIntegration.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import s from 'underscore.string'; import { hasPermission } from '../../../authorization'; -import { settings } from '../../../settings'; +import { Settings } from '../../../models/server'; Meteor.methods({ 'livechat:saveIntegration'(values) { @@ -11,43 +11,43 @@ Meteor.methods({ } if (typeof values.Livechat_webhookUrl !== 'undefined') { - settings.updateById('Livechat_webhookUrl', s.trim(values.Livechat_webhookUrl)); + Settings.updateValueById('Livechat_webhookUrl', s.trim(values.Livechat_webhookUrl)); } if (typeof values.Livechat_secret_token !== 'undefined') { - settings.updateById('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.updateById('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.updateById('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.updateById('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.updateById('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.updateById('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.updateById('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.updateById('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.updateById('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/methods/saveOfficeHours.js b/app/livechat/server/methods/saveOfficeHours.js deleted file mode 100644 index d0e16a59843bc..0000000000000 --- a/app/livechat/server/methods/saveOfficeHours.js +++ /dev/null @@ -1,21 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; -import { LivechatBusinessHours } from '../../../models/server/raw'; - -Meteor.methods({ - 'livechat:saveOfficeHours'(day, start, finish, open) { - console.warn('Method "livechat:saveOfficeHour" is deprecated and will be removed after v4.0.0'); - - if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-business-hours')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveOfficeHours' }); - } - - LivechatBusinessHours.updateDayOfGlobalBusinessHour({ - day, - start, - finish, - open, - }); - }, -}); diff --git a/app/livechat/server/methods/sendMessageLivechat.js b/app/livechat/server/methods/sendMessageLivechat.js index b294d00f22c04..b0e682411ef16 100644 --- a/app/livechat/server/methods/sendMessageLivechat.js +++ b/app/livechat/server/methods/sendMessageLivechat.js @@ -3,6 +3,8 @@ import { Match, check } from 'meteor/check'; import { LivechatVisitors } from '../../../models'; import { Livechat } from '../lib/Livechat'; +import { OmnichannelSourceType } from '../../../../definition/IRoom'; +import { settings } from '../../../settings/server'; Meteor.methods({ sendMessageLivechat({ token, _id, rid, msg, file, attachments }, agent) { @@ -29,6 +31,10 @@ Meteor.methods({ throw new Meteor.Error('invalid-token'); } + if (settings.get('Livechat_enable_message_character_limit') && msg.length > parseInt(settings.get('Livechat_message_character_limit'))) { + throw new Meteor.Error('message-length-exceeds-character-limit'); + } + return Livechat.sendMessage({ guest, message: { @@ -40,6 +46,11 @@ Meteor.methods({ attachments, }, agent, + roomInfo: { + source: { + type: OmnichannelSourceType.API, + }, + }, }); }, }); diff --git a/app/livechat/server/methods/startFileUploadRoom.js b/app/livechat/server/methods/startFileUploadRoom.js index 2779af23d2bb4..ccea332e18a37 100644 --- a/app/livechat/server/methods/startFileUploadRoom.js +++ b/app/livechat/server/methods/startFileUploadRoom.js @@ -3,9 +3,12 @@ import { Random } from 'meteor/random'; import { LivechatVisitors } from '../../../models'; import { Livechat } from '../lib/Livechat'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { OmnichannelSourceType } from '../../../../definition/IRoom'; Meteor.methods({ 'livechat:startFileUploadRoom'(roomId, token) { + methodDeprecationLogger.warn('livechat:startFileUploadRoom will be deprecated in future versions of Rocket.Chat'); const guest = LivechatVisitors.getVisitorByToken(token); const message = { @@ -16,6 +19,11 @@ Meteor.methods({ token: guest.token, }; - return Livechat.getRoom(guest, message); + const roomInfo = { + source: OmnichannelSourceType.API, + alias: 'file-upload', + }; + + return Livechat.getRoom(guest, message, roomInfo); }, }); diff --git a/app/livechat/server/methods/startVideoCall.js b/app/livechat/server/methods/startVideoCall.js index 58e51ec30087e..f64c8d260830e 100644 --- a/app/livechat/server/methods/startVideoCall.js +++ b/app/livechat/server/methods/startVideoCall.js @@ -4,9 +4,12 @@ import { Random } from 'meteor/random'; import { Messages } from '../../../models'; import { settings } from '../../../settings'; import { Livechat } from '../lib/Livechat'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { OmnichannelSourceType } from '../../../../definition/IRoom'; Meteor.methods({ async 'livechat:startVideoCall'(roomId) { + methodDeprecationLogger.warn('livechat:startVideoCall will be deprecated in future versions of Rocket.Chat'); if (!Meteor.userId()) { throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'livechat:closeByVisitor' }); } @@ -20,7 +23,15 @@ Meteor.methods({ ts: new Date(), }; - const room = await Livechat.getRoom(guest, message, { jitsiTimeout: new Date(Date.now() + 3600 * 1000) }); + const roomInfo = { + jitsiTimeout: new Date(Date.now() + 3600 * 1000), + source: { + type: OmnichannelSourceType.API, + alias: 'video-call', + }, + }; + + const room = await Livechat.getRoom(guest, message, roomInfo); message.rid = room._id; Messages.createWithTypeRoomIdMessageAndUser('livechat_video_call', room._id, '', guest, { diff --git a/app/livechat/server/methods/webhookTest.js b/app/livechat/server/methods/webhookTest.js index 45a642a661765..bc5b32fd75a07 100644 --- a/app/livechat/server/methods/webhookTest.js +++ b/app/livechat/server/methods/webhookTest.js @@ -1,17 +1,16 @@ import { Meteor } from 'meteor/meteor'; import { HTTP } from 'meteor/http'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; -const postCatchError = Meteor.wrapAsync(function(url, options, resolve) { - HTTP.post(url, options, function(err, res) { - if (err) { - resolve(null, err.response); - } else { - resolve(null, res); - } - }); -}); +const postCatchError = function(url, options) { + try { + return HTTP.post(url, options); + } catch (e) { + return e; + } +}; Meteor.methods({ 'livechat:webhookTest'() { @@ -73,7 +72,7 @@ Meteor.methods({ const response = postCatchError(settings.get('Livechat_webhookUrl'), options); - console.log('response ->', response); + SystemLogger.debug({ response }); if (response && response.statusCode && response.statusCode === 200) { return true; diff --git a/app/livechat/server/roomAccessValidator.compatibility.js b/app/livechat/server/roomAccessValidator.compatibility.js index 9eb7929fdf8a5..484db3a210809 100644 --- a/app/livechat/server/roomAccessValidator.compatibility.js +++ b/app/livechat/server/roomAccessValidator.compatibility.js @@ -1,6 +1,5 @@ -import { LivechatRooms } from '../../models'; -import { hasPermission, hasRole } from '../../authorization'; -import { LivechatDepartment, LivechatDepartmentAgents, LivechatInquiry } from '../../models/server'; +import { hasPermission, hasRole } from '../../authorization/server'; +import { LivechatDepartment, LivechatDepartmentAgents, LivechatInquiry, LivechatRooms } from '../../models/server'; import { RoutingManager } from './lib/RoutingManager'; export const validators = [ diff --git a/app/livechat/server/sendMessageBySMS.js b/app/livechat/server/sendMessageBySMS.js index 6094d1dc62a33..5e3bd1f133424 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/startup.js b/app/livechat/server/startup.js index 719a545de2b10..729eb4f92979f 100644 --- a/app/livechat/server/startup.js +++ b/app/livechat/server/startup.js @@ -1,14 +1,17 @@ import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { roomTypes } from '../../utils'; import { LivechatRooms } from '../../models'; import { callbacks } from '../../callbacks'; -import { settings } from '../../settings'; +import { settings } from '../../settings/server'; import { LivechatAgentActivityMonitor } from './statistics/LivechatAgentActivityMonitor'; import { businessHourManager } from './business-hour'; import { createDefaultBusinessHourIfNotExists } from './business-hour/Helper'; import { hasPermission } from '../../authorization/server'; +import { Livechat } from './lib/Livechat'; +import { RoutingManager } from './lib/RoutingManager'; import './roomAccessValidator.internalService'; @@ -36,7 +39,7 @@ Meteor.startup(async () => { const monitor = new LivechatAgentActivityMonitor(); let TroubleshootDisableLivechatActivityMonitor; - settings.get('Troubleshoot_Disable_Livechat_Activity_Monitor', (key, value) => { + settings.watch('Troubleshoot_Disable_Livechat_Activity_Monitor', (value) => { if (TroubleshootDisableLivechatActivityMonitor === value) { return; } TroubleshootDisableLivechatActivityMonitor = value; @@ -48,10 +51,16 @@ Meteor.startup(async () => { }); await createDefaultBusinessHourIfNotExists(); - settings.get('Livechat_enable_business_hours', async (key, value) => { + settings.watch('Livechat_enable_business_hours', async (value) => { if (value) { return businessHourManager.startManager(); } return businessHourManager.stopManager(); }); + + settings.watch('Livechat_Routing_Method', function(value) { + RoutingManager.setMethodNameAndStartQueue(value); + }); + + Accounts.onLogout(({ user }) => user?.roles?.includes('livechat-agent') && !user?.roles?.includes('bot') && Livechat.setUserStatusLivechatIf(user._id, 'not-available', {}, { livechatStatusSystemModified: true })); }); diff --git a/app/livechat/server/statistics/LivechatAgentActivityMonitor.js b/app/livechat/server/statistics/LivechatAgentActivityMonitor.js index 1066e112f027e..2c894afe4f1f9 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/livestream/client/views/broadcastView.js b/app/livestream/client/views/broadcastView.js index 4fbe6fc554fcd..afa8f26142d37 100644 --- a/app/livestream/client/views/broadcastView.js +++ b/app/livestream/client/views/broadcastView.js @@ -3,8 +3,8 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { Session } from 'meteor/session'; import { Template } from 'meteor/templating'; -import { handleError } from '../../../utils'; -import { settings } from '../../../settings'; +import { handleError } from '../../../../client/lib/utils/handleError'; +import { settings } from '../../../settings/client'; const getMedia = () => navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; const createAndConnect = (url) => { diff --git a/app/livestream/client/views/liveStreamTab.js b/app/livestream/client/views/liveStreamTab.js index 19f64cde9299f..90aade2e23b87 100644 --- a/app/livestream/client/views/liveStreamTab.js +++ b/app/livestream/client/views/liveStreamTab.js @@ -4,16 +4,17 @@ import { Blaze } from 'meteor/blaze'; import { Session } from 'meteor/session'; import { Template } from 'meteor/templating'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import toastr from 'toastr'; import { auth } from '../oauth.js'; import { RocketChatAnnouncement } from '../../../lib'; import { popout } from '../../../ui-utils'; -import { t, handleError } from '../../../utils'; +import { t } from '../../../utils'; import { settings } from '../../../settings'; import { callbacks } from '../../../callbacks'; import { hasAllPermission } from '../../../authorization'; import { Users, Rooms } from '../../../models'; +import { handleError } from '../../../../client/lib/utils/handleError'; +import { dispatchToastMessage } from '../../../../client/lib/toast'; export const call = (...args) => new Promise(function(resolve, reject) { Meteor.call(...args, function(err, result) { @@ -134,7 +135,7 @@ Template.liveStreamTab.events({ i.streamingOptions.set(clearedObject); const roomAnnouncement = new RocketChatAnnouncement().getByRoom(i.data.rid); if (roomAnnouncement.getMessage() !== '') { roomAnnouncement.clear(); } - return toastr.success(TAPi18n.__('Livestream_source_changed_succesfully')); + return dispatchToastMessage({ type: 'success', message: TAPi18n.__('Livestream_source_changed_succesfully') }); }); }, 'click .js-save'(e, i) { @@ -166,7 +167,7 @@ Template.liveStreamTab.events({ } } - return toastr.success(TAPi18n.__('Livestream_source_changed_succesfully')); + return dispatchToastMessage({ type: 'success', message: TAPi18n.__('Livestream_source_changed_succesfully') }); }); }, 'click .streaming-source-settings'(e, i) { @@ -226,7 +227,7 @@ Template.liveStreamTab.events({ roomAnnouncement.clear(); } } - return toastr.success(TAPi18n.__('Livestream_source_changed_succesfully')); + return dispatchToastMessage({ type: 'success', message: TAPi18n.__('Livestream_source_changed_succesfully') }); }); }, 'click .js-popout'(e, i) { diff --git a/app/livestream/server/routes.js b/app/livestream/server/routes.js index 3a52aec6031c2..36a093d2c25a9 100644 --- a/app/livestream/server/routes.js +++ b/app/livestream/server/routes.js @@ -1,4 +1,3 @@ -import { Meteor } from 'meteor/meteor'; import google from 'googleapis'; import { settings } from '../../settings'; @@ -9,7 +8,11 @@ const { OAuth2 } = google.auth; API.v1.addRoute('livestream/oauth', { get: function functionName() { - const clientAuth = new OAuth2(settings.get('Broadcasting_client_id'), settings.get('Broadcasting_client_secret'), `${ settings.get('Site_Url') }/api/v1/livestream/oauth/callback`.replace(/\/{2}api/g, '/api')); + const clientAuth = new OAuth2( + settings.get('Broadcasting_client_id'), + settings.get('Broadcasting_client_secret'), + `${ settings.get('Site_Url') }/api/v1/livestream/oauth/callback`.replace(/\/{2}api/g, '/api'), + ); const { userId } = this.queryParams; const url = clientAuth.generateAuthUrl({ access_type: 'offline', @@ -35,9 +38,13 @@ API.v1.addRoute('livestream/oauth/callback', { const { userId } = JSON.parse(state); - const clientAuth = new OAuth2(settings.get('Broadcasting_client_id'), settings.get('Broadcasting_client_secret'), `${ settings.get('Site_Url') }/api/v1/livestream/oauth/callback`.replace(/\/{2}api/g, '/api')); + const clientAuth = new OAuth2( + settings.get('Broadcasting_client_id'), + settings.get('Broadcasting_client_secret'), + `${ settings.get('Site_Url') }/api/v1/livestream/oauth/callback`.replace(/\/{2}api/g, '/api'), + ); - const ret = Meteor.wrapAsync(clientAuth.getToken.bind(clientAuth))(code); + const ret = Promise.await(clientAuth.getToken(code)); Users.update({ _id: userId }, { $set: { 'settings.livestream': ret, diff --git a/app/livestream/server/settings.js b/app/livestream/server/settings.js deleted file mode 100644 index 5fc796efa87d0..0000000000000 --- a/app/livestream/server/settings.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(function() { - settings.addGroup('LiveStream & Broadcasting', function() { - this.add('Livestream_enabled', false, { - type: 'boolean', - public: true, - alert: 'This feature is currently in beta! Please report bugs to github.com/RocketChat/Rocket.Chat/issues', - }); - - this.add('Broadcasting_enabled', false, { - type: 'boolean', - public: true, - alert: 'This feature is currently in beta! Please report bugs to github.com/RocketChat/Rocket.Chat/issues', - enableQuery: { _id: 'Livestream_enabled', value: true }, - }); - - this.add('Broadcasting_client_id', '', { type: 'string', public: false, enableQuery: { _id: 'Broadcasting_enabled', value: true } }); - this.add('Broadcasting_client_secret', '', { type: 'string', public: false, enableQuery: { _id: 'Broadcasting_enabled', value: true } }); - this.add('Broadcasting_api_key', '', { type: 'string', public: false, enableQuery: { _id: 'Broadcasting_enabled', value: true } }); - this.add('Broadcasting_media_server_url', '', { type: 'string', public: true, enableQuery: { _id: 'Broadcasting_enabled', value: true } }); - }); -}); diff --git a/app/livestream/server/settings.ts b/app/livestream/server/settings.ts new file mode 100644 index 0000000000000..75658206347d1 --- /dev/null +++ b/app/livestream/server/settings.ts @@ -0,0 +1,25 @@ +import { Meteor } from 'meteor/meteor'; + +import { settingsRegistry } from '../../settings/server'; + +Meteor.startup(function() { + settingsRegistry.addGroup('LiveStream & Broadcasting', function() { + this.add('Livestream_enabled', false, { + type: 'boolean', + public: true, + alert: 'This feature is currently in beta! Please report bugs to github.com/RocketChat/Rocket.Chat/issues', + }); + + this.add('Broadcasting_enabled', false, { + type: 'boolean', + public: true, + alert: 'This feature is currently in beta! Please report bugs to github.com/RocketChat/Rocket.Chat/issues', + enableQuery: { _id: 'Livestream_enabled', value: true }, + }); + + this.add('Broadcasting_client_id', '', { type: 'string', public: false, enableQuery: { _id: 'Broadcasting_enabled', value: true } }); + this.add('Broadcasting_client_secret', '', { type: 'string', public: false, enableQuery: { _id: 'Broadcasting_enabled', value: true } }); + this.add('Broadcasting_api_key', '', { type: 'string', public: false, enableQuery: { _id: 'Broadcasting_enabled', value: true } }); + this.add('Broadcasting_media_server_url', '', { type: 'string', public: true, enableQuery: { _id: 'Broadcasting_enabled', value: true } }); + }); +}); diff --git a/app/logger/client/logger.js b/app/logger/client/logger.js index 61eb0bf9c11d7..4e05f18ba3b33 100644 --- a/app/logger/client/logger.js +++ b/app/logger/client/logger.js @@ -1,7 +1,7 @@ import { Template } from 'meteor/templating'; import _ from 'underscore'; -import { getConfig } from '../../ui-utils/client/config'; +import { getConfig } from '../../../client/lib/utils/getConfig'; Template.log = !!(getConfig('debug') || getConfig('debug-template')); diff --git a/app/logger/server/index.js b/app/logger/server/index.js index 06fa09708dbe2..5630b3a0aa2aa 100644 --- a/app/logger/server/index.js +++ b/app/logger/server/index.js @@ -1,8 +1,2 @@ -import './streamer.js'; -import { LoggerManager, Logger, SystemLogger } from './server'; - -export { - LoggerManager, - Logger, - SystemLogger, -}; +// TODO there are imports pointing to this file still, ideally we should point everything to "/server/lib/logger/Logger" and remove this file +export { Logger } from '../../../server/lib/logger/Logger'; diff --git a/app/logger/server/server.js b/app/logger/server/server.js deleted file mode 100644 index bce22758c551f..0000000000000 --- a/app/logger/server/server.js +++ /dev/null @@ -1,321 +0,0 @@ -import { EventEmitter } from 'events'; - -import _ from 'underscore'; -import s from 'underscore.string'; - -export const LoggerManager = new class extends EventEmitter { - constructor() { - super(); - this.enabled = false; - this.loggers = {}; - this.queue = []; - this.showPackage = false; - this.showFileAndLine = false; - this.logLevel = 0; - } - - register(logger) { - // eslint-disable-next-line no-use-before-define - if (!(logger instanceof Logger)) { - return; - } - this.loggers[logger.name] = logger; - this.emit('register', logger); - } - - addToQueue(logger, args) { - this.queue.push({ - logger, args, - }); - } - - dispatchQueue() { - _.each(this.queue, (item) => item.logger._log.apply(item.logger, item.args)); - this.clearQueue(); - } - - clearQueue() { - this.queue = []; - } - - disable() { - this.enabled = false; - } - - enable(dispatchQueue = false) { - this.enabled = true; - return dispatchQueue === true ? this.dispatchQueue() : this.clearQueue(); - } -}(); - -const defaultTypes = { - debug: { - name: 'debug', - color: 'blue', - level: 2, - }, - log: { - name: 'info', - color: 'blue', - level: 1, - }, - info: { - name: 'info', - color: 'blue', - level: 1, - }, - success: { - name: 'info', - color: 'green', - level: 1, - }, - warn: { - name: 'warn', - color: 'magenta', - level: 1, - }, - error: { - name: 'error', - color: 'red', - level: 0, - }, - deprecation: { - name: 'warn', - color: 'magenta', - level: 0, - }, -}; - -export class Logger { - constructor(name, config = {}) { - const self = this; - this.name = name; - - this.config = Object.assign({}, config); - if (LoggerManager.loggers && LoggerManager.loggers[this.name] != null) { - LoggerManager.loggers[this.name].warn('Duplicated instance'); - return LoggerManager.loggers[this.name]; - } - _.each(defaultTypes, (typeConfig, type) => { - this[type] = function(...args) { - return self._log.call(self, { - section: this.__section, - type, - level: typeConfig.level, - method: typeConfig.name, - arguments: args, - }); - }; - - self[`${ type }_box`] = function(...args) { - return self._log.call(self, { - section: this.__section, - type, - box: true, - level: typeConfig.level, - method: typeConfig.name, - arguments: args, - }); - }; - }); - if (this.config.methods) { - _.each(this.config.methods, (typeConfig, method) => { - if (this[method] != null) { - self.warn(`Method ${ method } already exists`); - } - if (defaultTypes[typeConfig.type] == null) { - self.warn(`Method type ${ typeConfig.type } does not exist`); - } - this[method] = function(...args) { - return self._log.call(self, { - section: this.__section, - type: typeConfig.type, - level: typeConfig.level != null ? typeConfig.level : defaultTypes[typeConfig.type] && defaultTypes[typeConfig.type].level, - method, - arguments: args, - }); - }; - this[`${ method }_box`] = function(...args) { - return self._log.call(self, { - section: this.__section, - type: typeConfig.type, - box: true, - level: typeConfig.level != null ? typeConfig.level : defaultTypes[typeConfig.type] && defaultTypes[typeConfig.type].level, - method, - arguments: args, - }); - }; - }); - } - if (this.config.sections) { - _.each(this.config.sections, (name, section) => { - this[section] = {}; - _.each(defaultTypes, (typeConfig, type) => { - self[section][type] = (...args) => this[type].apply({ __section: name }, args); - self[section][`${ type }_box`] = (...args) => this[`${ type }_box`].apply({ __section: name }, args); - }); - _.each(this.config.methods, (typeConfig, method) => { - self[section][method] = (...args) => self[method].apply({ __section: name }, args); - self[section][`${ method }_box`] = (...args) => self[`${ method }_box`].apply({ __section: name }, args); - }); - }); - } - - LoggerManager.register(this); - } - - getPrefix(options) { - let prefix = `${ this.name } ➔ ${ options.method }`; - if (options.section) { - prefix = `${ this.name } ➔ ${ options.section }.${ options.method }`; - } - const details = this._getCallerDetails(); - const detailParts = []; - if (details.package && (LoggerManager.showPackage === true || options.type === 'error')) { - detailParts.push(details.package); - } - if (LoggerManager.showFileAndLine === true || options.type === 'error') { - if ((details.file != null) && (details.line != null)) { - detailParts.push(`${ details.file }:${ details.line }`); - } else { - if (details.file != null) { - detailParts.push(details.file); - } - if (details.line != null) { - detailParts.push(details.line); - } - } - } - if (defaultTypes[options.type]) { - // format the message to a colored message - prefix = prefix[defaultTypes[options.type].color]; - } - if (detailParts.length > 0) { - prefix = `${ detailParts.join(' ') } ${ prefix }`; - } - return prefix; - } - - _getCallerDetails() { - const getStack = () => { - // We do NOT use Error.prepareStackTrace here (a V8 extension that gets us a - // core-parsed stack) since it's impossible to compose it with the use of - // Error.prepareStackTrace used on the server for source maps. - const { stack } = new Error(); - return stack; - }; - const stack = getStack(); - if (!stack) { - return {}; - } - const lines = stack.split('\n').splice(1); - // looking for the first line outside the logging package (or an - // eval if we find that first) - let line = lines[0]; - for (let index = 0, len = lines.length; index < len; index++, line = lines[index]) { - if (line.match(/^\s*at eval \(eval/)) { - return { file: 'eval' }; - } - - if (!line.match(/packages\/rocketchat_logger(?:\/|\.js)/)) { - break; - } - } - - const details = {}; - // The format for FF is 'functionName@filePath:lineNumber' - // The format for V8 is 'functionName (packages/logging/logging.js:81)' or - // 'packages/logging/logging.js:81' - const match = /(?:[@(]| at )([^(]+?):([0-9:]+)(?:\)|$)/.exec(line); - if (!match) { - return details; - } - details.line = match[2].split(':')[0]; - // Possible format: https://foo.bar.com/scripts/file.js?random=foobar - // XXX: if you can write the following in better way, please do it - // XXX: what about evals? - details.file = match[1].split('/').slice(-1)[0].split('?')[0]; - const packageMatch = match[1].match(/packages\/([^\.\/]+)(?:\/|\.)/); - if (packageMatch) { - details.package = packageMatch[1]; - } - return details; - } - - makeABox(message, title) { - if (!_.isArray(message)) { - message = message.split('\n'); - } - let len = 0; - - len = Math.max.apply(null, message.map((line) => line.length)); - - const topLine = `+--${ s.pad('', len, '-') }--+`; - const separator = `| ${ s.pad('', len, '') } |`; - let lines = []; - - lines.push(topLine); - if (title) { - lines.push(`| ${ s.lrpad(title, len) } |`); - lines.push(topLine); - } - lines.push(separator); - - lines = [...lines, ...message.map((line) => `| ${ s.rpad(line, len) } |`)]; - - lines.push(separator); - lines.push(topLine); - return lines; - } - - _log(options, ...args) { - if (LoggerManager.enabled === false) { - LoggerManager.addToQueue(this, [options, ...args]); - return; - } - if (options.level == null) { - options.level = 1; - } - - if (LoggerManager.logLevel < options.level) { - return; - } - - // Deferred logging - if (typeof options.arguments[0] === 'function') { - options.arguments[0] = options.arguments[0](); - } - - const prefix = this.getPrefix(options); - - if (options.box === true && _.isString(options.arguments[0])) { - let color = undefined; - if (defaultTypes[options.type]) { - color = defaultTypes[options.type].color; - } - - const box = this.makeABox(options.arguments[0], options.arguments[1]); - let subPrefix = '➔'; - if (color) { - subPrefix = subPrefix[color]; - } - - console.log(subPrefix, prefix); - box.forEach((line) => { - console.log(subPrefix, color ? line[color] : line); - }); - } else { - options.arguments.unshift(prefix); - console.log.apply(console, options.arguments); - } - } -} - -export const SystemLogger = new Logger('System', { - methods: { - startup: { - type: 'success', - level: 0, - }, - }, -}); diff --git a/app/logger/server/streamer.js b/app/logger/server/streamer.js deleted file mode 100644 index 3bff9032aafbe..0000000000000 --- a/app/logger/server/streamer.js +++ /dev/null @@ -1,68 +0,0 @@ -import { EventEmitter } from 'events'; - -import { Meteor } from 'meteor/meteor'; -import { Random } from 'meteor/random'; -import { EJSON } from 'meteor/ejson'; -import { Log } from 'meteor/logging'; - -import { settings } from '../../settings'; -import notifications from '../../notifications/server/lib/Notifications'; - -export const processString = function(string, date) { - let obj; - try { - if (string[0] === '{') { - obj = EJSON.parse(string); - } else { - obj = { - message: string, - time: date, - level: 'info', - }; - } - return Log.format(obj, { color: true }); - } catch (error) { - return string; - } -}; - -export const StdOut = new class extends EventEmitter { - constructor() { - super(); - const { write } = process.stdout; - this.queue = []; - process.stdout.write = (...args) => { - write.apply(process.stdout, args); - const date = new Date(); - const string = processString(args[0], date); - const item = { - id: Random.id(), - string, - ts: date, - }; - this.queue.push(item); - - const limit = settings.get('Log_View_Limit') || 1000; - if (limit && this.queue.length > limit) { - this.queue.shift(); - } - - this.emit('write', string, item); - }; - } -}(); - -Meteor.startup(() => { - const handler = (string, item) => { - // TODO having this as 'emitWithoutBroadcast' will not sent this data to ddp-streamer, so this data - // won't be available when using micro services. - notifications.streamStdout.emitWithoutBroadcast('stdout', { - ...item, - }); - }; - - // do not emit to StdOut if moleculer log level set to debug because it creates an infinite loop - if (String(process.env.MOLECULER_LOG_LEVEL).toLowerCase() !== 'debug') { - StdOut.on('write', handler); - } -}); diff --git a/app/mail-messages/server/functions/sendMail.js b/app/mail-messages/server/functions/sendMail.js index 65dc7687331bf..6b33a1dc2b0f8 100644 --- a/app/mail-messages/server/functions/sendMail.js +++ b/app/mail-messages/server/functions/sendMail.js @@ -3,7 +3,8 @@ import { EJSON } from 'meteor/ejson'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { escapeHTML } from '@rocket.chat/string-helpers'; -import { placeholders } from '../../../utils'; +import { placeholders } from '../../../utils/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import * as Mailer from '../../../mailer'; export const sendMail = function(from, subject, body, dryrun, query) { @@ -33,7 +34,7 @@ export const sendMail = function(from, subject, body, dryrun, query) { email, }); - console.log(`Sending email to ${ email }`); + SystemLogger.debug(`Sending email to ${ email }`); return Mailer.send({ to: email, from, @@ -54,7 +55,7 @@ export const sendMail = function(from, subject, body, dryrun, query) { name: escapeHTML(user.name), email: escapeHTML(email), }); - console.log(`Sending email to ${ email }`); + SystemLogger.debug(`Sending email to ${ email }`); return Mailer.send({ to: email, from, diff --git a/app/mail-messages/server/functions/unsubscribe.js b/app/mail-messages/server/functions/unsubscribe.js index 9d892acb0c50d..c06c568ff8221 100644 --- a/app/mail-messages/server/functions/unsubscribe.js +++ b/app/mail-messages/server/functions/unsubscribe.js @@ -1,8 +1,13 @@ -import { Users } from '../../../models'; +import { Users } from '../../../models/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; export const unsubscribe = function(_id, createdAt) { if (_id && createdAt) { - return Users.rocketMailUnsubscribe(_id, createdAt) === 1; + const affectedRows = Users.rocketMailUnsubscribe(_id, createdAt) === 1; + + SystemLogger.debug('[Mailer:Unsubscribe]', _id, createdAt, new Date(parseInt(createdAt)), affectedRows); + + return affectedRows; } return false; }; diff --git a/app/mail-messages/server/index.js b/app/mail-messages/server/index.js index a05bc57cc61ea..25c66f5dafd6e 100644 --- a/app/mail-messages/server/index.js +++ b/app/mail-messages/server/index.js @@ -1,4 +1,3 @@ -import './startup'; import './methods/sendMail'; import './methods/unsubscribe'; import { Mailer } from './lib/Mailer'; diff --git a/app/mail-messages/server/methods/sendMail.js b/app/mail-messages/server/methods/sendMail.js index 9fef15299682d..179544535cc5d 100644 --- a/app/mail-messages/server/methods/sendMail.js +++ b/app/mail-messages/server/methods/sendMail.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Mailer } from '../lib/Mailer'; -import { hasRole } from '../../../authorization'; +import { hasRole } from '../../../authorization/server'; Meteor.methods({ 'Mailer.sendMail'(from, subject, body, dryrun, query) { diff --git a/app/mail-messages/server/startup.js b/app/mail-messages/server/startup.js deleted file mode 100644 index eadfc796e7762..0000000000000 --- a/app/mail-messages/server/startup.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Permissions } from '../../models'; - -Meteor.startup(function() { - return Permissions.create('access-mailer', ['admin']); -}); diff --git a/app/mailer/server/api.js b/app/mailer/server/api.js deleted file mode 100644 index 1405841f1b45f..0000000000000 --- a/app/mailer/server/api.js +++ /dev/null @@ -1,153 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Email } from 'meteor/email'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import _ from 'underscore'; -import s from 'underscore.string'; -import juice from 'juice'; -import stripHtml from 'string-strip-html'; -import { escapeHTML } from '@rocket.chat/string-helpers'; - -import { settings } from '../../settings/server'; -import { replaceVariables } from './utils.js'; - -let contentHeader; -let contentFooter; - -let body; -let Settings = { - get: () => {}, -}; - -// define server language for email translations -// @TODO: change TAPi18n.__ function to use the server language by default -let lng = 'en'; -settings.get('Language', (key, value) => { - lng = value || 'en'; -}); - - -export const replacekey = (str, key, value = '') => str.replace( - new RegExp(`(\\[${ key }\\]|__${ key }__)`, 'igm'), - value, -); - -export const translate = (str) => replaceVariables(str, (match, key) => TAPi18n.__(key, { lng })); -export const replace = function replace(str, data = {}) { - if (!str) { - return ''; - } - const options = { - Site_Name: Settings.get('Site_Name'), - Site_URL: Settings.get('Site_Url'), - Site_URL_Slash: Settings.get('Site_Url').replace(/\/?$/, '/'), - ...data.name && { - fname: s.strLeft(data.name, ' '), - lname: s.strRightBack(data.name, ' '), - }, - ...data, - }; - return Object.entries(options).reduce((ret, [key, value]) => replacekey(ret, key, value), translate(str)); -}; - -const nonEscapeKeys = ['room_path']; - -export const replaceEscaped = (str, data = {}) => replace(str, { - Site_Name: escapeHTML(settings.get('Site_Name')), - Site_Url: escapeHTML(settings.get('Site_Url')), - ...Object.entries(data).reduce((ret, [key, value]) => { - ret[key] = nonEscapeKeys.includes(key) ? value : escapeHTML(value); - return ret; - }, {}), -}); - -export const wrap = (html, data = {}) => { - if (settings.get('email_plain_text_only')) { - return replace(html, data); - } - - return replaceEscaped(body.replace('{{body}}', html), data); -}; -export const inlinecss = (html) => juice.inlineContent(html, Settings.get('email_style')); -export const getTemplate = (template, fn, escape = true) => { - let html = ''; - Settings.get(template, (key, value) => { - html = value || ''; - fn(escape ? inlinecss(html) : html); - }); - Settings.get('email_style', () => { - fn(escape ? inlinecss(html) : html); - }); -}; -export const getTemplateWrapped = (template, fn) => { - let html = ''; - const wrapInlineCSS = _.debounce(() => fn(wrap(inlinecss(html))), 100); - - Settings.get('Email_Header', () => html && wrapInlineCSS()); - Settings.get('Email_Footer', () => html && wrapInlineCSS()); - Settings.get('email_style', () => html && wrapInlineCSS()); - Settings.get(template, (key, value) => { - html = value || ''; - return html && wrapInlineCSS(); - }); -}; -export const setSettings = (s) => { - Settings = s; - - getTemplate('Email_Header', (value) => { - contentHeader = replace(value || ''); - body = inlinecss(`${ contentHeader } {{body}} ${ contentFooter }`); - }, false); - - getTemplate('Email_Footer', (value) => { - contentFooter = replace(value || ''); - body = inlinecss(`${ contentHeader } {{body}} ${ contentFooter }`); - }, false); - - body = inlinecss(`${ contentHeader } {{body}} ${ contentFooter }`); -}; - -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) => rfcMailPatternWithName.test(from); - -export const sendNoWrap = ({ to, from, replyTo, subject, html, text, headers }) => { - if (!checkAddressFormat(to)) { - return; - } - - if (!text) { - text = stripHtml(html).result; - } - - if (settings.get('email_plain_text_only')) { - html = undefined; - } - - Meteor.defer(() => Email.send({ to, from, replyTo, subject, html, text, headers })); -}; - -export const send = ({ to, from, replyTo, subject, html, text, data, headers }) => - sendNoWrap({ - to, - from, - replyTo, - subject: replace(subject, data), - text: text - ? replace(text, data) - : stripHtml(replace(html, data)).result, - html: wrap(html, data), - headers, - }); - -export const checkAddressFormatAndThrow = (from, func) => { - if (checkAddressFormat(from)) { - return true; - } - throw new Meteor.Error('error-invalid-from-address', 'Invalid from address', { - function: func, - }); -}; - -export const getHeader = () => contentHeader; - -export const getFooter = () => contentFooter; diff --git a/app/mailer/server/api.ts b/app/mailer/server/api.ts new file mode 100644 index 0000000000000..c59cd2151fd39 --- /dev/null +++ b/app/mailer/server/api.ts @@ -0,0 +1,212 @@ +import { Meteor } from 'meteor/meteor'; +import { Email } from 'meteor/email'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import _ from 'underscore'; +import s from 'underscore.string'; +import juice from 'juice'; +import stripHtml from 'string-strip-html'; +import { escapeHTML } from '@rocket.chat/string-helpers'; + +import { settings } from '../../settings/server'; +import { ISetting } from '../../../definition/ISetting'; +import { replaceVariables } from './replaceVariables'; + +let contentHeader: string | undefined; +let contentFooter: string | undefined; +let body: string | undefined; + +// define server language for email translations +// @TODO: change TAPi18n.__ function to use the server language by default +let lng = 'en'; +settings.watch('Language', (value) => { + lng = value || 'en'; +}); + +export const replacekey = (str: string, key: string, value = ''): string => + str.replace( + new RegExp(`(\\[${ key }\\]|__${ key }__)`, 'igm'), + value, + ); + +export const translate = (str: string): string => + replaceVariables(str, (_match, key) => TAPi18n.__(key, { lng })); + +export const replace = (str: string, data: { [key: string]: unknown } = {}): string => { + if (!str) { + return ''; + } + + const options = { + // eslint-disable-next-line @typescript-eslint/camelcase + Site_Name: settings.get('Site_Name'), + // eslint-disable-next-line @typescript-eslint/camelcase + Site_URL: settings.get('Site_Url'), + // eslint-disable-next-line @typescript-eslint/camelcase + Site_URL_Slash: settings.get('Site_Url')?.replace(/\/?$/, '/'), + ...data.name ? { + fname: s.strLeft(String(data.name), ' '), + lname: s.strRightBack(String(data.name), ' '), + } : {}, + ...data, + }; + + return Object.entries(options) + .reduce((ret, [key, value]) => replacekey(ret, key, value), translate(str)); +}; + +const nonEscapeKeys = ['room_path']; + +export const replaceEscaped = (str: string, data: { [key: string]: unknown } = {}): string => { + const siteName = settings.get('Site_Name'); + const siteUrl = settings.get('Site_Url'); + + return replace(str, { + // eslint-disable-next-line @typescript-eslint/camelcase + Site_Name: siteName ? escapeHTML(siteName) : undefined, + // eslint-disable-next-line @typescript-eslint/camelcase + Site_Url: siteUrl ? escapeHTML(siteUrl) : undefined, + ...Object.entries(data).reduce<{[key: string]: string}>((ret, [key, value]) => { + if (value !== undefined && value !== null) { + ret[key] = nonEscapeKeys.includes(key) ? String(value) : escapeHTML(String(value)); + } + return ret; + }, {}), + }); +}; + +export const wrap = (html: string, data: { [key: string]: unknown } = {}): string => { + if (settings.get('email_plain_text_only')) { + return replace(html, data); + } + + if (!body) { + throw new Error('`body` is not set yet'); + } + + return replaceEscaped(body.replace('{{body}}', html), data); +}; +export const inlinecss = (html: string): string => { + const css = settings.get('email_style'); + return css ? juice.inlineContent(html, css) : html; +}; + +export const getTemplate = (template: ISetting['_id'], fn: (html: string) => void, escape = true): void => { + let html = ''; + + settings.watch(template, (value) => { + html = value || ''; + fn(escape ? inlinecss(html) : html); + }); + + settings.watch('email_style', () => { + fn(escape ? inlinecss(html) : html); + }); +}; + +export const getTemplateWrapped = (template: ISetting['_id'], fn: (html: string) => void): void => { + let html = ''; + const wrapInlineCSS = _.debounce(() => fn(wrap(inlinecss(html))), 100); + + settings.watch('Email_Header', () => html && wrapInlineCSS()); + settings.watch('Email_Footer', () => html && wrapInlineCSS()); + settings.watch('email_style', () => html && wrapInlineCSS()); + settings.watch(template, (value) => { + html = value || ''; + return html && wrapInlineCSS(); + }); +}; + +settings.watchMultiple(['Email_Header', 'Email_Footer'], () => { + getTemplate('Email_Header', (value) => { + contentHeader = replace(value || ''); + body = inlinecss(`${ contentHeader } {{body}} ${ contentFooter }`); + }, false); + + getTemplate('Email_Footer', (value) => { + contentFooter = replace(value || ''); + body = inlinecss(`${ contentHeader } {{body}} ${ contentFooter }`); + }, false); + + body = inlinecss(`${ contentHeader } {{body}} ${ contentFooter }`); +}); + +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 = (adresses: string | string[]): boolean => ([] as string[]).concat(adresses).every((address) => rfcMailPatternWithName.test(address)); + +export const sendNoWrap = ({ + to, + from, + replyTo, + subject, + html, + text, + headers, +}: { + to: string; + from: string; + replyTo?: string; + subject: string; + html?: string; + text?: string; + headers?: string; +}): void => { + if (!checkAddressFormat(to)) { + throw new Meteor.Error('invalid email'); + } + + if (!text) { + text = html ? stripHtml(html).result : undefined; + } + + if (settings.get('email_plain_text_only')) { + html = undefined; + } + + Meteor.defer(() => Email.send({ to, from, replyTo, subject, html, text, headers })); +}; + +export const send = ({ + to, + from, + replyTo, + subject, + html, + text, + data, + headers, +}: { + to: string; + from: string; + replyTo?: string; + subject: string; + html?: string; + text?: string; + headers?: string; + data?: { [key: string]: unknown }; +}): void => + sendNoWrap({ + to, + from, + replyTo, + subject: replace(subject, data), + text: (text && replace(text, data)) + || (html && stripHtml(replace(html, data)).result) + || undefined, + html: html ? wrap(html, data) : undefined, + headers, + }); + +export const checkAddressFormatAndThrow = (from: string, func: Function): asserts from => { + if (checkAddressFormat(from)) { + return; + } + + throw new Meteor.Error('error-invalid-from-address', 'Invalid from address', { + function: func, + }); +}; + +export const getHeader = (): string | undefined => contentHeader; + +export const getFooter = (): string | undefined => contentFooter; diff --git a/app/mailer/server/replaceVariables.ts b/app/mailer/server/replaceVariables.ts new file mode 100644 index 0000000000000..4681fd1135c3e --- /dev/null +++ b/app/mailer/server/replaceVariables.ts @@ -0,0 +1,5 @@ +export const replaceVariables = ( + str: string, + replacer: (substring: string, key: string) => string, +): string => + str.replace(/\{ *([^\{\} ]+)[^\{\}]*\}/gmi, replacer); diff --git a/app/mailer/server/utils.js b/app/mailer/server/utils.js deleted file mode 100644 index 796215bd89c73..0000000000000 --- a/app/mailer/server/utils.js +++ /dev/null @@ -1 +0,0 @@ -export const replaceVariables = (str, callback) => str.replace(/\{ *([^\{\} ]+)[^\{\}]*\}/gmi, callback); diff --git a/app/mailer/tests/api.spec.ts b/app/mailer/tests/api.spec.ts new file mode 100644 index 0000000000000..5bb1397c62e7f --- /dev/null +++ b/app/mailer/tests/api.spec.ts @@ -0,0 +1,67 @@ +import { expect } from 'chai'; + +import { replaceVariables } from '../server/replaceVariables'; + +describe('Mailer-API', function() { + describe('replaceVariables', () => { + const i18n: { + [key: string]: string; + } = { + key: 'value', + }; + + describe('single key', function functionName() { + it(`should be equal to test ${ 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 }`, () => { + 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 }`, () => { + 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 }`, () => { + 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 }`, () => { + 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 }`, () => { + 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 }`, () => { + 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 }}`, () => { + 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 }}`, () => { + expect(`test {${ i18n.key }}`).to.be.equal(replaceVariables('test {{key}}', (_match, key) => i18n[key])); + }); + }); + }); +}); diff --git a/app/mailer/tests/api.tests.js b/app/mailer/tests/api.tests.js deleted file mode 100644 index 6ecf3b9f9de17..0000000000000 --- a/app/mailer/tests/api.tests.js +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint-env mocha */ -import assert from 'assert'; - -import { replaceVariables } from '../server/utils.js'; - -describe('Mailer-API', function() { - describe('translate', () => { - const i18n = { - key: 'value', - }; - - describe('single key', function functionName() { - it(`should be equal to test ${ i18n.key }`, () => { - assert.equal(`test ${ i18n.key }`, replaceVariables('test {key}', (match, key) => i18n[key])); - }); - }); - - describe('multiple keys', function functionName() { - it(`should be equal to test ${ i18n.key } and ${ i18n.key }`, () => { - assert.equal(`test ${ i18n.key } and ${ i18n.key }`, 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.equal(`test ${ i18n.key }`, replaceVariables('test {key }', (match, key) => i18n[key])); - }); - }); - - describe('key with a leading space', function functionName() { - it(`should be equal to test ${ i18n.key }`, () => { - assert.equal(`test ${ i18n.key }`, 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.equal(`test ${ i18n.key }`, replaceVariables('test { key }', (match, key) => i18n[key])); - }); - }); - - describe('key with multiple words', function functionName() { - it(`should be equal to test ${ i18n.key }`, () => { - assert.equal(`test ${ i18n.key }`, 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.equal(`test {${ i18n.key }`, replaceVariables('test {{key}', (match, key) => i18n[key])); - }); - }); - - describe('key with multiple closing brackets', function functionName() { - it(`should be equal to test ${ i18n.key }}`, () => { - assert.equal(`test ${ i18n.key }}`, 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.equal(`test {${ i18n.key }}`, replaceVariables('test {{key}}', (match, key) => i18n[key])); - }); - }); - }); -}); diff --git a/app/mapview/server/settings.js b/app/mapview/server/settings.js deleted file mode 100644 index 96c604de513e0..0000000000000 --- a/app/mapview/server/settings.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(function() { - settings.add('MapView_Enabled', false, { type: 'boolean', group: 'Message', section: 'Google Maps', public: true, i18nLabel: 'MapView_Enabled', i18nDescription: 'MapView_Enabled_Description' }); - return settings.add('MapView_GMapsAPIKey', '', { type: 'string', group: 'Message', section: 'Google Maps', public: true, i18nLabel: 'MapView_GMapsAPIKey', i18nDescription: 'MapView_GMapsAPIKey_Description', secret: true }); -}); diff --git a/app/mapview/server/settings.ts b/app/mapview/server/settings.ts new file mode 100644 index 0000000000000..9fccbb8647832 --- /dev/null +++ b/app/mapview/server/settings.ts @@ -0,0 +1,8 @@ +import { Meteor } from 'meteor/meteor'; + +import { settingsRegistry } from '../../settings/server'; + +Meteor.startup(function() { + settingsRegistry.add('MapView_Enabled', false, { type: 'boolean', group: 'Message', section: 'Google Maps', public: true, i18nLabel: 'MapView_Enabled', i18nDescription: 'MapView_Enabled_Description' }); + settingsRegistry.add('MapView_GMapsAPIKey', '', { type: 'string', group: 'Message', section: 'Google Maps', public: true, i18nLabel: 'MapView_GMapsAPIKey', i18nDescription: 'MapView_GMapsAPIKey_Description', secret: true }); +}); diff --git a/app/markdown/lib/hljs.js b/app/markdown/lib/hljs.js index a8a57dd89cdee..6a5232ba2f1cc 100644 --- a/app/markdown/lib/hljs.js +++ b/app/markdown/lib/hljs.js @@ -1,371 +1,361 @@ import hljs from 'highlight.js/lib/highlight'; -// import onec from 'highlight.js/lib/languages/1c'; -// import abnf from 'highlight.js/lib/languages/abnf'; -// import accesslog from 'highlight.js/lib/languages/accesslog'; -// import actionscript from 'highlight.js/lib/languages/actionscript'; -// import ada from 'highlight.js/lib/languages/ada'; -// import angelscript from 'highlight.js/lib/languages/angelscript'; -// import apache from 'highlight.js/lib/languages/apache'; -// import applescript from 'highlight.js/lib/languages/applescript'; -// import arcade from 'highlight.js/lib/languages/arcade'; -// import cpp from 'highlight.js/lib/languages/cpp'; -// import arduino from 'highlight.js/lib/languages/arduino'; -// import armasm from 'highlight.js/lib/languages/armasm'; -// import xml from 'highlight.js/lib/languages/xml'; -// import asciidoc from 'highlight.js/lib/languages/asciidoc'; -// import aspectj from 'highlight.js/lib/languages/aspectj'; -// import autohotkey from 'highlight.js/lib/languages/autohotkey'; -// import autoit from 'highlight.js/lib/languages/autoit'; -// import avrasm from 'highlight.js/lib/languages/avrasm'; -// import awk from 'highlight.js/lib/languages/awk'; -// import axapta from 'highlight.js/lib/languages/axapta'; -import bash from 'highlight.js/lib/languages/bash'; -// import basic from 'highlight.js/lib/languages/basic'; -// import bnf from 'highlight.js/lib/languages/bnf'; -// import brainfuck from 'highlight.js/lib/languages/brainfuck'; -// import cal from 'highlight.js/lib/languages/cal'; -// import capnproto from 'highlight.js/lib/languages/capnproto'; -// import ceylon from 'highlight.js/lib/languages/ceylon'; import clean from 'highlight.js/lib/languages/clean'; -// import clojure from 'highlight.js/lib/languages/clojure'; -// import clojureRepl from 'highlight.js/lib/languages/clojure-repl'; -// import cmake from 'highlight.js/lib/languages/cmake'; -// import coq from 'highlight.js/lib/languages/coq'; -// import cos from 'highlight.js/lib/languages/cos'; -// import crmsh from 'highlight.js/lib/languages/crmsh'; -// import crystal from 'highlight.js/lib/languages/crystal'; -// import cs from 'highlight.js/lib/languages/cs'; -// import csp from 'highlight.js/lib/languages/csp'; -import css from 'highlight.js/lib/languages/css'; -// import d from 'highlight.js/lib/languages/d'; import markdown from 'highlight.js/lib/languages/markdown'; -// import dart from 'highlight.js/lib/languages/dart'; -// import delphi from 'highlight.js/lib/languages/delphi'; -// import diff from 'highlight.js/lib/languages/diff'; -// import django from 'highlight.js/lib/languages/django'; -// import dns from 'highlight.js/lib/languages/dns'; -import dockerfile from 'highlight.js/lib/languages/dockerfile'; -// import dos from 'highlight.js/lib/languages/dos'; -// import dsconfig from 'highlight.js/lib/languages/dsconfig'; -// import dts from 'highlight.js/lib/languages/dts'; -// import dust from 'highlight.js/lib/languages/dust'; -// import ebnf from 'highlight.js/lib/languages/ebnf'; -// import elixir from 'highlight.js/lib/languages/elixir'; -// import elm from 'highlight.js/lib/languages/elm'; -// import ruby from 'highlight.js/lib/languages/ruby'; -// import erb from 'highlight.js/lib/languages/erb'; -// import erlangRepl from 'highlight.js/lib/languages/erlang-repl'; -// import erlang from 'highlight.js/lib/languages/erlang'; -// import excel from 'highlight.js/lib/languages/excel'; -// import fix from 'highlight.js/lib/languages/fix'; -// import flix from 'highlight.js/lib/languages/flix'; -// import fortran from 'highlight.js/lib/languages/fortran'; -// import fsharp from 'highlight.js/lib/languages/fsharp'; -// import gams from 'highlight.js/lib/languages/gams'; -// import gauss from 'highlight.js/lib/languages/gauss'; -// import gcode from 'highlight.js/lib/languages/gcode'; -// import gherkin from 'highlight.js/lib/languages/gherkin'; -// import glsl from 'highlight.js/lib/languages/glsl'; -// import gml from 'highlight.js/lib/languages/gml'; -import go from 'highlight.js/lib/languages/go'; -// import golo from 'highlight.js/lib/languages/golo'; -// import gradle from 'highlight.js/lib/languages/gradle'; -// import groovy from 'highlight.js/lib/languages/groovy'; -// import haml from 'highlight.js/lib/languages/haml'; -// import handlebars from 'highlight.js/lib/languages/handlebars'; -// import haskell from 'highlight.js/lib/languages/haskell'; -// import haxe from 'highlight.js/lib/languages/haxe'; -// import hsp from 'highlight.js/lib/languages/hsp'; -// import htmlbars from 'highlight.js/lib/languages/htmlbars'; -// import http from 'highlight.js/lib/languages/http'; -// import hy from 'highlight.js/lib/languages/hy'; -// import inform7 from 'highlight.js/lib/languages/inform7'; -// import ini from 'highlight.js/lib/languages/ini'; -// import irpf90 from 'highlight.js/lib/languages/irpf90'; -// import isbl from 'highlight.js/lib/languages/isbl'; -// import java from 'highlight.js/lib/languages/java'; import javascript from 'highlight.js/lib/languages/javascript'; -// import jbossCli from 'highlight.js/lib/languages/jboss-cli'; -import json from 'highlight.js/lib/languages/json'; -// import julia from 'highlight.js/lib/languages/julia'; -// import juliaRepl from 'highlight.js/lib/languages/julia-repl'; -// import kotlin from 'highlight.js/lib/languages/kotlin'; -// import lasso from 'highlight.js/lib/languages/lasso'; -// import ldif from 'highlight.js/lib/languages/ldif'; -// import leaf from 'highlight.js/lib/languages/leaf'; -// import less from 'highlight.js/lib/languages/less'; -// import lisp from 'highlight.js/lib/languages/lisp'; -// import livecodeserver from 'highlight.js/lib/languages/livecodeserver'; -// import llvm from 'highlight.js/lib/languages/llvm'; -// import lsl from 'highlight.js/lib/languages/lsl'; -// import lua from 'highlight.js/lib/languages/lua'; -// import makefile from 'highlight.js/lib/languages/makefile'; -// import mathematica from 'highlight.js/lib/languages/mathematica'; -// import matlab from 'highlight.js/lib/languages/matlab'; -// import maxima from 'highlight.js/lib/languages/maxima'; -// import mel from 'highlight.js/lib/languages/mel'; -// import mercury from 'highlight.js/lib/languages/mercury'; -// import mipsasm from 'highlight.js/lib/languages/mipsasm'; -// import mizar from 'highlight.js/lib/languages/mizar'; -// import perl from 'highlight.js/lib/languages/perl'; -// import mojolicious from 'highlight.js/lib/languages/mojolicious'; -// import monkey from 'highlight.js/lib/languages/monkey'; -// import moonscript from 'highlight.js/lib/languages/moonscript'; -// import n1ql from 'highlight.js/lib/languages/n1ql'; -// import nginx from 'highlight.js/lib/languages/nginx'; -// import nimrod from 'highlight.js/lib/languages/nimrod'; -// import nix from 'highlight.js/lib/languages/nix'; -// import nsis from 'highlight.js/lib/languages/nsis'; -// import objectivec from 'highlight.js/lib/languages/objectivec'; -// import ocaml from 'highlight.js/lib/languages/ocaml'; -// import openscad from 'highlight.js/lib/languages/openscad'; -// import oxygene from 'highlight.js/lib/languages/oxygene'; -// import parser3 from 'highlight.js/lib/languages/parser3'; -// import pf from 'highlight.js/lib/languages/pf'; -// import pgsql from 'highlight.js/lib/languages/pgsql'; -// import php from 'highlight.js/lib/languages/php'; -import plaintext from 'highlight.js/lib/languages/plaintext'; -// import pony from 'highlight.js/lib/languages/pony'; -import powershell from 'highlight.js/lib/languages/powershell'; -// import processing from 'highlight.js/lib/languages/processing'; -// import profile from 'highlight.js/lib/languages/profile'; -// import prolog from 'highlight.js/lib/languages/prolog'; -// import properties from 'highlight.js/lib/languages/properties'; -// import protobuf from 'highlight.js/lib/languages/protobuf'; -// import puppet from 'highlight.js/lib/languages/puppet'; -// import purebasic from 'highlight.js/lib/languages/purebasic'; -// import python from 'highlight.js/lib/languages/python'; -// import q from 'highlight.js/lib/languages/q'; -// import qml from 'highlight.js/lib/languages/qml'; -// import r from 'highlight.js/lib/languages/r'; -// import reasonml from 'highlight.js/lib/languages/reasonml'; -// import rib from 'highlight.js/lib/languages/rib'; -// import roboconf from 'highlight.js/lib/languages/roboconf'; -// import routeros from 'highlight.js/lib/languages/routeros'; -// import rsl from 'highlight.js/lib/languages/rsl'; -// import ruleslanguage from 'highlight.js/lib/languages/ruleslanguage'; -import rust from 'highlight.js/lib/languages/rust'; -// import sas from 'highlight.js/lib/languages/sas'; -// import scala from 'highlight.js/lib/languages/scala'; -// import scheme from 'highlight.js/lib/languages/scheme'; -// import scilab from 'highlight.js/lib/languages/scilab'; -import scss from 'highlight.js/lib/languages/scss'; -import shell from 'highlight.js/lib/languages/shell'; -// import smali from 'highlight.js/lib/languages/smali'; -// import smalltalk from 'highlight.js/lib/languages/smalltalk'; -// import sml from 'highlight.js/lib/languages/sml'; -// import sqf from 'highlight.js/lib/languages/sqf'; -// import sql from 'highlight.js/lib/languages/sql'; -// import stan from 'highlight.js/lib/languages/stan'; -// import stata from 'highlight.js/lib/languages/stata'; -// import step21 from 'highlight.js/lib/languages/step21'; -// import stylus from 'highlight.js/lib/languages/stylus'; -// import subunit from 'highlight.js/lib/languages/subunit'; -// import swift from 'highlight.js/lib/languages/swift'; -// import taggerscript from 'highlight.js/lib/languages/taggerscript'; -import yaml from 'highlight.js/lib/languages/yaml'; -// import tap from 'highlight.js/lib/languages/tap'; -// import tcl from 'highlight.js/lib/languages/tcl'; -// import tex from 'highlight.js/lib/languages/tex'; -// import thrift from 'highlight.js/lib/languages/thrift'; -// import tp from 'highlight.js/lib/languages/tp'; -// import twig from 'highlight.js/lib/languages/twig'; -// import typescript from 'highlight.js/lib/languages/typescript'; -// import vala from 'highlight.js/lib/languages/vala'; -// import vbnet from 'highlight.js/lib/languages/vbnet'; -// import vbscript from 'highlight.js/lib/languages/vbscript'; -// import vbscriptHtml from 'highlight.js/lib/languages/vbscript-html'; -// import verilog from 'highlight.js/lib/languages/verilog'; -// import vhdl from 'highlight.js/lib/languages/vhdl'; -import vim from 'highlight.js/lib/languages/vim'; -// import x86asm from 'highlight.js/lib/languages/x86asm'; -// import xl from 'highlight.js/lib/languages/xl'; -// import xquery from 'highlight.js/lib/languages/xquery'; -// import zephir from 'highlight.js/lib/languages/zephir'; - -hljs.registerLanguage('javascript', javascript); -// hljs.registerLanguage('typescript', typescript); -// hljs.registerLanguage('python', python); -// hljs.registerLanguage('java', java); -// hljs.registerLanguage('php', php); -hljs.registerLanguage('css', css); hljs.registerLanguage('markdown', markdown); -hljs.registerLanguage('dockerfile', dockerfile); -hljs.registerLanguage('json', json); -// hljs.registerLanguage('r', r); -// hljs.registerLanguage('objectivec', objectivec); -// hljs.registerLanguage('swift', swift); -// hljs.registerLanguage('matlab', matlab); -// hljs.registerLanguage('kotlin', kotlin); -hljs.registerLanguage('go', go); -// hljs.registerLanguage('ruby', ruby); -// hljs.registerLanguage('scala', scala); -hljs.registerLanguage('rust', rust); -// hljs.registerLanguage('dart', dart); -// hljs.registerLanguage('lua', lua); -// hljs.registerLanguage('ada', ada); -// hljs.registerLanguage('groovy', groovy); -// hljs.registerLanguage('julia', julia); -// hljs.registerLanguage('julia-repl', juliaRepl); -// hljs.registerLanguage('haskell', haskell); -// hljs.registerLanguage('delphi', delphi); hljs.registerLanguage('clean', clean); -// hljs.registerLanguage('1c', onec); -// hljs.registerLanguage('abnf', abnf); -// hljs.registerLanguage('accesslog', accesslog); -// hljs.registerLanguage('actionscript', actionscript); -// hljs.registerLanguage('angelscript', angelscript); -// hljs.registerLanguage('apache', apache); -// hljs.registerLanguage('applescript', applescript); -// hljs.registerLanguage('arcade', arcade); -// hljs.registerLanguage('cpp', cpp); -// hljs.registerLanguage('arduino', arduino); -// hljs.registerLanguage('armasm', armasm); -// hljs.registerLanguage('xml', xml); -// hljs.registerLanguage('asciidoc', asciidoc); -// hljs.registerLanguage('aspectj', aspectj); -// hljs.registerLanguage('autohotkey', autohotkey); -// hljs.registerLanguage('autoit', autoit); -// hljs.registerLanguage('avrasm', avrasm); -// hljs.registerLanguage('awk', awk); -// hljs.registerLanguage('axapta', axapta); -hljs.registerLanguage('bash', bash); -// hljs.registerLanguage('basic', basic); -// hljs.registerLanguage('bnf', bnf); -// hljs.registerLanguage('brainfuck', brainfuck); -// hljs.registerLanguage('cal', cal); -// hljs.registerLanguage('capnproto', capnproto); -// hljs.registerLanguage('ceylon', ceylon); -// hljs.registerLanguage('clojure', clojure); -// hljs.registerLanguage('clojure-repl', clojureRepl); -// hljs.registerLanguage('cmake', cmake); -// hljs.registerLanguage('coq', coq); -// hljs.registerLanguage('cos', cos); -// hljs.registerLanguage('crmsh', crmsh); -// hljs.registerLanguage('crystal', crystal); -// hljs.registerLanguage('cs', cs); -// hljs.registerLanguage('csp', csp); -// hljs.registerLanguage('d', d); -// hljs.registerLanguage('diff', diff); -// hljs.registerLanguage('django', django); -// hljs.registerLanguage('dns', dns); -// hljs.registerLanguage('dos', dos); -// hljs.registerLanguage('dsconfig', dsconfig); -// hljs.registerLanguage('dts', dts); -// hljs.registerLanguage('dust', dust); -// hljs.registerLanguage('ebnf', ebnf); -// hljs.registerLanguage('elixir', elixir); -// hljs.registerLanguage('elm', elm); -// hljs.registerLanguage('erb', erb); -// hljs.registerLanguage('erlang-repl', erlangRepl); -// hljs.registerLanguage('erlang', erlang); -// hljs.registerLanguage('excel', excel); -// hljs.registerLanguage('fix', fix); -// hljs.registerLanguage('flix', flix); -// hljs.registerLanguage('fortran', fortran); -// hljs.registerLanguage('fsharp', fsharp); -// hljs.registerLanguage('gams', gams); -// hljs.registerLanguage('gauss', gauss); -// hljs.registerLanguage('gcode', gcode); -// hljs.registerLanguage('gherkin', gherkin); -// hljs.registerLanguage('glsl', glsl); -// hljs.registerLanguage('gml', gml); -// hljs.registerLanguage('golo', golo); -// hljs.registerLanguage('gradle', gradle); -// hljs.registerLanguage('haml', haml); -// hljs.registerLanguage('handlebars', handlebars); -// hljs.registerLanguage('haxe', haxe); -// hljs.registerLanguage('hsp', hsp); -// hljs.registerLanguage('htmlbars', htmlbars); -// hljs.registerLanguage('http', http); -// hljs.registerLanguage('hy', hy); -// hljs.registerLanguage('inform7', inform7); -// hljs.registerLanguage('ini', ini); -// hljs.registerLanguage('irpf90', irpf90); -// hljs.registerLanguage('isbl', isbl); -// hljs.registerLanguage('jboss-cli', jbossCli); -// hljs.registerLanguage('lasso', lasso); -// hljs.registerLanguage('ldif', ldif); -// hljs.registerLanguage('leaf', leaf); -// hljs.registerLanguage('less', less); -// hljs.registerLanguage('lisp', lisp); -// hljs.registerLanguage('livecodeserver', livecodeserver); -// hljs.registerLanguage('llvm', llvm); -// hljs.registerLanguage('lsl', lsl); -// hljs.registerLanguage('makefile', makefile); -// hljs.registerLanguage('mathematica', mathematica); -// hljs.registerLanguage('maxima', maxima); -// hljs.registerLanguage('mel', mel); -// hljs.registerLanguage('mercury', mercury); -// hljs.registerLanguage('mipsasm', mipsasm); -// hljs.registerLanguage('mizar', mizar); -// hljs.registerLanguage('perl', perl); -// hljs.registerLanguage('mojolicious', mojolicious); -// hljs.registerLanguage('monkey', monkey); -// hljs.registerLanguage('moonscript', moonscript); -// hljs.registerLanguage('n1ql', n1ql); -// hljs.registerLanguage('nginx', nginx); -// hljs.registerLanguage('nimrod', nimrod); -// hljs.registerLanguage('nix', nix); -// hljs.registerLanguage('nsis', nsis); -// hljs.registerLanguage('ocaml', ocaml); -// hljs.registerLanguage('openscad', openscad); -// hljs.registerLanguage('oxygene', oxygene); -// hljs.registerLanguage('parser3', parser3); -// hljs.registerLanguage('pf', pf); -// hljs.registerLanguage('pgsql', pgsql); -hljs.registerLanguage('plaintext', plaintext); -// hljs.registerLanguage('pony', pony); -hljs.registerLanguage('powershell', powershell); -// hljs.registerLanguage('processing', processing); -// hljs.registerLanguage('profile', profile); -// hljs.registerLanguage('prolog', prolog); -// hljs.registerLanguage('properties', properties); -// hljs.registerLanguage('protobuf', protobuf); -// hljs.registerLanguage('puppet', puppet); -// hljs.registerLanguage('purebasic', purebasic); -// hljs.registerLanguage('q', q); -// hljs.registerLanguage('qml', qml); -// hljs.registerLanguage('reasonml', reasonml); -// hljs.registerLanguage('rib', rib); -// hljs.registerLanguage('roboconf', roboconf); -// hljs.registerLanguage('routeros', routeros); -// hljs.registerLanguage('rsl', rsl); -// hljs.registerLanguage('ruleslanguage', ruleslanguage); -// hljs.registerLanguage('sas', sas); -// hljs.registerLanguage('scheme', scheme); -// hljs.registerLanguage('scilab', scilab); -hljs.registerLanguage('scss', scss); -hljs.registerLanguage('shell', shell); -// hljs.registerLanguage('smali', smali); -// hljs.registerLanguage('smalltalk', smalltalk); -// hljs.registerLanguage('sml', sml); -// hljs.registerLanguage('sqf', sqf); -// hljs.registerLanguage('sql', sql); -// hljs.registerLanguage('stan', stan); -// hljs.registerLanguage('stata', stata); -// hljs.registerLanguage('step21', step21); -// hljs.registerLanguage('stylus', stylus); -// hljs.registerLanguage('subunit', subunit); -// hljs.registerLanguage('taggerscript', taggerscript); -hljs.registerLanguage('yaml', yaml); -// hljs.registerLanguage('tap', tap); -// hljs.registerLanguage('tcl', tcl); -// hljs.registerLanguage('tex', tex); -// hljs.registerLanguage('thrift', thrift); -// hljs.registerLanguage('tp', tp); -// hljs.registerLanguage('twig', twig); -// hljs.registerLanguage('vala', vala); -// hljs.registerLanguage('vbnet', vbnet); -// hljs.registerLanguage('vbscript', vbscript); -// hljs.registerLanguage('vbscript-html', vbscriptHtml); -// hljs.registerLanguage('verilog', verilog); -// hljs.registerLanguage('vhdl', vhdl); -hljs.registerLanguage('vim', vim); -// hljs.registerLanguage('x86asm', x86asm); -// hljs.registerLanguage('xl', xl); -// hljs.registerLanguage('xquery', xquery); -// hljs.registerLanguage('zephir', zephir); +hljs.registerLanguage('javascript', javascript); +// eslint-disable-next-line complexity +export const register = async (lang) => { + switch (lang) { + case 'onec': + return hljs.registerLanguage('onec', (await import('highlight.js/lib/languages/1c')).default); + case 'abnf': + return hljs.registerLanguage('abnf', (await import('highlight.js/lib/languages/abnf')).default); + case 'accesslog': + return hljs.registerLanguage('accesslog', (await import('highlight.js/lib/languages/accesslog')).default); + case 'actionscript': + return hljs.registerLanguage('actionscript', (await import('highlight.js/lib/languages/actionscript')).default); + case 'ada': + return hljs.registerLanguage('ada', (await import('highlight.js/lib/languages/ada')).default); + case 'apache': + return hljs.registerLanguage('apache', (await import('highlight.js/lib/languages/apache')).default); + case 'applescript': + return hljs.registerLanguage('applescript', (await import('highlight.js/lib/languages/applescript')).default); + case 'arduino': + return hljs.registerLanguage('arduino', (await import('highlight.js/lib/languages/arduino')).default); + case 'armasm': + return hljs.registerLanguage('armasm', (await import('highlight.js/lib/languages/armasm')).default); + case 'asciidoc': + return hljs.registerLanguage('asciidoc', (await import('highlight.js/lib/languages/asciidoc')).default); + case 'aspectj': + return hljs.registerLanguage('aspectj', (await import('highlight.js/lib/languages/aspectj')).default); + case 'autohotkey': + return hljs.registerLanguage('autohotkey', (await import('highlight.js/lib/languages/autohotkey')).default); + case 'autoit': + return hljs.registerLanguage('autoit', (await import('highlight.js/lib/languages/autoit')).default); + case 'avrasm': + return hljs.registerLanguage('avrasm', (await import('highlight.js/lib/languages/avrasm')).default); + case 'awk': + return hljs.registerLanguage('awk', (await import('highlight.js/lib/languages/awk')).default); + case 'axapta': + return hljs.registerLanguage('axapta', (await import('highlight.js/lib/languages/axapta')).default); + case 'bash': + return hljs.registerLanguage('bash', (await import('highlight.js/lib/languages/bash')).default); + case 'basic': + return hljs.registerLanguage('basic', (await import('highlight.js/lib/languages/basic')).default); + case 'bnf': + return hljs.registerLanguage('bnf', (await import('highlight.js/lib/languages/bnf')).default); + case 'brainfuck': + return hljs.registerLanguage('brainfuck', (await import('highlight.js/lib/languages/brainfuck')).default); + case 'cal': + return hljs.registerLanguage('cal', (await import('highlight.js/lib/languages/cal')).default); + case 'capnproto': + return hljs.registerLanguage('capnproto', (await import('highlight.js/lib/languages/capnproto')).default); + case 'ceylon': + return hljs.registerLanguage('ceylon', (await import('highlight.js/lib/languages/ceylon')).default); + case 'clean': + return hljs.registerLanguage('clean', (await import('highlight.js/lib/languages/clean')).default); + case 'clojure': + return hljs.registerLanguage('clojure', (await import('highlight.js/lib/languages/clojure')).default); + case 'clojure-repl': + return hljs.registerLanguage('clojure-repl', (await import('highlight.js/lib/languages/clojure-repl')).default); + case 'cmake': + return hljs.registerLanguage('cmake', (await import('highlight.js/lib/languages/cmake')).default); + case 'coffeescript': + return hljs.registerLanguage('coffeescript', (await import('highlight.js/lib/languages/coffeescript')).default); + case 'coq': + return hljs.registerLanguage('coq', (await import('highlight.js/lib/languages/coq')).default); + case 'cos': + return hljs.registerLanguage('cos', (await import('highlight.js/lib/languages/cos')).default); + case 'cpp': + return hljs.registerLanguage('cpp', (await import('highlight.js/lib/languages/cpp')).default); + case 'crmsh': + return hljs.registerLanguage('crmsh', (await import('highlight.js/lib/languages/crmsh')).default); + case 'crystal': + return hljs.registerLanguage('crystal', (await import('highlight.js/lib/languages/crystal')).default); + case 'cs': + return hljs.registerLanguage('cs', (await import('highlight.js/lib/languages/cs')).default); + case 'csp': + return hljs.registerLanguage('csp', (await import('highlight.js/lib/languages/csp')).default); + case 'css': + return hljs.registerLanguage('css', (await import('highlight.js/lib/languages/css')).default); + case 'd': + return hljs.registerLanguage('d', (await import('highlight.js/lib/languages/d')).default); + case 'dart': + return hljs.registerLanguage('dart', (await import('highlight.js/lib/languages/dart')).default); + case 'delphi': + return hljs.registerLanguage('delphi', (await import('highlight.js/lib/languages/delphi')).default); + case 'diff': + return hljs.registerLanguage('diff', (await import('highlight.js/lib/languages/diff')).default); + case 'django': + return hljs.registerLanguage('django', (await import('highlight.js/lib/languages/django')).default); + case 'dns': + return hljs.registerLanguage('dns', (await import('highlight.js/lib/languages/dns')).default); + case 'dockerfile': + return hljs.registerLanguage('dockerfile', (await import('highlight.js/lib/languages/dockerfile')).default); + case 'dos': + return hljs.registerLanguage('dos', (await import('highlight.js/lib/languages/dos')).default); + case 'dsconfig': + return hljs.registerLanguage('dsconfig', (await import('highlight.js/lib/languages/dsconfig')).default); + case 'dts': + return hljs.registerLanguage('dts', (await import('highlight.js/lib/languages/dts')).default); + case 'dust': + return hljs.registerLanguage('dust', (await import('highlight.js/lib/languages/dust')).default); + case 'ebnf': + return hljs.registerLanguage('ebnf', (await import('highlight.js/lib/languages/ebnf')).default); + case 'elixir': + return hljs.registerLanguage('elixir', (await import('highlight.js/lib/languages/elixir')).default); + case 'elm': + return hljs.registerLanguage('elm', (await import('highlight.js/lib/languages/elm')).default); + case 'erb': + return hljs.registerLanguage('erb', (await import('highlight.js/lib/languages/erb')).default); + case 'erlang': + return hljs.registerLanguage('erlang', (await import('highlight.js/lib/languages/erlang')).default); + case 'excel': + return hljs.registerLanguage('excel', (await import('highlight.js/lib/languages/excel')).default); + case 'fix': + return hljs.registerLanguage('fix', (await import('highlight.js/lib/languages/fix')).default); + case 'flix': + return hljs.registerLanguage('flix', (await import('highlight.js/lib/languages/flix')).default); + case 'fortran': + return hljs.registerLanguage('fortran', (await import('highlight.js/lib/languages/fortran')).default); + case 'fsharp': + return hljs.registerLanguage('fsharp', (await import('highlight.js/lib/languages/fsharp')).default); + case 'gams': + return hljs.registerLanguage('gams', (await import('highlight.js/lib/languages/gams')).default); + case 'gauss': + return hljs.registerLanguage('gauss', (await import('highlight.js/lib/languages/gauss')).default); + case 'gcode': + return hljs.registerLanguage('gcode', (await import('highlight.js/lib/languages/gcode')).default); + case 'gherkin': + return hljs.registerLanguage('gherkin', (await import('highlight.js/lib/languages/gherkin')).default); + case 'glsl': + return hljs.registerLanguage('glsl', (await import('highlight.js/lib/languages/glsl')).default); + case 'go': + return hljs.registerLanguage('go', (await import('highlight.js/lib/languages/go')).default); + case 'golo': + return hljs.registerLanguage('golo', (await import('highlight.js/lib/languages/golo')).default); + case 'gradle': + return hljs.registerLanguage('gradle', (await import('highlight.js/lib/languages/gradle')).default); + case 'groovy': + return hljs.registerLanguage('groovy', (await import('highlight.js/lib/languages/groovy')).default); + case 'haml': + return hljs.registerLanguage('haml', (await import('highlight.js/lib/languages/haml')).default); + case 'handlebars': + return hljs.registerLanguage('handlebars', (await import('highlight.js/lib/languages/handlebars')).default); + case 'haskell': + return hljs.registerLanguage('haskell', (await import('highlight.js/lib/languages/haskell')).default); + case 'haxe': + return hljs.registerLanguage('haxe', (await import('highlight.js/lib/languages/haxe')).default); + case 'hsp': + return hljs.registerLanguage('hsp', (await import('highlight.js/lib/languages/hsp')).default); + case 'htmlbars': + return hljs.registerLanguage('htmlbars', (await import('highlight.js/lib/languages/htmlbars')).default); + case 'http': + return hljs.registerLanguage('http', (await import('highlight.js/lib/languages/http')).default); + case 'hy': + return hljs.registerLanguage('hy', (await import('highlight.js/lib/languages/hy')).default); + case 'inform7': + return hljs.registerLanguage('inform7', (await import('highlight.js/lib/languages/inform7')).default); + case 'ini': + return hljs.registerLanguage('ini', (await import('highlight.js/lib/languages/ini')).default); + case 'irpf90': + return hljs.registerLanguage('irpf90', (await import('highlight.js/lib/languages/irpf90')).default); + case 'java': + return hljs.registerLanguage('java', (await import('highlight.js/lib/languages/java')).default); + case 'javascript': + return hljs.registerLanguage('javascript', (await import('highlight.js/lib/languages/javascript')).default); + case 'jboss-cli': + return hljs.registerLanguage('jboss-cli', (await import('highlight.js/lib/languages/jboss-cli')).default); + case 'json': + return hljs.registerLanguage('json', (await import('highlight.js/lib/languages/json')).default); + case 'julia': + return hljs.registerLanguage('julia', (await import('highlight.js/lib/languages/julia')).default); + case 'julia-repl': + return hljs.registerLanguage('julia-repl', (await import('highlight.js/lib/languages/julia-repl')).default); + case 'kotlin': + return hljs.registerLanguage('kotlin', (await import('highlight.js/lib/languages/kotlin')).default); + case 'lasso': + return hljs.registerLanguage('lasso', (await import('highlight.js/lib/languages/lasso')).default); + case 'ldif': + return hljs.registerLanguage('ldif', (await import('highlight.js/lib/languages/ldif')).default); + case 'leaf': + return hljs.registerLanguage('leaf', (await import('highlight.js/lib/languages/leaf')).default); + case 'less': + return hljs.registerLanguage('less', (await import('highlight.js/lib/languages/less')).default); + case 'lisp': + return hljs.registerLanguage('lisp', (await import('highlight.js/lib/languages/lisp')).default); + case 'livecodeserver': + return hljs.registerLanguage('livecodeserver', (await import('highlight.js/lib/languages/livecodeserver')).default); + case 'livescript': + return hljs.registerLanguage('livescript', (await import('highlight.js/lib/languages/livescript')).default); + case 'llvm': + return hljs.registerLanguage('llvm', (await import('highlight.js/lib/languages/llvm')).default); + case 'lsl': + return hljs.registerLanguage('lsl', (await import('highlight.js/lib/languages/lsl')).default); + case 'lua': + return hljs.registerLanguage('lua', (await import('highlight.js/lib/languages/lua')).default); + case 'makefile': + return hljs.registerLanguage('makefile', (await import('highlight.js/lib/languages/makefile')).default); + case 'markdown': + return hljs.registerLanguage('markdown', (await import('highlight.js/lib/languages/markdown')).default); + case 'mathematica': + return hljs.registerLanguage('mathematica', (await import('highlight.js/lib/languages/mathematica')).default); + case 'matlab': + return hljs.registerLanguage('matlab', (await import('highlight.js/lib/languages/matlab')).default); + case 'maxima': + return hljs.registerLanguage('maxima', (await import('highlight.js/lib/languages/maxima')).default); + case 'mel': + return hljs.registerLanguage('mel', (await import('highlight.js/lib/languages/mel')).default); + case 'mercury': + return hljs.registerLanguage('mercury', (await import('highlight.js/lib/languages/mercury')).default); + case 'mipsasm': + return hljs.registerLanguage('mipsasm', (await import('highlight.js/lib/languages/mipsasm')).default); + case 'mizar': + return hljs.registerLanguage('mizar', (await import('highlight.js/lib/languages/mizar')).default); + case 'perl': + return hljs.registerLanguage('perl', (await import('highlight.js/lib/languages/perl')).default); + case 'mojolicious': + return hljs.registerLanguage('mojolicious', (await import('highlight.js/lib/languages/mojolicious')).default); + case 'monkey': + return hljs.registerLanguage('monkey', (await import('highlight.js/lib/languages/monkey')).default); + case 'moonscript': + return hljs.registerLanguage('moonscript', (await import('highlight.js/lib/languages/moonscript')).default); + case 'n1ql': + return hljs.registerLanguage('n1ql', (await import('highlight.js/lib/languages/n1ql')).default); + case 'nginx': + return hljs.registerLanguage('nginx', (await import('highlight.js/lib/languages/nginx')).default); + case 'nimrod': + return hljs.registerLanguage('nimrod', (await import('highlight.js/lib/languages/nimrod')).default); + case 'nix': + return hljs.registerLanguage('nix', (await import('highlight.js/lib/languages/nix')).default); + case 'nsis': + return hljs.registerLanguage('nsis', (await import('highlight.js/lib/languages/nsis')).default); + case 'objectivec': + return hljs.registerLanguage('objectivec', (await import('highlight.js/lib/languages/objectivec')).default); + case 'ocaml': + return hljs.registerLanguage('ocaml', (await import('highlight.js/lib/languages/ocaml')).default); + case 'openscad': + return hljs.registerLanguage('openscad', (await import('highlight.js/lib/languages/openscad')).default); + case 'oxygene': + return hljs.registerLanguage('oxygene', (await import('highlight.js/lib/languages/oxygene')).default); + case 'parser3': + return hljs.registerLanguage('parser3', (await import('highlight.js/lib/languages/parser3')).default); + case 'pf': + return hljs.registerLanguage('pf', (await import('highlight.js/lib/languages/pf')).default); + case 'php': + return hljs.registerLanguage('php', (await import('highlight.js/lib/languages/php')).default); + case 'pony': + return hljs.registerLanguage('pony', (await import('highlight.js/lib/languages/pony')).default); + case 'powershell': + return hljs.registerLanguage('powershell', (await import('highlight.js/lib/languages/powershell')).default); + case 'processing': + return hljs.registerLanguage('processing', (await import('highlight.js/lib/languages/processing')).default); + case 'profile': + return hljs.registerLanguage('profile', (await import('highlight.js/lib/languages/profile')).default); + case 'prolog': + return hljs.registerLanguage('prolog', (await import('highlight.js/lib/languages/prolog')).default); + case 'protobuf': + return hljs.registerLanguage('protobuf', (await import('highlight.js/lib/languages/protobuf')).default); + case 'puppet': + return hljs.registerLanguage('puppet', (await import('highlight.js/lib/languages/puppet')).default); + case 'purebasic': + return hljs.registerLanguage('purebasic', (await import('highlight.js/lib/languages/purebasic')).default); + case 'python': + return hljs.registerLanguage('python', (await import('highlight.js/lib/languages/python')).default); + case 'q': + return hljs.registerLanguage('q', (await import('highlight.js/lib/languages/q')).default); + case 'qml': + return hljs.registerLanguage('qml', (await import('highlight.js/lib/languages/qml')).default); + case 'r': + return hljs.registerLanguage('r', (await import('highlight.js/lib/languages/r')).default); + case 'rib': + return hljs.registerLanguage('rib', (await import('highlight.js/lib/languages/rib')).default); + case 'roboconf': + return hljs.registerLanguage('roboconf', (await import('highlight.js/lib/languages/roboconf')).default); + case 'rsl': + return hljs.registerLanguage('rsl', (await import('highlight.js/lib/languages/rsl')).default); + case 'ruleslanguage': + return hljs.registerLanguage('ruleslanguage', (await import('highlight.js/lib/languages/ruleslanguage')).default); + case 'rust': + return hljs.registerLanguage('rust', (await import('highlight.js/lib/languages/rust')).default); + case 'scala': + return hljs.registerLanguage('scala', (await import('highlight.js/lib/languages/scala')).default); + case 'scheme': + return hljs.registerLanguage('scheme', (await import('highlight.js/lib/languages/scheme')).default); + case 'scilab': + return hljs.registerLanguage('scilab', (await import('highlight.js/lib/languages/scilab')).default); + case 'scss': + return hljs.registerLanguage('scss', (await import('highlight.js/lib/languages/scss')).default); + case 'shell': + return hljs.registerLanguage('shell', (await import('highlight.js/lib/languages/shell')).default); + case 'smali': + return hljs.registerLanguage('smali', (await import('highlight.js/lib/languages/smali')).default); + case 'smalltalk': + return hljs.registerLanguage('smalltalk', (await import('highlight.js/lib/languages/smalltalk')).default); + case 'sml': + return hljs.registerLanguage('sml', (await import('highlight.js/lib/languages/sml')).default); + case 'sqf': + return hljs.registerLanguage('sqf', (await import('highlight.js/lib/languages/sqf')).default); + case 'sql': + return hljs.registerLanguage('sql', (await import('highlight.js/lib/languages/sql')).default); + case 'stan': + return hljs.registerLanguage('stan', (await import('highlight.js/lib/languages/stan')).default); + case 'stata': + return hljs.registerLanguage('stata', (await import('highlight.js/lib/languages/stata')).default); + case 'step21': + return hljs.registerLanguage('step21', (await import('highlight.js/lib/languages/step21')).default); + case 'stylus': + return hljs.registerLanguage('stylus', (await import('highlight.js/lib/languages/stylus')).default); + case 'subunit': + return hljs.registerLanguage('subunit', (await import('highlight.js/lib/languages/subunit')).default); + case 'swift': + return hljs.registerLanguage('swift', (await import('highlight.js/lib/languages/swift')).default); + case 'taggerscript': + return hljs.registerLanguage('taggerscript', (await import('highlight.js/lib/languages/taggerscript')).default); + case 'yaml': + return hljs.registerLanguage('yaml', (await import('highlight.js/lib/languages/yaml')).default); + case 'tap': + return hljs.registerLanguage('tap', (await import('highlight.js/lib/languages/tap')).default); + case 'tcl': + return hljs.registerLanguage('tcl', (await import('highlight.js/lib/languages/tcl')).default); + case 'tex': + return hljs.registerLanguage('tex', (await import('highlight.js/lib/languages/tex')).default); + case 'thrift': + return hljs.registerLanguage('thrift', (await import('highlight.js/lib/languages/thrift')).default); + case 'tp': + return hljs.registerLanguage('tp', (await import('highlight.js/lib/languages/tp')).default); + case 'twig': + return hljs.registerLanguage('twig', (await import('highlight.js/lib/languages/twig')).default); + case 'typescript': + return hljs.registerLanguage('typescript', (await import('highlight.js/lib/languages/typescript')).default); + case 'vala': + return hljs.registerLanguage('vala', (await import('highlight.js/lib/languages/vala')).default); + case 'vbnet': + return hljs.registerLanguage('vbnet', (await import('highlight.js/lib/languages/vbnet')).default); + case 'vbscript': + return hljs.registerLanguage('vbscript', (await import('highlight.js/lib/languages/vbscript')).default); + case 'vbscript-html': + return hljs.registerLanguage('vbscript-html(', (await import('highlight.js/lib/languages/vbscript-html')).default); + case 'verilog': + return hljs.registerLanguage('verilog', (await import('highlight.js/lib/languages/verilog')).default); + case 'vhdl': + return hljs.registerLanguage('vhdl', (await import('highlight.js/lib/languages/vhdl')).default); + case 'vim': + return hljs.registerLanguage('vim', (await import('highlight.js/lib/languages/vim')).default); + case 'x86asm': + return hljs.registerLanguage('x86asm', (await import('highlight.js/lib/languages/x86asm')).default); + case 'xl': + return hljs.registerLanguage('xl', (await import('highlight.js/lib/languages/xl')).default); + case 'xquery': + return hljs.registerLanguage('xquery', (await import('highlight.js/lib/languages/xquery')).default); + case 'zephir': + return hljs.registerLanguage('zephir', (await import('highlight.js/lib/languages/zephir')).default); + default: + return hljs.registerLanguage('plaintext', (await import('highlight.js/lib/languages/plaintext')).default); + } +}; export default hljs; diff --git a/app/markdown/lib/parser/marked/marked.js b/app/markdown/lib/parser/marked/marked.js index 4d349a4bb1426..d7ca58fa1c844 100644 --- a/app/markdown/lib/parser/marked/marked.js +++ b/app/markdown/lib/parser/marked/marked.js @@ -4,7 +4,7 @@ import _marked from 'marked'; import createDOMPurify from 'dompurify'; import { unescapeHTML, escapeHTML } from '@rocket.chat/string-helpers'; -import hljs from '../../hljs'; +import hljs, { register } from '../../hljs'; import { getGlobalWindow } from '../../getGlobalWindow'; const renderer = new _marked.Renderer(); @@ -72,6 +72,7 @@ const highlight = function(code, lang) { return code; } try { + register(lang); return hljs.highlight(lang, code).value; } catch (e) { // Unknown language diff --git a/app/markdown/lib/parser/original/code.js b/app/markdown/lib/parser/original/code.js index 5b84b0d45c669..87532beb0d3de 100644 --- a/app/markdown/lib/parser/original/code.js +++ b/app/markdown/lib/parser/original/code.js @@ -4,7 +4,7 @@ */ import { unescapeHTML } from '@rocket.chat/string-helpers'; -import hljs from '../../hljs'; +import hljs, { register } from '../../hljs'; import { addAsToken } from './token'; const inlinecode = (message) => { @@ -40,7 +40,17 @@ const codeblocks = (message) => { const emptyLanguage = lang === '' ? unescapeHTML(codeMatch[1] + codeMatch[2]) : unescapeHTML(codeMatch[2]); const code = singleLine ? unescapeHTML(codeMatch[1]) : emptyLanguage; - const result = lang === '' ? hljs.highlightAuto(lang + code) : hljs.highlight(lang, code); + const result = (() => { + if (lang) { + try { + register(lang); + return hljs.highlight(lang, code); + } catch (error) { + console.error(error); + } + } + return hljs.highlightAuto(lang + code); + })(); const token = addAsToken( message, `
\`\`\`
${ result.value }
\`\`\`
`, diff --git a/app/markdown/server/settings.js b/app/markdown/server/settings.js deleted file mode 100644 index 71f3dc02c7f6a..0000000000000 --- a/app/markdown/server/settings.js +++ /dev/null @@ -1,89 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(() => { - settings.add('Markdown_Parser', 'original', { - type: 'select', - values: [{ - key: 'disabled', - i18nLabel: 'Disabled', - }, { - key: 'original', - i18nLabel: 'Original', - }, { - key: 'marked', - i18nLabel: 'Marked', - }], - group: 'Message', - section: 'Markdown', - public: true, - }); - - const enableQueryOriginal = { _id: 'Markdown_Parser', value: 'original' }; - settings.add('Markdown_Headers', false, { - type: 'boolean', - group: 'Message', - section: 'Markdown', - public: true, - enableQuery: enableQueryOriginal, - }); - settings.add('Markdown_SupportSchemesForLink', 'http,https', { - type: 'string', - group: 'Message', - section: 'Markdown', - public: true, - i18nDescription: 'Markdown_SupportSchemesForLink_Description', - enableQuery: enableQueryOriginal, - }); - - const enableQueryMarked = { _id: 'Markdown_Parser', value: 'marked' }; - settings.add('Markdown_Marked_GFM', true, { - type: 'boolean', - group: 'Message', - section: 'Markdown', - public: true, - enableQuery: enableQueryMarked, - }); - settings.add('Markdown_Marked_Tables', true, { - type: 'boolean', - group: 'Message', - section: 'Markdown', - public: true, - enableQuery: enableQueryMarked, - }); - settings.add('Markdown_Marked_Breaks', true, { - type: 'boolean', - group: 'Message', - section: 'Markdown', - public: true, - enableQuery: enableQueryMarked, - }); - settings.add('Markdown_Marked_Pedantic', false, { - type: 'boolean', - group: 'Message', - section: 'Markdown', - public: true, - enableQuery: [{ - _id: 'Markdown_Parser', - value: 'marked', - }, { - _id: 'Markdown_Marked_GFM', - value: false, - }], - }); - settings.add('Markdown_Marked_SmartLists', true, { - type: 'boolean', - group: 'Message', - section: 'Markdown', - public: true, - enableQuery: enableQueryMarked, - }); - settings.add('Markdown_Marked_Smartypants', true, { - type: 'boolean', - group: 'Message', - section: 'Markdown', - public: true, - enableQuery: enableQueryMarked, - }); -}); diff --git a/app/markdown/server/settings.ts b/app/markdown/server/settings.ts new file mode 100644 index 0000000000000..6a1b5262d4118 --- /dev/null +++ b/app/markdown/server/settings.ts @@ -0,0 +1,85 @@ +import { settingsRegistry } from '../../settings/server'; + +settingsRegistry.add('Markdown_Parser', 'original', { + type: 'select', + values: [{ + key: 'disabled', + i18nLabel: 'Disabled', + }, { + key: 'original', + i18nLabel: 'Original', + }, { + key: 'marked', + i18nLabel: 'Marked', + }], + group: 'Message', + section: 'Markdown', + public: true, +}); + +const enableQueryOriginal = { _id: 'Markdown_Parser', value: 'original' }; +settingsRegistry.add('Markdown_Headers', false, { + type: 'boolean', + group: 'Message', + section: 'Markdown', + public: true, + enableQuery: enableQueryOriginal, +}); +settingsRegistry.add('Markdown_SupportSchemesForLink', 'http,https', { + type: 'string', + group: 'Message', + section: 'Markdown', + public: true, + i18nDescription: 'Markdown_SupportSchemesForLink_Description', + enableQuery: enableQueryOriginal, +}); + +const enableQueryMarked = { _id: 'Markdown_Parser', value: 'marked' }; +settingsRegistry.add('Markdown_Marked_GFM', true, { + type: 'boolean', + group: 'Message', + section: 'Markdown', + public: true, + enableQuery: enableQueryMarked, +}); +settingsRegistry.add('Markdown_Marked_Tables', true, { + type: 'boolean', + group: 'Message', + section: 'Markdown', + public: true, + enableQuery: enableQueryMarked, +}); +settingsRegistry.add('Markdown_Marked_Breaks', true, { + type: 'boolean', + group: 'Message', + section: 'Markdown', + public: true, + enableQuery: enableQueryMarked, +}); +settingsRegistry.add('Markdown_Marked_Pedantic', false, { + type: 'boolean', + group: 'Message', + section: 'Markdown', + public: true, + enableQuery: [{ + _id: 'Markdown_Parser', + value: 'marked', + }, { + _id: 'Markdown_Marked_GFM', + value: false, + }], +}); +settingsRegistry.add('Markdown_Marked_SmartLists', true, { + type: 'boolean', + group: 'Message', + section: 'Markdown', + public: true, + enableQuery: enableQueryMarked, +}); +settingsRegistry.add('Markdown_Marked_Smartypants', true, { + type: 'boolean', + group: 'Message', + section: 'Markdown', + public: true, + enableQuery: enableQueryMarked, +}); diff --git a/app/markdown/tests/client.tests.js b/app/markdown/tests/client.tests.js index 83567dff63231..8d741f696ddb9 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 5854ec14ba6ab..e90249dcf23c2 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 30bc075649840..a1a77bac30584 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/message-mark-as-unread/client/actionButton.js b/app/message-mark-as-unread/client/actionButton.js index cc0ef68a6957e..6b79e608d3167 100644 --- a/app/message-mark-as-unread/client/actionButton.js +++ b/app/message-mark-as-unread/client/actionButton.js @@ -3,9 +3,9 @@ import { FlowRouter } from 'meteor/kadira:flow-router'; import { RoomManager, MessageAction } from '../../ui-utils'; import { messageArgs } from '../../ui-utils/client/lib/messageArgs'; -import { handleError } from '../../utils'; import { ChatSubscription } from '../../models'; import { roomTypes } from '../../utils/client'; +import { handleError } from '../../../client/lib/utils/handleError'; Meteor.startup(() => { MessageAction.addButton({ diff --git a/app/message-mark-as-unread/server/logger.js b/app/message-mark-as-unread/server/logger.js index 1327ecda6e8cc..4752c07a23dc5 100644 --- a/app/message-mark-as-unread/server/logger.js +++ b/app/message-mark-as-unread/server/logger.js @@ -1,9 +1,4 @@ import { Logger } from '../../logger'; -const logger = new Logger('MessageMarkAsUnread', { - sections: { - connection: 'Connection', - events: 'Events', - }, -}); +const logger = new Logger('MessageMarkAsUnread'); export default logger; diff --git a/app/message-mark-as-unread/server/unreadMessages.js b/app/message-mark-as-unread/server/unreadMessages.js index da31e72a35af8..c10a3fc1ae5cf 100644 --- a/app/message-mark-as-unread/server/unreadMessages.js +++ b/app/message-mark-as-unread/server/unreadMessages.js @@ -48,9 +48,9 @@ Meteor.methods({ } const lastSeen = Subscriptions.findOneByRoomIdAndUserId(originalMessage.rid, userId).ls; if (firstUnreadMessage.ts >= lastSeen) { - return logger.connection.debug('Provided message is already marked as unread'); + return logger.debug('Provided message is already marked as unread'); } - logger.connection.debug(`Updating unread message of ${ originalMessage.ts } as the first unread`); + logger.debug(`Updating unread message of ${ originalMessage.ts } as the first unread`); return Subscriptions.setAsUnreadByRoomIdAndUserId(originalMessage.rid, userId, originalMessage.ts); }, }); diff --git a/app/message-pin/client/actionButton.js b/app/message-pin/client/actionButton.js index 07c0fde808d38..ead57e8ff1032 100644 --- a/app/message-pin/client/actionButton.js +++ b/app/message-pin/client/actionButton.js @@ -2,15 +2,15 @@ import { Meteor } from 'meteor/meteor'; import { Template } from 'meteor/templating'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { FlowRouter } from 'meteor/kadira:flow-router'; -import toastr from 'toastr'; import { RoomHistoryManager, MessageAction } from '../../ui-utils'; import { messageArgs } from '../../ui-utils/client/lib/messageArgs'; -import { handleError } from '../../utils'; import { settings } from '../../settings'; import { hasAtLeastOnePermission } from '../../authorization'; import { Rooms } from '../../models/client'; import { roomTypes } from '../../utils/client'; +import { handleError } from '../../../client/lib/utils/handleError'; +import { dispatchToastMessage } from '../../../client/lib/toast'; Meteor.startup(function() { MessageAction.addButton({ @@ -106,7 +106,7 @@ Meteor.startup(function() { const { msg: message } = messageArgs(this); const permalink = await MessageAction.getPermaLink(message._id); navigator.clipboard.writeText(permalink); - toastr.success(TAPi18n.__('Copied')); + dispatchToastMessage({ type: 'success', message: TAPi18n.__('Copied') }); }, condition({ subscription }) { return !!subscription; diff --git a/app/message-pin/client/pinMessage.js b/app/message-pin/client/pinMessage.js index 5843be4de181b..8d969513d5a82 100644 --- a/app/message-pin/client/pinMessage.js +++ b/app/message-pin/client/pinMessage.js @@ -1,29 +1,29 @@ import { Meteor } from 'meteor/meteor'; -import toastr from 'toastr'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { settings } from '../../settings'; import { ChatMessage, Subscriptions } from '../../models'; +import { dispatchToastMessage } from '../../../client/lib/toast'; Meteor.methods({ pinMessage(message) { if (!Meteor.userId()) { - toastr.error(TAPi18n.__('error-not-authorized')); + dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-not-authorized') }); return false; } if (!settings.get('Message_AllowPinning')) { - toastr.error(TAPi18n.__('pinning-not-allowed')); + dispatchToastMessage({ type: 'error', message: TAPi18n.__('pinning-not-allowed') }); return false; } if (Subscriptions.findOne({ rid: message.rid }) == null) { - toastr.error(TAPi18n.__('error-pinning-message')); + dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-pinning-message') }); return false; } if (typeof message._id !== 'string') { - toastr.error(TAPi18n.__('error-pinning-message')); + dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-pinning-message') }); return false; } - toastr.success(TAPi18n.__('Message_has_been_pinned')); + dispatchToastMessage({ type: 'success', message: TAPi18n.__('Message_has_been_pinned') }); return ChatMessage.update({ _id: message._id, rid: message.rid, @@ -35,22 +35,22 @@ Meteor.methods({ }, unpinMessage(message) { if (!Meteor.userId()) { - toastr.error(TAPi18n.__('error-not-authorized')); + dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-not-authorized') }); return false; } if (!settings.get('Message_AllowPinning')) { - toastr.error(TAPi18n.__('unpinning-not-allowed')); + dispatchToastMessage({ type: 'error', message: TAPi18n.__('unpinning-not-allowed') }); return false; } if (Subscriptions.findOne({ rid: message.rid }) == null) { - toastr.error(TAPi18n.__('error-unpinning-message')); + dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-unpinning-message') }); return false; } if (typeof message._id !== 'string') { - toastr.error(TAPi18n.__('error-unpinning-message')); + dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-unpinning-message') }); return false; } - toastr.success(TAPi18n.__('Message_has_been_unpinned')); + dispatchToastMessage({ type: 'success', message: TAPi18n.__('Message_has_been_unpinned') }); return ChatMessage.update({ _id: message._id, rid: message.rid, diff --git a/app/message-pin/server/pinMessage.js b/app/message-pin/server/pinMessage.js index 4543496ad2a07..545a350b7d6aa 100644 --- a/app/message-pin/server/pinMessage.js +++ b/app/message-pin/server/pinMessage.js @@ -1,11 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { settings } from '../../settings'; -import { callbacks } from '../../callbacks'; -import { isTheLastMessage } from '../../lib'; +import { settings } from '../../settings/server'; +import { callbacks } from '../../callbacks/server'; +import { isTheLastMessage } from '../../lib/server'; import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; -import { hasPermission } from '../../authorization'; +import { canAccessRoom, hasPermission } from '../../authorization/server'; import { Subscriptions, Messages, Users, Rooms } from '../../models'; const recursiveRemove = (msg, deep = 1) => { @@ -72,7 +72,11 @@ Meteor.methods({ if (settings.get('Message_KeepHistory')) { Messages.cloneAndSaveAsHistoryById(message._id, me); } - const room = Meteor.call('canAccessRoom', originalMessage.rid, Meteor.userId()); + + const room = Rooms.findOneById(originalMessage.rid); + if (!canAccessRoom(room, { _id: Meteor.userId() })) { + throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'pinMessage' }); + } originalMessage.pinned = true; originalMessage.pinnedAt = pinnedAt || Date.now; @@ -166,7 +170,12 @@ Meteor.methods({ username: me.username, }; originalMessage = callbacks.run('beforeSaveMessage', originalMessage); - const room = Meteor.call('canAccessRoom', originalMessage.rid, Meteor.userId()); + + const room = Rooms.findOneById(originalMessage.rid, { fields: { lastMessage: 1 } }); + if (!canAccessRoom(room, { _id: Meteor.userId() })) { + throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'unpinMessage' }); + } + if (isTheLastMessage(room, message)) { Rooms.setLastMessagePinned(room._id, originalMessage.pinnedBy, originalMessage.pinned); } diff --git a/app/message-pin/server/settings.js b/app/message-pin/server/settings.js deleted file mode 100644 index c16f82a183c36..0000000000000 --- a/app/message-pin/server/settings.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; -import { Permissions } from '../../models'; - -Meteor.startup(function() { - settings.add('Message_AllowPinning', true, { - type: 'boolean', - group: 'Message', - public: true, - }); - return Permissions.create('pin-message', ['owner', 'moderator', 'admin']); -}); diff --git a/app/message-pin/server/settings.ts b/app/message-pin/server/settings.ts new file mode 100644 index 0000000000000..cc8b961978950 --- /dev/null +++ b/app/message-pin/server/settings.ts @@ -0,0 +1,7 @@ +import { settingsRegistry } from '../../settings/server'; + +settingsRegistry.add('Message_AllowPinning', true, { + type: 'boolean', + group: 'Message', + public: true, +}); diff --git a/app/message-read-receipt/client/index.js b/app/message-read-receipt/client/index.js deleted file mode 100644 index 537b2f2268a12..0000000000000 --- a/app/message-read-receipt/client/index.js +++ /dev/null @@ -1 +0,0 @@ -import './views/readReceipts'; diff --git a/app/message-read-receipt/client/views/readReceipts.html b/app/message-read-receipt/client/views/readReceipts.html deleted file mode 100644 index 3a81ee9cdcdfd..0000000000000 --- a/app/message-read-receipt/client/views/readReceipts.html +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/app/message-read-receipt/client/views/readReceipts.js b/app/message-read-receipt/client/views/readReceipts.js deleted file mode 100644 index 7664d860ba138..0000000000000 --- a/app/message-read-receipt/client/views/readReceipts.js +++ /dev/null @@ -1,39 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Template } from 'meteor/templating'; -import moment from 'moment'; - -import { settings } from '../../../settings'; - -import './readReceipts.html'; - -Template.readReceipts.helpers({ - receipts() { - return Template.instance().readReceipts.get(); - }, - displayName() { - return (settings.get('UI_Use_Real_Name') && this.user.name) || this.user.username; - }, - time() { - return moment(this.ts).format('L LTS'); - }, - isLoading() { - return Template.instance().loading.get(); - }, -}); - -Template.readReceipts.onCreated(function readReceiptsOnCreated() { - this.loading = new ReactiveVar(false); - this.readReceipts = new ReactiveVar([]); -}); - -Template.readReceipts.onRendered(function readReceiptsOnRendered() { - this.loading.set(true); - Meteor.call('getReadReceipts', { messageId: this.data.messageId }, (error, result) => { - if (!error) { - this.readReceipts.set(result); - } - - this.loading.set(false); - }); -}); diff --git a/app/message-snippet/client/page/snippetPage.js b/app/message-snippet/client/page/snippetPage.js index c4bd5d0b4a69f..a65025e547c4f 100644 --- a/app/message-snippet/client/page/snippetPage.js +++ b/app/message-snippet/client/page/snippetPage.js @@ -3,10 +3,10 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; import moment from 'moment'; -import { DateFormat } from '../../../lib'; import { settings } from '../../../settings'; import { Markdown } from '../../../markdown/client'; import { APIClient } from '../../../utils/client'; +import { formatTime } from '../../../../client/lib/utils/formatTime'; Template.snippetPage.helpers({ snippet() { @@ -30,7 +30,7 @@ Template.snippetPage.helpers({ time() { const snippet = Template.instance().message.get(); if (snippet !== undefined) { - return DateFormat.formatTime(snippet.ts); + return formatTime(snippet.ts); } }, }); diff --git a/app/message-snippet/server/startup/settings.js b/app/message-snippet/server/startup/settings.js deleted file mode 100644 index 15f8c349e8e62..0000000000000 --- a/app/message-snippet/server/startup/settings.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../../settings'; -import { Permissions } from '../../../models'; - -Meteor.startup(function() { - settings.add('Message_AllowSnippeting', false, { - type: 'boolean', - public: true, - group: 'Message', - }); - Permissions.create('snippet-message', ['owner', 'moderator', 'admin']); -}); diff --git a/app/message-snippet/server/startup/settings.ts b/app/message-snippet/server/startup/settings.ts new file mode 100644 index 0000000000000..c7de88a07d018 --- /dev/null +++ b/app/message-snippet/server/startup/settings.ts @@ -0,0 +1,7 @@ +import { settingsRegistry } from '../../../settings/server'; + +settingsRegistry.add('Message_AllowSnippeting', false, { + type: 'boolean', + public: true, + group: 'Message', +}); diff --git a/app/message-star/client/actionButton.js b/app/message-star/client/actionButton.js index 092e3e3c8dc25..dfbf69883d71b 100644 --- a/app/message-star/client/actionButton.js +++ b/app/message-star/client/actionButton.js @@ -2,14 +2,14 @@ import { Meteor } from 'meteor/meteor'; import { Template } from 'meteor/templating'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { FlowRouter } from 'meteor/kadira:flow-router'; -import toastr from 'toastr'; -import { handleError } from '../../utils'; import { settings } from '../../settings'; import { RoomHistoryManager, MessageAction } from '../../ui-utils'; import { messageArgs } from '../../ui-utils/client/lib/messageArgs'; import { Rooms } from '../../models/client'; import { roomTypes } from '../../utils/client'; +import { handleError } from '../../../client/lib/utils/handleError'; +import { dispatchToastMessage } from '../../../client/lib/toast'; Meteor.startup(function() { MessageAction.addButton({ @@ -110,7 +110,7 @@ Meteor.startup(function() { const { msg: message } = messageArgs(this); const permalink = await MessageAction.getPermaLink(message._id); navigator.clipboard.writeText(permalink); - toastr.success(TAPi18n.__('Copied')); + dispatchToastMessage({ type: 'success', message: TAPi18n.__('Copied') }); }, condition({ msg, subscription, u }) { if (subscription == null) { diff --git a/app/message-star/client/starMessage.js b/app/message-star/client/starMessage.js index 67bdb49c709e2..109d9b2baf914 100644 --- a/app/message-star/client/starMessage.js +++ b/app/message-star/client/starMessage.js @@ -1,32 +1,32 @@ import { Meteor } from 'meteor/meteor'; -import toastr from 'toastr'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { settings } from '../../settings'; import { ChatMessage, Subscriptions } from '../../models'; +import { dispatchToastMessage } from '../../../client/lib/toast'; Meteor.methods({ starMessage(message) { if (!Meteor.userId()) { - toastr.error(TAPi18n.__('error-starring-message')); + dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-starring-message') }); return false; } if (Subscriptions.findOne({ rid: message.rid }) == null) { - toastr.error(TAPi18n.__('error-starring-message')); + dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-starring-message') }); return false; } if (!ChatMessage.findOneByRoomIdAndMessageId(message.rid, message._id)) { - toastr.error(TAPi18n.__('error-starring-message')); + dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-starring-message') }); return false; } if (!settings.get('Message_AllowStarring')) { - toastr.error(TAPi18n.__('error-starring-message')); + dispatchToastMessage({ type: 'error', message: TAPi18n.__('error-starring-message') }); return false; } if (message.starred) { - toastr.success(TAPi18n.__('Message_has_been_starred')); + dispatchToastMessage({ type: 'success', message: TAPi18n.__('Message_has_been_starred') }); } else { - toastr.success(TAPi18n.__('Message_has_been_unstarred')); + dispatchToastMessage({ type: 'success', message: TAPi18n.__('Message_has_been_unstarred') }); } return ChatMessage.update({ _id: message._id, diff --git a/app/message-star/server/settings.js b/app/message-star/server/settings.js deleted file mode 100644 index a951d82fc2733..0000000000000 --- a/app/message-star/server/settings.js +++ /dev/null @@ -1,11 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(function() { - return settings.add('Message_AllowStarring', true, { - type: 'boolean', - group: 'Message', - public: true, - }); -}); diff --git a/app/message-star/server/settings.ts b/app/message-star/server/settings.ts new file mode 100644 index 0000000000000..5eeb4cb4a0fd9 --- /dev/null +++ b/app/message-star/server/settings.ts @@ -0,0 +1,7 @@ +import { settingsRegistry } from '../../settings/server'; + +settingsRegistry.add('Message_AllowStarring', true, { + type: 'boolean', + group: 'Message', + public: true, +}); diff --git a/app/message-star/server/starMessage.js b/app/message-star/server/starMessage.js index 4d22d67278359..097b640f31efd 100644 --- a/app/message-star/server/starMessage.js +++ b/app/message-star/server/starMessage.js @@ -1,8 +1,9 @@ import { Meteor } from 'meteor/meteor'; -import { settings } from '../../settings'; -import { isTheLastMessage } from '../../lib'; -import { Subscriptions, Rooms, Messages } from '../../models'; +import { settings } from '../../settings/server'; +import { isTheLastMessage } from '../../lib/server'; +import { canAccessRoom } from '../../authorization/server'; +import { Subscriptions, Rooms, Messages } from '../../models/server'; Meteor.methods({ starMessage(message) { @@ -26,7 +27,12 @@ Meteor.methods({ if (!Messages.findOneByRoomIdAndMessageId(message.rid, message._id)) { return false; } - const room = Meteor.call('canAccessRoom', message.rid, Meteor.userId()); + + const room = Rooms.findOneById(message.rid, { fields: { lastMessage: 1 } }); + if (!canAccessRoom(room, { _id: Meteor.userId() })) { + throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'starMessage' }); + } + if (isTheLastMessage(room, message)) { Rooms.updateLastMessageStar(room._id, Meteor.userId(), message.starred); } diff --git a/app/meteor-accounts-saml/server/definition/IAttributeMapping.ts b/app/meteor-accounts-saml/server/definition/IAttributeMapping.ts index eda13d450d5ee..573ca7afe4b02 100644 --- a/app/meteor-accounts-saml/server/definition/IAttributeMapping.ts +++ b/app/meteor-accounts-saml/server/definition/IAttributeMapping.ts @@ -5,7 +5,6 @@ export interface IAttributeMapping { } export interface IUserDataMap { - customFields: Map; attributeList: Set; identifier: { type: string; diff --git a/app/meteor-accounts-saml/server/definition/ISAMLGlobalSettings.ts b/app/meteor-accounts-saml/server/definition/ISAMLGlobalSettings.ts index 126a300eea07d..a7fe88d19109f 100644 --- a/app/meteor-accounts-saml/server/definition/ISAMLGlobalSettings.ts +++ b/app/meteor-accounts-saml/server/definition/ISAMLGlobalSettings.ts @@ -4,8 +4,6 @@ export interface ISAMLGlobalSettings { mailOverwrite: boolean; immutableProperty: string; defaultUserRole: string; - roleAttributeName: string; - roleAttributeSync: boolean; userDataFieldMap: string; usernameNormalize: string; channelsAttributeUpdate: boolean; diff --git a/app/meteor-accounts-saml/server/definition/ISAMLUser.ts b/app/meteor-accounts-saml/server/definition/ISAMLUser.ts index 7a643463f729d..0dccac76d4252 100644 --- a/app/meteor-accounts-saml/server/definition/ISAMLUser.ts +++ b/app/meteor-accounts-saml/server/definition/ISAMLUser.ts @@ -1,8 +1,7 @@ export interface ISAMLUser { - customFields: Map; emailList: Array; fullName: string | null; - roles: Array; + roles?: Array; eppn: string | null; username?: string; diff --git a/app/meteor-accounts-saml/server/definition/IServiceProviderOptions.ts b/app/meteor-accounts-saml/server/definition/IServiceProviderOptions.ts index db045b2e6c674..acc67681fcdc6 100644 --- a/app/meteor-accounts-saml/server/definition/IServiceProviderOptions.ts +++ b/app/meteor-accounts-saml/server/definition/IServiceProviderOptions.ts @@ -9,8 +9,6 @@ export interface IServiceProviderOptions { customAuthnContext: string; authnContextComparison: string; defaultUserRole: string; - roleAttributeName: string; - roleAttributeSync: boolean; allowedClockDrift: number; signatureValidationType: string; identifierFormat: string; diff --git a/app/meteor-accounts-saml/server/lib/SAML.ts b/app/meteor-accounts-saml/server/lib/SAML.ts index ca876193ac374..a0c60ff1d3a59 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'; @@ -17,6 +18,7 @@ import { IServiceProviderOptions } from '../definition/IServiceProviderOptions'; import { ISAMLAction } from '../definition/ISAMLAction'; import { ISAMLUser } from '../definition/ISAMLUser'; import { SAMLUtils } from './Utils'; +import { SystemLogger } from '../../../../server/lib/logger/system'; const showErrorMessage = function(res: ServerResponse, err: string): void { res.writeHead(200, { @@ -54,24 +56,24 @@ 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} { - const { roleAttributeSync, generateUsername, immutableProperty, nameOverwrite, mailOverwrite, channelsAttributeUpdate } = SAMLUtils.globalSettings; + const { generateUsername, immutableProperty, nameOverwrite, mailOverwrite, channelsAttributeUpdate, defaultUserRole = 'user' } = SAMLUtils.globalSettings; let customIdentifierMatch = false; let customIdentifierAttributeName: string | null = null; @@ -102,17 +104,19 @@ export class SAML { address: email, verified: settings.get('Accounts_Verify_Email_For_External_Accounts'), })); - const globalRoles = userObject.roles; let { username } = userObject; const active = !settings.get('Accounts_ManuallyApproveNewUsers'); if (!user) { + // If we received any role from the mapping, use them - otherwise use the default role for creation. + const roles = userObject.roles?.length ? userObject.roles : SAMLUtils.ensureArray(defaultUserRole.split(',')); + const newUser: Record = { name: userObject.fullName, active, - globalRoles, + globalRoles: roles, emails, services: { saml: { @@ -169,10 +173,6 @@ export class SAML { updateData[`services.saml.${ customIdentifierAttributeName }`] = userObject.attributeList.get(customIdentifierAttributeName); } - for (const [customField, value] of userObject.customFields) { - updateData[`customFields.${ customField }`] = value; - } - // Overwrite mail if needed if (mailOverwrite === true && (customIdentifierMatch === true || immutableProperty !== 'EMail')) { updateData.emails = emails; @@ -183,8 +183,9 @@ export class SAML { updateData.name = userObject.fullName; } - if (roleAttributeSync) { - updateData.roles = globalRoles; + // When updating an user, we only update the roles if we received them from the mapping + if (userObject.roles?.length) { + updateData.roles = userObject.roles; } if (userObject.channels && channelsAttributeUpdate === true) { @@ -215,7 +216,7 @@ export class SAML { res.writeHead(200); res.write(serviceProvider.generateServiceProviderMetadata()); res.end(); - } catch (err) { + } catch (err: any) { showErrorMessage(res, err); } } @@ -240,7 +241,7 @@ export class SAML { const serviceProvider = new SAMLServiceProvider(service); serviceProvider.validateLogoutRequest(req.query.SAMLRequest, (err, result) => { if (err) { - console.error(err); + SystemLogger.error({ err }); throw new Meteor.Error('Unable to Validate Logout Request'); } @@ -293,14 +294,14 @@ export class SAML { serviceProvider.logoutResponseToUrl(response, (err, url) => { if (err) { - console.error(err); + SystemLogger.error({ err }); return redirect(); } redirect(url); }); - } catch (e) { - console.error(e); + } catch (e: any) { + SystemLogger.error(e); redirect(); } }).run(); @@ -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, @@ -471,8 +472,8 @@ export class SAML { } } } - } catch (err) { - console.error(err); + } catch (err: any) { + SystemLogger.error(err); } } } diff --git a/app/meteor-accounts-saml/server/lib/ServiceProvider.ts b/app/meteor-accounts-saml/server/lib/ServiceProvider.ts index 5ab421c7903dd..e582c1b5acefe 100644 --- a/app/meteor-accounts-saml/server/lib/ServiceProvider.ts +++ b/app/meteor-accounts-saml/server/lib/ServiceProvider.ts @@ -24,12 +24,16 @@ import { export class SAMLServiceProvider { serviceProviderOptions: IServiceProviderOptions; + syncRequestToUrl: (request: string, operation: string) => void; + constructor(serviceProviderOptions: IServiceProviderOptions) { if (!serviceProviderOptions) { throw new Error('SAMLServiceProvider instantiated without an options object'); } this.serviceProviderOptions = serviceProviderOptions; + + this.syncRequestToUrl = Meteor.wrapAsync(this.requestToUrl, this); } private signRequest(xml: string): string { @@ -151,10 +155,6 @@ export class SAMLServiceProvider { }); } - public syncRequestToUrl(request: string, operation: string): void { - return Meteor.wrapAsync(this.requestToUrl, this)(request, operation); - } - public getAuthorizeUrl(callback: (err: string | object | null, url?: string) => void): void { const request = this.generateAuthorizeRequest(); SAMLUtils.log('-----REQUEST------'); diff --git a/app/meteor-accounts-saml/server/lib/Utils.ts b/app/meteor-accounts-saml/server/lib/Utils.ts index 20671c07a4154..4b978069ea7a2 100644 --- a/app/meteor-accounts-saml/server/lib/Utils.ts +++ b/app/meteor-accounts-saml/server/lib/Utils.ts @@ -1,4 +1,5 @@ import zlib from 'zlib'; +import { EventEmitter } from 'events'; import _ from 'underscore'; @@ -7,15 +8,12 @@ import { ISAMLUser } from '../definition/ISAMLUser'; import { ISAMLGlobalSettings } from '../definition/ISAMLGlobalSettings'; import { IUserDataMap, IAttributeMapping } from '../definition/IAttributeMapping'; import { StatusCode } from './constants'; - -// @ToDo remove this ts-ignore someday -// @ts-ignore skip checking if Logger exists to avoid having to import the Logger class here (it would bring a lot of baggage with its dependencies, affecting the unit tests) -type NullableLogger = Logger | null; +import { Logger } from '../../../../server/lib/logger/Logger'; let providerList: Array = []; let debug = false; let relayState: string | null = null; -let logger: NullableLogger = null; +let logger: Logger | undefined; const globalSettings: ISAMLGlobalSettings = { generateUsername: false, @@ -23,8 +21,6 @@ const globalSettings: ISAMLGlobalSettings = { mailOverwrite: false, immutableProperty: 'EMail', defaultUserRole: 'user', - roleAttributeName: '', - roleAttributeSync: false, userDataFieldMap: '{"username":"username", "email":"email", "cn": "name"}', usernameNormalize: 'None', channelsAttributeUpdate: false, @@ -32,6 +28,8 @@ const globalSettings: ISAMLGlobalSettings = { }; export class SAMLUtils { + public static events: EventEmitter; + public static get isDebugging(): boolean { return debug; } @@ -53,8 +51,7 @@ export class SAMLUtils { } public static getServiceProviderOptions(providerName: string): IServiceProviderOptions | undefined { - this.log(providerName); - this.log(providerList); + this.log(providerName, providerList); return _.find(providerList, (providerOptions) => providerOptions.provider === providerName); } @@ -63,7 +60,7 @@ export class SAMLUtils { providerList = list; } - public static setLoggerInstance(instance: NullableLogger): void { + public static setLoggerInstance(instance: Logger): void { logger = instance; } @@ -74,7 +71,6 @@ export class SAMLUtils { globalSettings.generateUsername = Boolean(samlConfigs.generateUsername); globalSettings.nameOverwrite = Boolean(samlConfigs.nameOverwrite); globalSettings.mailOverwrite = Boolean(samlConfigs.mailOverwrite); - globalSettings.roleAttributeSync = Boolean(samlConfigs.roleAttributeSync); globalSettings.channelsAttributeUpdate = Boolean(samlConfigs.channelsAttributeUpdate); globalSettings.includePrivateChannelsInUpdate = Boolean(samlConfigs.includePrivateChannelsInUpdate); @@ -90,10 +86,6 @@ export class SAMLUtils { globalSettings.defaultUserRole = samlConfigs.defaultUserRole; } - if (samlConfigs.roleAttributeName && typeof samlConfigs.roleAttributeName === 'string') { - globalSettings.roleAttributeName = samlConfigs.roleAttributeName; - } - if (samlConfigs.userDataFieldMap && typeof samlConfigs.userDataFieldMap === 'string') { globalSettings.userDataFieldMap = samlConfigs.userDataFieldMap; } @@ -139,15 +131,15 @@ export class SAMLUtils { return newTemplate; } - public static log(...args: Array): void { + public static log(obj: any, ...args: Array): void { if (debug && logger) { - logger.info(...args); + logger.debug(obj, ...args); } } - public static error(...args: Array): void { + public static error(obj: any, ...args: Array): void { if (logger) { - logger.error(...args); + logger.error(obj, ...args); } } @@ -221,7 +213,6 @@ export class SAMLUtils { } const parsedMap: IUserDataMap = { - customFields: new Map(), attributeList: new Set(), email: { fieldName: 'email', @@ -306,12 +297,11 @@ export class SAMLUtils { if (attributeMap) { if (spFieldName === 'email' || spFieldName === 'username' || spFieldName === 'name') { parsedMap[spFieldName] = attributeMap; - } else { - parsedMap.customFields.set(spFieldName, attributeMap); } } } + if (identifier) { const defaultTypes = [ 'email', @@ -326,7 +316,6 @@ export class SAMLUtils { parsedMap.attributeList.add(identifier); } } - return parsedMap; } @@ -421,7 +410,6 @@ export class SAMLUtils { public static mapProfileToUserObject(profile: Record): ISAMLUser { const userDataMap = this.getUserDataMapping(); SAMLUtils.log('parsed userDataMap', userDataMap); - const { defaultUserRole = 'user', roleAttributeName } = this.globalSettings; if (userDataMap.identifier.type === 'custom') { if (!userDataMap.identifier.attribute) { @@ -440,7 +428,6 @@ export class SAMLUtils { } attributeList.set(attributeName, profile[attributeName]); } - const email = this.getProfileValue(profile, userDataMap.email); const profileUsername = this.getProfileValue(profile, userDataMap.username, true); const name = this.getProfileValue(profile, userDataMap.name); @@ -451,7 +438,6 @@ export class SAMLUtils { } const userObject: ISAMLUser = { - customFields: new Map(), samlLogin: { provider: this.relayState, idp: profile.issuer, @@ -460,7 +446,6 @@ export class SAMLUtils { }, emailList: this.ensureArray(email), fullName: name || profile.displayName || profile.username, - roles: this.ensureArray(defaultUserRole.split(',')), eppn: profile.eppn, attributeList, identifier: userDataMap.identifier, @@ -470,15 +455,6 @@ export class SAMLUtils { userObject.username = this.normalizeUsername(profileUsername); } - if (roleAttributeName && profile[roleAttributeName]) { - let value = profile[roleAttributeName] || ''; - if (typeof value === 'string') { - value = value.split(','); - } - - userObject.roles = this.ensureArray(value); - } - if (profile.language) { userObject.language = profile.language; } @@ -491,13 +467,10 @@ export class SAMLUtils { } } - for (const [fieldName, customField] of userDataMap.customFields) { - const value = this.getProfileValue(profile, customField); - if (value) { - userObject.customFields.set(fieldName, value); - } - } + this.events.emit('mapUser', { profile, userObject }); return userObject; } } + +SAMLUtils.events = new EventEmitter(); diff --git a/app/meteor-accounts-saml/server/lib/parsers/LogoutRequest.ts b/app/meteor-accounts-saml/server/lib/parsers/LogoutRequest.ts index 7f6d85181ef0e..67da8570c493f 100644 --- a/app/meteor-accounts-saml/server/lib/parsers/LogoutRequest.ts +++ b/app/meteor-accounts-saml/server/lib/parsers/LogoutRequest.ts @@ -38,7 +38,7 @@ export class LogoutRequestParser { return callback(null, { idpSession, nameID, id }); } catch (e) { - console.error(e); + SAMLUtils.error(e); SAMLUtils.log(`Caught error: ${ e }`); const msg = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'StatusMessage'); diff --git a/app/meteor-accounts-saml/server/lib/parsers/Response.ts b/app/meteor-accounts-saml/server/lib/parsers/Response.ts index 1a813b3f0a75f..f92ba7fedef20 100644 --- a/app/meteor-accounts-saml/server/lib/parsers/Response.ts +++ b/app/meteor-accounts-saml/server/lib/parsers/Response.ts @@ -140,7 +140,7 @@ export class ResponseParser { } } - SAMLUtils.log(`NameID: ${ JSON.stringify(profile) }`); + SAMLUtils.log({ msg: 'NameID', profile }); return callback(null, profile, false); } @@ -176,9 +176,9 @@ 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) { - console.error(err); + SAMLUtils.error(err); } const document = new xmldom.DOMParser().parseFromString(result, 'text/xml'); @@ -318,9 +318,9 @@ 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) { - console.error(err); + SAMLUtils.error(err); } subject = new xmldom.DOMParser().parseFromString(result, 'text/xml'); }); @@ -342,7 +342,7 @@ export class ResponseParser { private validateNotBeforeNotOnOrAfterAssertions(element: Element): boolean { const sysnow = new Date(); - const allowedclockdrift = this.serviceProviderOptions.allowedClockDrift; + const allowedclockdrift = this.serviceProviderOptions.allowedClockDrift || 0; const now = new Date(sysnow.getTime() + allowedclockdrift); diff --git a/app/meteor-accounts-saml/server/lib/settings.ts b/app/meteor-accounts-saml/server/lib/settings.ts index b18b509c772de..6459a243d8e64 100644 --- a/app/meteor-accounts-saml/server/lib/settings.ts +++ b/app/meteor-accounts-saml/server/lib/settings.ts @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { ServiceConfiguration } from 'meteor/service-configuration'; -import { settings } from '../../../settings/server'; -import { SettingComposedValue } from '../../../settings/lib/settings'; +import { settings, settingsRegistry } from '../../../settings/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { IServiceProviderOptions } from '../definition/IServiceProviderOptions'; import { SAMLUtils } from './Utils'; import { @@ -18,7 +18,7 @@ import { } from './constants'; export const getSamlConfigs = function(service: string): Record { - return { + const configs = { buttonLabelText: settings.get(`${ service }_button_label_text`), buttonLabelColor: settings.get(`${ service }_button_label_color`), buttonColor: settings.get(`${ service }_button_color`), @@ -35,11 +35,7 @@ export const getSamlConfigs = function(service: string): Record { mailOverwrite: settings.get(`${ service }_mail_overwrite`), issuer: settings.get(`${ service }_issuer`), logoutBehaviour: settings.get(`${ service }_logout_behaviour`), - customAuthnContext: settings.get(`${ service }_custom_authn_context`), - authnContextComparison: settings.get(`${ service }_authn_context_comparison`), defaultUserRole: settings.get(`${ service }_default_user_role`), - roleAttributeName: settings.get(`${ service }_role_attribute_name`), - roleAttributeSync: settings.get(`${ service }_role_attribute_sync`), secret: { privateKey: settings.get(`${ service }_private_key`), publicCert: settings.get(`${ service }_public_cert`), @@ -49,17 +45,22 @@ export const getSamlConfigs = function(service: string): Record { signatureValidationType: settings.get(`${ service }_signature_validation_type`), userDataFieldMap: settings.get(`${ service }_user_data_fieldmap`), allowedClockDrift: settings.get(`${ service }_allowed_clock_drift`), - identifierFormat: settings.get(`${ service }_identifier_format`), - nameIDPolicyTemplate: settings.get(`${ service }_NameId_template`), - authnContextTemplate: settings.get(`${ service }_AuthnContext_template`), - authRequestTemplate: settings.get(`${ service }_AuthRequest_template`), - logoutResponseTemplate: settings.get(`${ service }_LogoutResponse_template`), - logoutRequestTemplate: settings.get(`${ service }_LogoutRequest_template`), - metadataCertificateTemplate: settings.get(`${ service }_MetadataCertificate_template`), - metadataTemplate: settings.get(`${ service }_Metadata_template`), + customAuthnContext: defaultAuthnContext, + authnContextComparison: 'exact', + identifierFormat: defaultIdentifierFormat, + nameIDPolicyTemplate: defaultNameIDTemplate, + authnContextTemplate: defaultAuthnContextTemplate, + authRequestTemplate: defaultAuthRequestTemplate, + logoutResponseTemplate: defaultLogoutResponseTemplate, + logoutRequestTemplate: defaultLogoutRequestTemplate, + metadataCertificateTemplate: defaultMetadataCertificateTemplate, + metadataTemplate: defaultMetadataTemplate, channelsAttributeUpdate: settings.get(`${ service }_channels_update`), includePrivateChannelsInUpdate: settings.get(`${ service }_include_private_channels_update`), }; + + SAMLUtils.events.emit('loadConfigs', service, configs); + return configs; }; export const configureSamlService = function(samlConfigs: Record): IServiceProviderOptions { @@ -86,8 +87,6 @@ export const configureSamlService = function(samlConfigs: Record): customAuthnContext: samlConfigs.customAuthnContext, authnContextComparison: samlConfigs.authnContextComparison, defaultUserRole: samlConfigs.defaultUserRole, - roleAttributeName: samlConfigs.roleAttributeName, - roleAttributeSync: samlConfigs.roleAttributeSync, allowedClockDrift: parseInt(samlConfigs.allowedClockDrift) || 0, signatureValidationType: samlConfigs.signatureValidationType, identifierFormat: samlConfigs.identifierFormat, @@ -104,16 +103,16 @@ export const configureSamlService = function(samlConfigs: Record): export const loadSamlServiceProviders = function(): void { const serviceName = 'saml'; - const services = settings.get(/^(SAML_Custom_)[a-z]+$/i) as SettingComposedValue[] | undefined; + const services = settings.getByRegexp(/^(SAML_Custom_)[a-z]+$/i); if (!services) { return SAMLUtils.setServiceProvidersList([]); } - const providers = services.map((service) => { - if (service.value === true) { - const samlConfigs = getSamlConfigs(service.key); - SAMLUtils.log(service.key); + const providers = services.map(([key, value]) => { + if (value === true) { + const samlConfigs = getSamlConfigs(key); + SAMLUtils.log(key); ServiceConfiguration.configurations.upsert({ service: serviceName.toLowerCase(), }, { @@ -131,294 +130,163 @@ export const loadSamlServiceProviders = function(): void { }; export const addSamlService = function(name: string): void { - console.log(`Adding ${ name } is deprecated`); + SystemLogger.warn(`Adding ${ name } is deprecated`); }; export const addSettings = function(name: string): void { - settings.add(`SAML_Custom_${ name }`, false, { - type: 'boolean', - group: 'SAML', - i18nLabel: 'Accounts_OAuth_Custom_Enable', - }); - settings.add(`SAML_Custom_${ name }_provider`, 'provider-name', { - type: 'string', - group: 'SAML', - i18nLabel: 'SAML_Custom_Provider', - }); - settings.add(`SAML_Custom_${ name }_entry_point`, 'https://example.com/simplesaml/saml2/idp/SSOService.php', { - type: 'string', - group: 'SAML', - i18nLabel: 'SAML_Custom_Entry_point', - }); - settings.add(`SAML_Custom_${ name }_idp_slo_redirect_url`, 'https://example.com/simplesaml/saml2/idp/SingleLogoutService.php', { - type: 'string', - group: 'SAML', - i18nLabel: 'SAML_Custom_IDP_SLO_Redirect_URL', - }); - settings.add(`SAML_Custom_${ name }_issuer`, 'https://your-rocket-chat/_saml/metadata/provider-name', { - type: 'string', - group: 'SAML', - i18nLabel: 'SAML_Custom_Issuer', - }); - settings.add(`SAML_Custom_${ name }_debug`, false, { - type: 'boolean', - group: 'SAML', - i18nLabel: 'SAML_Custom_Debug', - }); - - // UI Settings - settings.add(`SAML_Custom_${ name }_button_label_text`, 'SAML', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_1_User_Interface', - i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Text', - }); - settings.add(`SAML_Custom_${ name }_button_label_color`, '#FFFFFF', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_1_User_Interface', - i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Color', - }); - settings.add(`SAML_Custom_${ name }_button_color`, '#1d74f5', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_1_User_Interface', - i18nLabel: 'Accounts_OAuth_Custom_Button_Color', - }); - - // Certificate settings - settings.add(`SAML_Custom_${ name }_cert`, '', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_2_Certificate', - i18nLabel: 'SAML_Custom_Cert', - multiline: true, - secret: true, - }); - settings.add(`SAML_Custom_${ name }_public_cert`, '', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_2_Certificate', - multiline: true, - i18nLabel: 'SAML_Custom_Public_Cert', - }); - settings.add(`SAML_Custom_${ name }_signature_validation_type`, 'All', { - type: 'select', - values: [ - { key: 'Response', i18nLabel: 'SAML_Custom_signature_validation_response' }, - { key: 'Assertion', i18nLabel: 'SAML_Custom_signature_validation_assertion' }, - { key: 'Either', i18nLabel: 'SAML_Custom_signature_validation_either' }, - { key: 'All', i18nLabel: 'SAML_Custom_signature_validation_all' }, - ], - group: 'SAML', - section: 'SAML_Section_2_Certificate', - i18nLabel: 'SAML_Custom_signature_validation_type', - i18nDescription: 'SAML_Custom_signature_validation_type_description', - }); - settings.add(`SAML_Custom_${ name }_private_key`, '', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_2_Certificate', - multiline: true, - i18nLabel: 'SAML_Custom_Private_Key', - secret: true, - }); - - // Settings to customize behavior - settings.add(`SAML_Custom_${ name }_generate_username`, false, { - type: 'boolean', - group: 'SAML', - section: 'SAML_Section_3_Behavior', - i18nLabel: 'SAML_Custom_Generate_Username', - }); - settings.add(`SAML_Custom_${ name }_username_normalize`, 'None', { - type: 'select', - values: [ - { key: 'None', i18nLabel: 'SAML_Custom_Username_Normalize_None' }, - { key: 'Lowercase', i18nLabel: 'SAML_Custom_Username_Normalize_Lowercase' }, - ], - group: 'SAML', - section: 'SAML_Section_3_Behavior', - i18nLabel: 'SAML_Custom_Username_Normalize', - }); - settings.add(`SAML_Custom_${ name }_immutable_property`, 'EMail', { - type: 'select', - values: [ - { key: 'Username', i18nLabel: 'SAML_Custom_Immutable_Property_Username' }, - { key: 'EMail', i18nLabel: 'SAML_Custom_Immutable_Property_EMail' }, - ], - group: 'SAML', - section: 'SAML_Section_3_Behavior', - i18nLabel: 'SAML_Custom_Immutable_Property', - }); - settings.add(`SAML_Custom_${ name }_name_overwrite`, false, { - type: 'boolean', - group: 'SAML', - section: 'SAML_Section_3_Behavior', - i18nLabel: 'SAML_Custom_name_overwrite', - }); - settings.add(`SAML_Custom_${ name }_mail_overwrite`, false, { - type: 'boolean', - group: 'SAML', - section: 'SAML_Section_3_Behavior', - i18nLabel: 'SAML_Custom_mail_overwrite', - }); - settings.add(`SAML_Custom_${ name }_logout_behaviour`, 'SAML', { - type: 'select', - values: [ - { key: 'SAML', i18nLabel: 'SAML_Custom_Logout_Behaviour_Terminate_SAML_Session' }, - { key: 'Local', i18nLabel: 'SAML_Custom_Logout_Behaviour_End_Only_RocketChat' }, - ], - group: 'SAML', - section: 'SAML_Section_3_Behavior', - i18nLabel: 'SAML_Custom_Logout_Behaviour', - }); - settings.add(`SAML_Custom_${ name }_channels_update`, false, { - type: 'boolean', - group: 'SAML', - section: 'SAML_Section_3_Behavior', - i18nLabel: 'SAML_Custom_channels_update', - i18nDescription: 'SAML_Custom_channels_update_description', - }); - settings.add(`SAML_Custom_${ name }_include_private_channels_update`, false, { - type: 'boolean', - group: 'SAML', - section: 'SAML_Section_3_Behavior', - i18nLabel: 'SAML_Custom_include_private_channels_update', - i18nDescription: 'SAML_Custom_include_private_channels_update_description', - }); - - // Roles Settings - settings.add(`SAML_Custom_${ name }_default_user_role`, 'user', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_4_Roles', - i18nLabel: 'SAML_Default_User_Role', - i18nDescription: 'SAML_Default_User_Role_Description', - }); - settings.add(`SAML_Custom_${ name }_role_attribute_name`, '', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_4_Roles', - i18nLabel: 'SAML_Role_Attribute_Name', - i18nDescription: 'SAML_Role_Attribute_Name_Description', - }); - settings.add(`SAML_Custom_${ name }_role_attribute_sync`, false, { - type: 'boolean', - group: 'SAML', - section: 'SAML_Section_4_Roles', - i18nLabel: 'SAML_Role_Attribute_Sync', - i18nDescription: 'SAML_Role_Attribute_Sync_Description', - }); - - - // Data Mapping Settings - settings.add(`SAML_Custom_${ name }_user_data_fieldmap`, '{"username":"username", "email":"email", "name": "cn"}', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_5_Mapping', - i18nLabel: 'SAML_Custom_user_data_fieldmap', - i18nDescription: 'SAML_Custom_user_data_fieldmap_description', - multiline: true, - }); - - // Advanced settings - settings.add(`SAML_Custom_${ name }_allowed_clock_drift`, 0, { - type: 'int', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_Allowed_Clock_Drift', - i18nDescription: 'SAML_Allowed_Clock_Drift_Description', - }); - settings.add(`SAML_Custom_${ name }_identifier_format`, defaultIdentifierFormat, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_Identifier_Format', - i18nDescription: 'SAML_Identifier_Format_Description', - }); - - settings.add(`SAML_Custom_${ name }_NameId_template`, defaultNameIDTemplate, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_NameIdPolicy_Template', - i18nDescription: 'SAML_NameIdPolicy_Template_Description', - multiline: true, - }); - - settings.add(`SAML_Custom_${ name }_custom_authn_context`, defaultAuthnContext, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_Custom_Authn_Context', - i18nDescription: 'SAML_Custom_Authn_Context_description', - }); - settings.add(`SAML_Custom_${ name }_authn_context_comparison`, 'exact', { - type: 'select', - values: [ - { key: 'better', i18nLabel: 'Better' }, - { key: 'exact', i18nLabel: 'Exact' }, - { key: 'maximum', i18nLabel: 'Maximum' }, - { key: 'minimum', i18nLabel: 'Minimum' }, - ], - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_Custom_Authn_Context_Comparison', - }); + settingsRegistry.addGroup('SAML', function() { + this.with({ + tab: 'SAML_Connection', + }, function() { + this.add(`SAML_Custom_${ name }`, false, { + type: 'boolean', + i18nLabel: 'Accounts_OAuth_Custom_Enable', + }); + this.add(`SAML_Custom_${ name }_provider`, 'provider-name', { + type: 'string', + i18nLabel: 'SAML_Custom_Provider', + }); + this.add(`SAML_Custom_${ name }_entry_point`, 'https://example.com/simplesaml/saml2/idp/SSOService.php', { + type: 'string', + i18nLabel: 'SAML_Custom_Entry_point', + }); + this.add(`SAML_Custom_${ name }_idp_slo_redirect_url`, 'https://example.com/simplesaml/saml2/idp/SingleLogoutService.php', { + type: 'string', + i18nLabel: 'SAML_Custom_IDP_SLO_Redirect_URL', + }); + this.add(`SAML_Custom_${ name }_issuer`, 'https://your-rocket-chat/_saml/metadata/provider-name', { + type: 'string', + i18nLabel: 'SAML_Custom_Issuer', + }); + this.add(`SAML_Custom_${ name }_debug`, false, { + type: 'boolean', + i18nLabel: 'SAML_Custom_Debug', + }); - settings.add(`SAML_Custom_${ name }_AuthnContext_template`, defaultAuthnContextTemplate, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_AuthnContext_Template', - i18nDescription: 'SAML_AuthnContext_Template_Description', - multiline: true, - }); + this.section('SAML_Section_2_Certificate', function() { + this.add(`SAML_Custom_${ name }_cert`, '', { + type: 'string', + i18nLabel: 'SAML_Custom_Cert', + multiline: true, + secret: true, + }); + this.add(`SAML_Custom_${ name }_public_cert`, '', { + type: 'string', + multiline: true, + i18nLabel: 'SAML_Custom_Public_Cert', + }); + this.add(`SAML_Custom_${ name }_signature_validation_type`, 'All', { + type: 'select', + values: [ + { key: 'Response', i18nLabel: 'SAML_Custom_signature_validation_response' }, + { key: 'Assertion', i18nLabel: 'SAML_Custom_signature_validation_assertion' }, + { key: 'Either', i18nLabel: 'SAML_Custom_signature_validation_either' }, + { key: 'All', i18nLabel: 'SAML_Custom_signature_validation_all' }, + ], + i18nLabel: 'SAML_Custom_signature_validation_type', + i18nDescription: 'SAML_Custom_signature_validation_type_description', + }); + this.add(`SAML_Custom_${ name }_private_key`, '', { + type: 'string', + multiline: true, + i18nLabel: 'SAML_Custom_Private_Key', + secret: true, + }); + }); + }); + this.with({ + tab: 'SAML_General', + }, function() { + this.section('SAML_Section_1_User_Interface', function() { + this.add(`SAML_Custom_${ name }_button_label_text`, 'SAML', { + type: 'string', + i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Text', + }); + this.add(`SAML_Custom_${ name }_button_label_color`, '#FFFFFF', { + type: 'string', + i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Color', + }); + this.add(`SAML_Custom_${ name }_button_color`, '#1d74f5', { + type: 'string', + i18nLabel: 'Accounts_OAuth_Custom_Button_Color', + }); + }); - settings.add(`SAML_Custom_${ name }_AuthRequest_template`, defaultAuthRequestTemplate, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_AuthnRequest_Template', - i18nDescription: 'SAML_AuthnRequest_Template_Description', - multiline: true, - }); + this.section('SAML_Section_3_Behavior', function() { + // Settings to customize behavior + this.add(`SAML_Custom_${ name }_generate_username`, false, { + type: 'boolean', + i18nLabel: 'SAML_Custom_Generate_Username', + }); + this.add(`SAML_Custom_${ name }_username_normalize`, 'None', { + type: 'select', + values: [ + { key: 'None', i18nLabel: 'SAML_Custom_Username_Normalize_None' }, + { key: 'Lowercase', i18nLabel: 'SAML_Custom_Username_Normalize_Lowercase' }, + ], + i18nLabel: 'SAML_Custom_Username_Normalize', + }); + this.add(`SAML_Custom_${ name }_immutable_property`, 'EMail', { + type: 'select', + values: [ + { key: 'Username', i18nLabel: 'SAML_Custom_Immutable_Property_Username' }, + { key: 'EMail', i18nLabel: 'SAML_Custom_Immutable_Property_EMail' }, + ], + i18nLabel: 'SAML_Custom_Immutable_Property', + }); + this.add(`SAML_Custom_${ name }_name_overwrite`, false, { + type: 'boolean', + i18nLabel: 'SAML_Custom_name_overwrite', + }); + this.add(`SAML_Custom_${ name }_mail_overwrite`, false, { + type: 'boolean', + i18nLabel: 'SAML_Custom_mail_overwrite', + }); + this.add(`SAML_Custom_${ name }_logout_behaviour`, 'SAML', { + type: 'select', + values: [ + { key: 'SAML', i18nLabel: 'SAML_Custom_Logout_Behaviour_Terminate_SAML_Session' }, + { key: 'Local', i18nLabel: 'SAML_Custom_Logout_Behaviour_End_Only_RocketChat' }, + ], + i18nLabel: 'SAML_Custom_Logout_Behaviour', + }); + this.add(`SAML_Custom_${ name }_channels_update`, false, { + type: 'boolean', + i18nLabel: 'SAML_Custom_channels_update', + i18nDescription: 'SAML_Custom_channels_update_description', + }); + this.add(`SAML_Custom_${ name }_include_private_channels_update`, false, { + type: 'boolean', + i18nLabel: 'SAML_Custom_include_private_channels_update', + i18nDescription: 'SAML_Custom_include_private_channels_update_description', + }); - settings.add(`SAML_Custom_${ name }_LogoutResponse_template`, defaultLogoutResponseTemplate, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_LogoutResponse_Template', - i18nDescription: 'SAML_LogoutResponse_Template_Description', - multiline: true, - }); + this.add(`SAML_Custom_${ name }_default_user_role`, 'user', { + type: 'string', + i18nLabel: 'SAML_Default_User_Role', + i18nDescription: 'SAML_Default_User_Role_Description', + }); - settings.add(`SAML_Custom_${ name }_LogoutRequest_template`, defaultLogoutRequestTemplate, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_LogoutRequest_Template', - i18nDescription: 'SAML_LogoutRequest_Template_Description', - multiline: true, - }); + this.add(`SAML_Custom_${ name }_allowed_clock_drift`, false, { + type: 'int', + invalidValue: 0, + i18nLabel: 'SAML_Allowed_Clock_Drift', + i18nDescription: 'SAML_Allowed_Clock_Drift_Description', + }); + }); - settings.add(`SAML_Custom_${ name }_MetadataCertificate_template`, defaultMetadataCertificateTemplate, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_MetadataCertificate_Template', - i18nDescription: 'SAML_Metadata_Certificate_Template_Description', - multiline: true, - }); + this.section('SAML_Section_5_Mapping', function() { + // Data Mapping Settings + this.add(`SAML_Custom_${ name }_user_data_fieldmap`, '{"username":"username", "email":"email", "name": "cn"}', { + type: 'string', + i18nLabel: 'SAML_Custom_user_data_fieldmap', + i18nDescription: 'SAML_Custom_user_data_fieldmap_description', + multiline: true, + }); + }); + }); - settings.add(`SAML_Custom_${ name }_Metadata_template`, defaultMetadataTemplate, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_Metadata_Template', - i18nDescription: 'SAML_Metadata_Template_Description', - multiline: true, + SAMLUtils.events.emit('addSettings', name); }); }; diff --git a/app/meteor-accounts-saml/server/listener.ts b/app/meteor-accounts-saml/server/listener.ts index 4edae1cb301a9..d4352315459b5 100644 --- a/app/meteor-accounts-saml/server/listener.ts +++ b/app/meteor-accounts-saml/server/listener.ts @@ -6,6 +6,7 @@ import { RoutePolicy } from 'meteor/routepolicy'; import bodyParser from 'body-parser'; import fiber from 'fibers'; +import { SystemLogger } from '../../../server/lib/logger/system'; import { SAML } from './lib/SAML'; import { SAMLUtils } from './lib/Utils'; import { ISAMLAction } from './definition/ISAMLAction'; @@ -54,14 +55,14 @@ const middleware = function(req: IIncomingMessage, res: ServerResponse, next: (e const service = SAMLUtils.getServiceProviderOptions(samlObject.serviceName); if (!service) { - console.error(`${ samlObject.serviceName } service provider not found`); + SystemLogger.error(`${ samlObject.serviceName } service provider not found`); throw new Error('SAML Service Provider not found.'); } SAML.processRequest(req, res, service, samlObject); } catch (err) { // @ToDo: Ideally we should send some error message to the client, but there's no way to do it on a redirect right now. - console.log(err); + SystemLogger.error(err); const url = Meteor.absoluteUrl('home'); res.writeHead(302, { diff --git a/app/meteor-accounts-saml/server/loginHandler.ts b/app/meteor-accounts-saml/server/loginHandler.ts index 84beec3091aae..edb58716d974d 100644 --- a/app/meteor-accounts-saml/server/loginHandler.ts +++ b/app/meteor-accounts-saml/server/loginHandler.ts @@ -1,8 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { SAMLUtils } from './lib/Utils'; import { SAML } from './lib/SAML'; +import { SystemLogger } from '../../../server/lib/logger/system'; const makeError = (message: string): Record => ({ type: 'saml', @@ -15,8 +17,8 @@ Accounts.registerLoginHandler('saml', function(loginRequest) { return undefined; } - const loginResult = SAML.retrieveCredential(loginRequest.credentialToken); - SAMLUtils.log(`RESULT :${ JSON.stringify(loginResult) }`); + const loginResult = Promise.await(SAML.retrieveCredential(loginRequest.credentialToken)); + SAMLUtils.log({ msg: 'RESULT', loginResult }); if (!loginResult) { return makeError('No matching login attempt found'); @@ -28,10 +30,29 @@ Accounts.registerLoginHandler('saml', function(loginRequest) { try { const userObject = SAMLUtils.mapProfileToUserObject(loginResult.profile); - - return SAML.insertOrUpdateSAMLUser(userObject); - } catch (error) { - console.error(error); - return makeError(error.toString()); + const updatedUser = SAML.insertOrUpdateSAMLUser(userObject); + SAMLUtils.events.emit('updateCustomFields', loginResult, updatedUser); + + return updatedUser; + } catch (error: any) { + SystemLogger.error(error); + + let message = error.toString(); + let errorCode = ''; + + if (error instanceof Meteor.Error) { + errorCode = (error.error || error.message) as string; + } else if (error instanceof Error) { + errorCode = error.message; + } + + if (errorCode) { + const localizedMessage = TAPi18n.__(errorCode); + if (localizedMessage && localizedMessage !== errorCode) { + message = localizedMessage; + } + } + + return makeError(message); } }); diff --git a/app/meteor-accounts-saml/server/methods/samlLogout.ts b/app/meteor-accounts-saml/server/methods/samlLogout.ts index ceedc2de0cb19..9ee693123a9a0 100644 --- a/app/meteor-accounts-saml/server/methods/samlLogout.ts +++ b/app/meteor-accounts-saml/server/methods/samlLogout.ts @@ -32,7 +32,7 @@ Meteor.methods({ } const providerConfig = getSamlServiceProviderOptions(provider); - SAMLUtils.log(`Logout request from ${ JSON.stringify(providerConfig) }`); + SAMLUtils.log({ msg: 'Logout request', providerConfig }); // This query should respect upcoming array of SAML logins const user = Users.getSAMLByIdAndSAMLProvider(Meteor.userId(), provider); if (!user || !user.services || !user.services.saml) { @@ -40,7 +40,7 @@ Meteor.methods({ } const { nameID, idpSession } = user.services.saml; - SAMLUtils.log(`NameID for user ${ Meteor.userId() } found: ${ JSON.stringify(nameID) }`); + SAMLUtils.log({ msg: `NameID for user ${ Meteor.userId() } found`, nameID }); const _saml = new SAMLServiceProvider(providerConfig); diff --git a/app/meteor-accounts-saml/server/startup.ts b/app/meteor-accounts-saml/server/startup.ts index d76cf007d418a..3417589e48d80 100644 --- a/app/meteor-accounts-saml/server/startup.ts +++ b/app/meteor-accounts-saml/server/startup.ts @@ -1,21 +1,13 @@ import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; import { settings } from '../../settings/server'; import { loadSamlServiceProviders, addSettings } from './lib/settings'; import { Logger } from '../../logger/server'; import { SAMLUtils } from './lib/Utils'; -settings.addGroup('SAML'); - -export const logger = new Logger('steffo:meteor-accounts-saml', {}); +export const logger = new Logger('steffo:meteor-accounts-saml'); SAMLUtils.setLoggerInstance(logger); - -const updateServices = _.debounce(Meteor.bindEnvironment(() => { - loadSamlServiceProviders(); -}), 2000); - - -settings.get(/^SAML_.+/, updateServices); - -Meteor.startup(() => addSettings('Default')); +Meteor.startup(() => { + addSettings('Default'); + settings.watchByRegex(/^SAML_.+/, loadSamlServiceProviders); +}); diff --git a/app/meteor-accounts-saml/tests/data.ts b/app/meteor-accounts-saml/tests/data.ts index b74f59f0ed2cd..cf366a7564c54 100644 --- a/app/meteor-accounts-saml/tests/data.ts +++ b/app/meteor-accounts-saml/tests/data.ts @@ -9,8 +9,6 @@ export const serviceProviderOptions = { customAuthnContext: 'Password', authnContextComparison: 'Whatever', defaultUserRole: 'user', - roleAttributeName: 'role', - roleAttributeSync: false, allowedClockDrift: 0, signatureValidationType: 'All', identifierFormat: 'email', diff --git a/app/meteor-accounts-saml/tests/server.tests.ts b/app/meteor-accounts-saml/tests/server.tests.ts index b806e2dd24772..2b64134e22a94 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]', () => { @@ -632,13 +626,9 @@ describe('SAML', () => { username: 'anotherUsername', email: 'singleEmail', name: 'anotherName', - customField1: 'customField1', - customField2: 'customField2', - customField3: 'customField3', }; globalSettings.userDataFieldMap = JSON.stringify(fieldMap); - globalSettings.roleAttributeName = 'roles'; SAMLUtils.updateGlobalSettings(globalSettings); SAMLUtils.relayState = '[RelayState]'; @@ -653,15 +643,8 @@ describe('SAML', () => { expect(userObject).to.have.property('emailList').that.is.an('array').that.includes('testing@server.com'); expect(userObject).to.have.property('fullName').that.is.equal('[AnotherName]'); expect(userObject).to.have.property('username').that.is.equal('[AnotherUserName]'); - expect(userObject).to.have.property('roles').that.is.an('array').with.members(['user', 'ruler', 'admin', 'king', 'president', 'governor', 'mayor']); + expect(userObject).to.not.have.property('roles'); expect(userObject).to.have.property('channels').that.is.an('array').with.members(['pets', 'pics', 'funny', 'random', 'babies']); - - const map = new Map(); - map.set('customField1', 'value1'); - map.set('customField2', 'value2'); - map.set('customField3', 'value3'); - - expect(userObject).to.have.property('customFields').that.is.a('Map').and.is.deep.equal(map); }); it('should join array values if username receives an array of values', () => { @@ -738,37 +721,6 @@ describe('SAML', () => { expect(userObject).to.have.property('username').that.is.equal('[username]'); }); - it('should load multiple roles from the roleAttributeName when it has multiple values', () => { - const multipleRoles = { - ...profile, - roles: ['role1', 'role2'], - }; - - const userObject = SAMLUtils.mapProfileToUserObject(multipleRoles); - - expect(userObject).to.be.an('object').that.have.property('roles').that.is.an('array').with.members(['role1', 'role2']); - }); - - it('should assign the default role when the roleAttributeName is missing', () => { - const { globalSettings } = SAMLUtils; - globalSettings.roleAttributeName = ''; - SAMLUtils.updateGlobalSettings(globalSettings); - - const userObject = SAMLUtils.mapProfileToUserObject(profile); - - expect(userObject).to.be.an('object').that.have.property('roles').that.is.an('array').with.members(['user']); - }); - - it('should assign the default role when the value of the role attribute is missing', () => { - const { globalSettings } = SAMLUtils; - globalSettings.roleAttributeName = 'inexistentField'; - SAMLUtils.updateGlobalSettings(globalSettings); - - const userObject = SAMLUtils.mapProfileToUserObject(profile); - - expect(userObject).to.be.an('object').that.have.property('roles').that.is.an('array').with.members(['user']); - }); - it('should run custom regexes when one is used', () => { const { globalSettings } = SAMLUtils; @@ -882,7 +834,6 @@ describe('SAML', () => { expect(userObject).to.have.property('emailList').that.is.an('array').that.includes('user-1'); }); - it('should collect the values of every attribute on the field map', () => { const { globalSettings } = SAMLUtils; @@ -901,7 +852,6 @@ describe('SAML', () => { 'otherRoles', 'language', 'channels', - 'customField1', ], }, }; @@ -925,7 +875,6 @@ describe('SAML', () => { 'otherRoles', 'language', 'channels', - 'customField1', ]); // Workaround because chai doesn't handle Maps very well @@ -1001,11 +950,9 @@ describe('SAML', () => { template: 'user-__uid__', }, email: 'email', - epa: 'eduPersonAffiliation', }; globalSettings.userDataFieldMap = JSON.stringify(fieldMap); - globalSettings.roleAttributeName = 'roles'; SAMLUtils.updateGlobalSettings(globalSettings); SAMLUtils.relayState = '[RelayState]'; @@ -1024,8 +971,6 @@ describe('SAML', () => { const map = new Map(); map.set('epa', 'group1'); - - expect(userObject).to.have.property('customFields').that.is.a('Map').and.is.deep.equal(map); }); }); }); diff --git a/app/metrics/server/lib/collectMetrics.js b/app/metrics/server/lib/collectMetrics.js index 66a24da4df911..f72437df2f047 100644 --- a/app/metrics/server/lib/collectMetrics.js +++ b/app/metrics/server/lib/collectMetrics.js @@ -8,9 +8,10 @@ import { Meteor } from 'meteor/meteor'; import { Facts } from 'meteor/facts-base'; import { Info, getOplogInfo } from '../../../utils/server'; -import { Migrations } from '../../../migrations'; -import { settings } from '../../../settings'; -import { Statistics } from '../../../models'; +import { getControl } from '../../../../server/lib/migrations'; +import { settings } from '../../../settings/server'; +import { Statistics } from '../../../models/server/raw'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { metrics } from './metrics'; import { getAppsStatistics } from '../../../statistics/server/lib/getAppsStatistics'; @@ -41,13 +42,13 @@ 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; } metrics.version.set({ version: statistics.version }, 1); - metrics.migration.set(Migrations._getControl().version); + metrics.migration.set(getControl().version); metrics.instanceCount.set(statistics.instanceCount); metrics.oplogEnabled.set({ enabled: statistics.oplogEnabled }, 1); @@ -136,7 +137,7 @@ const updatePrometheusConfig = async () => { if (!is.enabled) { if (was.enabled) { - console.log('Disabling Prometheus'); + SystemLogger.info('Disabling Prometheus'); server.close(); Meteor.clearInterval(timer); } @@ -144,7 +145,7 @@ const updatePrometheusConfig = async () => { return; } - console.log('Configuring Prometheus', is); + SystemLogger.debug({ msg: 'Configuring Prometheus', is }); if (!was.enabled) { server.listen({ @@ -174,12 +175,12 @@ const updatePrometheusConfig = async () => { gcStats()(); } } catch (error) { - console.error(error); + SystemLogger.error(error); } Object.assign(was, is); }; Meteor.startup(async () => { - settings.get(/^Prometheus_.+/, updatePrometheusConfig); + settings.watchByRegex(/^Prometheus_.+/, updatePrometheusConfig); }); diff --git a/app/migrations/index.js b/app/migrations/index.js deleted file mode 100644 index ca39cd0df4b1a..0000000000000 --- a/app/migrations/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './server/index'; diff --git a/app/migrations/server/index.js b/app/migrations/server/index.js deleted file mode 100644 index bd8a290dd2b39..0000000000000 --- a/app/migrations/server/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Migrations } from './migrations'; - -export { Migrations }; diff --git a/app/migrations/server/migrations.d.ts b/app/migrations/server/migrations.d.ts deleted file mode 100644 index 25e4b5fd58e88..0000000000000 --- a/app/migrations/server/migrations.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export declare const Migrations: { - add(migration: { - version: number; - name?: string; - up: () => void; - down?: () => void; - }): void; -}; diff --git a/app/migrations/server/migrations.js b/app/migrations/server/migrations.js deleted file mode 100644 index c7d25a94dad63..0000000000000 --- a/app/migrations/server/migrations.js +++ /dev/null @@ -1,413 +0,0 @@ -/* eslint no-use-before-define:0 */ -import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; -import { Mongo } from 'meteor/mongo'; -import { Log } from 'meteor/logging'; -import _ from 'underscore'; -import s from 'underscore.string'; -import moment from 'moment'; - -import { Info } from '../../utils'; -/* - Adds migration capabilities. Migrations are defined like: - - Migrations.add({ - up: function() {}, //*required* code to run to migrate upwards - version: 1, //*required* number to identify migration order - down: function() {}, //*optional* code to run to migrate downwards - name: 'Something' //*optional* display name for the migration - }); - - The ordering of migrations is determined by the version you set. - - To run the migrations, set the MIGRATION_VERSION environment variable to either - 'latest' or the version number you want to migrate to. Optionally, append - ',exit' if you want the migrations to exit the meteor process, e.g if you're - migrating from a script (remember to pass the --once parameter). - - e.g: - MIGRATION_VERSION="latest" mrt # ensure we'll be at the latest version and run the app - MIGRATION_VERSION="latest,exit" mrt --once # ensure we'll be at the latest version and exit - MIGRATION_VERSION="2,exit" mrt --once # migrate to version 2 and exit - MIGRATION_VERSION="2,rerun,exit" mrt --once # rerun migration script for version 2 and exit - - Note: Migrations will lock ensuring only 1 app can be migrating at once. If - a migration crashes, the control record in the migrations collection will - remain locked and at the version it was at previously, however the db could - be in an inconsistant state. -*/ - -// since we'll be at version 0 by default, we should have a migration set for it. -const DefaultMigration = { - version: 0, - up() { - // @TODO: check if collection "migrations" exist - // If exists, rename and rerun _migrateTo - }, -}; - -export const Migrations = { - _list: [DefaultMigration], - options: { - // false disables logging - log: true, - // null or a function - logger: null, - // enable/disable info log "already at latest." - logIfLatest: true, - // lock will be valid for this amount of minutes - lockExpiration: 5, - // retry interval in seconds - retryInterval: 10, - // max number of attempts to retry unlock - maxAttempts: 30, - // migrations collection name - collectionName: 'migrations', - // collectionName: "rocketchat_migrations" - }, - config(opts) { - this.options = _.extend({}, this.options, opts); - }, -}; - -Migrations._collection = new Mongo.Collection(Migrations.options.collectionName); - -/* Create a box around messages for displaying on a console.log */ -function makeABox(message, color = 'red') { - if (!_.isArray(message)) { - message = message.split('\n'); - } - const len = _(message).reduce(function(memo, msg) { - return Math.max(memo, msg.length); - }, 0) + 4; - const text = message.map((msg) => '|'[color] + s.lrpad(msg, len)[color] + '|'[color]).join('\n'); - const topLine = '+'[color] + s.pad('', len, '-')[color] + '+'[color]; - const separator = '|'[color] + s.pad('', len, '') + '|'[color]; - const bottomLine = '+'[color] + s.pad('', len, '-')[color] + '+'[color]; - return `\n${ topLine }\n${ separator }\n${ text }\n${ separator }\n${ bottomLine }\n`; -} - -/* - Logger factory function. Takes a prefix string and options object - and uses an injected `logger` if provided, else falls back to - Meteor's `Log` package. - Will send a log object to the injected logger, on the following form: - message: String - level: String (info, warn, error, debug) - tag: 'Migrations' -*/ -function createLogger(prefix) { - check(prefix, String); - - // Return noop if logging is disabled. - if (Migrations.options.log === false) { - return function() {}; - } - - return function(level, message) { - check(level, Match.OneOf('info', 'error', 'warn', 'debug')); - check(message, Match.OneOf(String, [String])); - - const logger = Migrations.options && Migrations.options.logger; - - if (logger && _.isFunction(logger)) { - logger({ - level, - message, - tag: prefix, - }); - } else { - Log[level]({ - message: `${ prefix }: ${ message }`, - }); - } - }; -} - -// collection holding the control record - -const log = createLogger('Migrations'); - -['info', 'warn', 'error', 'debug'].forEach(function(level) { - log[level] = _.partial(log, level); -}); - -// if (process.env.MIGRATE) -// Migrations.migrateTo(process.env.MIGRATE); - -// Add a new migration: -// {up: function *required -// version: Number *required -// down: function *optional -// name: String *optional -// } -Migrations.add = function(migration) { - if (typeof migration.up !== 'function') { throw new Meteor.Error('Migration must supply an up function.'); } - - if (typeof migration.version !== 'number') { throw new Meteor.Error('Migration must supply a version number.'); } - - if (migration.version <= 0) { throw new Meteor.Error('Migration version must be greater than 0'); } - - // Freeze the migration object to make it hereafter immutable - Object.freeze(migration); - - this._list.push(migration); - this._list = _.sortBy(this._list, function(m) { - return m.version; - }); -}; - -// Attempts to run the migrations using command in the form of: -// e.g 'latest', 'latest,exit', 2 -// use 'XX,rerun' to re-run the migration at that version -Migrations.migrateTo = function(command) { - if (_.isUndefined(command) || command === '' || this._list.length === 0) { throw new Error(`Cannot migrate using invalid command: ${ command }`); } - - let version; - let subcommands; - if (typeof command === 'number') { - version = command; - } else { - version = command.split(',')[0]; - subcommands = command.split(',').slice(1); - } - - const { maxAttempts, retryInterval } = Migrations.options; - let migrated; - for (let attempts = 1; attempts <= maxAttempts; attempts++) { - if (version === 'latest') { - migrated = this._migrateTo(_.last(this._list).version); - } else { - migrated = this._migrateTo(parseInt(version), subcommands.includes('rerun')); - } - if (migrated) { - break; - } else { - let willRetry; - if (attempts < maxAttempts) { - willRetry = ` Trying again in ${ retryInterval } seconds.`; - Meteor._sleepForMs(retryInterval * 1000); - } else { - willRetry = ''; - } - console.log(`Not migrating, control is locked. Attempt ${ attempts }/${ maxAttempts }.${ willRetry }`.yellow); - } - } - if (!migrated) { - const control = this._getControl(); // Side effect: upserts control document. - console.log(makeABox([ - 'ERROR! SERVER STOPPED', - '', - 'Your database migration control is locked.', - 'Please make sure you are running the latest version and try again.', - 'If the problem persists, please contact support.', - '', - `This Rocket.Chat version: ${ Info.version }`, - `Database locked at version: ${ control.version }`, - `Database target version: ${ version === 'latest' ? _.last(this._list).version : version }`, - '', - `Commit: ${ Info.commit.hash }`, - `Date: ${ Info.commit.date }`, - `Branch: ${ Info.commit.branch }`, - `Tag: ${ Info.commit.tag }`, - ])); - process.exit(1); - } - - // remember to run meteor with --once otherwise it will restart - if (subcommands.includes('exit')) { process.exit(0); } -}; - -// just returns the current version -Migrations.getVersion = function() { - return this._getControl().version; -}; - -// migrates to the specific version passed in -Migrations._migrateTo = function(version, rerun) { - const self = this; - const control = this._getControl(); // Side effect: upserts control document. - let currentVersion = control.version; - - if (lock() === false) { - // log.info('Not migrating, control is locked.'); - // Warning - return false; - } - - if (rerun) { - log.info(`Rerunning version ${ version }`); - migrate('up', this._findIndexByVersion(version)); - log.info('Finished migrating.'); - unlock(); - return true; - } - - if (currentVersion === version) { - if (this.options.logIfLatest) { - log.info(`Not migrating, already at version ${ version }`); - } - unlock(); - return true; - } - - const startIdx = this._findIndexByVersion(currentVersion); - const endIdx = this._findIndexByVersion(version); - - // log.info('startIdx:' + startIdx + ' endIdx:' + endIdx); - log.info(`Migrating from version ${ this._list[startIdx].version } -> ${ this._list[endIdx].version }`); - - // run the actual migration - function migrate(direction, idx) { - const migration = self._list[idx]; - - if (typeof migration[direction] !== 'function') { - unlock(); - throw new Meteor.Error(`Cannot migrate ${ direction } on version ${ migration.version }`); - } - - function maybeName() { - return migration.name ? ` (${ migration.name })` : ''; - } - - log.info(`Running ${ direction }() on version ${ migration.version }${ maybeName() }`); - - try { - migration[direction](migration); - } catch (e) { - console.log(makeABox([ - 'ERROR! SERVER STOPPED', - '', - 'Your database migration failed:', - e.message, - '', - 'Please make sure you are running the latest version and try again.', - 'If the problem persists, please contact support.', - '', - `This Rocket.Chat version: ${ Info.version }`, - `Database locked at version: ${ control.version }`, - `Database target version: ${ version }`, - '', - `Commit: ${ Info.commit.hash }`, - `Date: ${ Info.commit.date }`, - `Branch: ${ Info.commit.branch }`, - `Tag: ${ Info.commit.tag }`, - ])); - console.log(e.stack); - process.exit(1); - } - } - - // Returns true if lock was acquired. - function lock() { - const date = new Date(); - const dateMinusInterval = moment(date).subtract(self.options.lockExpiration, 'minutes').toDate(); - const build = Info ? Info.build.date : date; - - // This is atomic. The selector ensures only one caller at a time will see - // the unlocked control, and locking occurs in the same update's modifier. - // All other simultaneous callers will get false back from the update. - return self._collection.update({ - _id: 'control', - $or: [{ - locked: false, - }, { - lockedAt: { - $lt: dateMinusInterval, - }, - }, { - buildAt: { - $ne: build, - }, - }], - }, { - $set: { - locked: true, - lockedAt: date, - buildAt: build, - }, - }) === 1; - } - - - // Side effect: saves version. - function unlock() { - self._setControl({ - locked: false, - version: currentVersion, - }); - } - - if (currentVersion < version) { - for (let i = startIdx; i < endIdx; i++) { - migrate('up', i + 1); - currentVersion = self._list[i + 1].version; - self._setControl({ - locked: true, - version: currentVersion, - }); - } - } else { - for (let i = startIdx; i > endIdx; i--) { - migrate('down', i); - currentVersion = self._list[i - 1].version; - self._setControl({ - locked: true, - version: currentVersion, - }); - } - } - - unlock(); - log.info('Finished migrating.'); -}; - -// gets the current control record, optionally creating it if non-existant -Migrations._getControl = function() { - const control = this._collection.findOne({ - _id: 'control', - }); - - return control || this._setControl({ - version: 0, - locked: false, - }); -}; - -// sets the control record -Migrations._setControl = function(control) { - // be quite strict - check(control.version, Number); - check(control.locked, Boolean); - - this._collection.update({ - _id: 'control', - }, { - $set: { - version: control.version, - locked: control.locked, - }, - }, { - upsert: true, - }); - - return control; -}; - -// returns the migration index in _list or throws if not found -Migrations._findIndexByVersion = function(version) { - for (let i = 0; i < this._list.length; i++) { - if (this._list[i].version === version) { return i; } - } - - throw new Meteor.Error(`Can't find migration version ${ version }`); -}; - -// reset (mainly intended for tests) -Migrations._reset = function() { - this._list = [{ - version: 0, - up() {}, - }]; - this._collection.remove({}); -}; diff --git a/app/models/client/models/Roles.js b/app/models/client/models/Roles.js index ed4ed9fc71e61..a5b63179e4e9d 100644 --- a/app/models/client/models/Roles.js +++ b/app/models/client/models/Roles.js @@ -3,9 +3,7 @@ import { ReactiveVar } from 'meteor/reactive-var'; import * as Models from '..'; -const Roles = new Mongo.Collection(null); - -Object.assign(Roles, { +const Roles = Object.assign(new Mongo.Collection(null), { findUsersInRole(name, scope, options) { const role = this.findOne(name); const roleScope = (role && role.scope) || 'Users'; diff --git a/app/models/server/index.js b/app/models/server/index.js index efcd3790302ff..5507cf53f0aa9 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,49 +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, @@ -86,10 +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 c521fda8923ed..0000000000000 --- 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 eddc203fedde1..0000000000000 --- a/app/models/server/models/Avatars.js +++ /dev/null @@ -1,118 +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); - } - - // @TODO deprecated - updateFileCompleteByNameAndUserId(name, userId, url) { - if (!name) { - return; - } - - const filter = { - name, - userId, - }; - - const update = { - $set: { - complete: true, - uploading: false, - progress: 1, - url, - }, - }; - - 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 7659538e032eb..0000000000000 --- 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 b9971b9542298..0000000000000 --- 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 eb3a586da6ba1..0000000000000 --- 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 490628be33837..0000000000000 --- 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 d0cd7d7bc4cba..0000000000000 --- 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 fb70d38925cab..0000000000000 --- 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 155deed53b956..0000000000000 --- 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 188f7cdc434e4..0000000000000 --- 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 9daf20d5a1284..0000000000000 --- 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/ImportData.ts b/app/models/server/models/ImportData.ts new file mode 100644 index 0000000000000..785086d7ba70a --- /dev/null +++ b/app/models/server/models/ImportData.ts @@ -0,0 +1,88 @@ +import { Base } from './_Base'; +import { IImportUserRecord, IImportChannelRecord } from '../../../../definition/IImportRecord'; + +class ImportDataModel extends Base { + constructor() { + super('import_data'); + } + + getAllUsersForSelection(): Array { + return this.find({ + dataType: 'user', + }, { + fields: { + 'data.importIds': 1, + 'data.username': 1, + 'data.emails': 1, + 'data.deleted': 1, + 'data.type': 1, + }, + }).fetch(); + } + + getAllChannelsForSelection(): Array { + return this.find({ + dataType: 'channel', + 'data.t': { + $ne: 'd', + }, + }, { + fields: { + 'data.importIds': 1, + 'data.name': 1, + 'data.archived': 1, + 'data.t': 1, + }, + }).fetch(); + } + + checkIfDirectMessagesExists(): boolean { + return this.find({ + dataType: 'channel', + 'data.t': 'd', + }, { + fields: { + _id: 1, + }, + }).count() > 0; + } + + countMessages(): number { + return this.find({ + dataType: 'message', + }).count(); + } + + findChannelImportIdByNameOrImportId(channelIdentifier: string): string | undefined { + const channel = this.findOne({ + dataType: 'channel', + $or: [ + { + 'data.name': channelIdentifier, + }, + { + 'data.importIds': channelIdentifier, + }, + ], + }, { + fields: { + 'data.importIds': 1, + }, + }); + + return channel?.data?.importIds?.shift(); + } + + findDMForImportedUsers(...users: Array): IImportChannelRecord | undefined { + const query = { + dataType: 'channel', + 'data.users': { + $all: users, + }, + }; + + return this.findOne(query); + } +} + +export default new ImportDataModel(); diff --git a/app/models/server/models/InstanceStatus.js b/app/models/server/models/InstanceStatus.js deleted file mode 100644 index 344381e442663..0000000000000 --- 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 817deae0d789a..0000000000000 --- 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 ffbf40c1dcce4..0000000000000 --- 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 517c1780f0ba1..0000000000000 --- 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/LivechatDepartmentAgents.js b/app/models/server/models/LivechatDepartmentAgents.js index cccfc9f83e8cb..871c2ebf37c24 100644 --- a/app/models/server/models/LivechatDepartmentAgents.js +++ b/app/models/server/models/LivechatDepartmentAgents.js @@ -14,6 +14,9 @@ export class LivechatDepartmentAgents extends Base { this.tryEnsureIndex({ departmentEnabled: 1 }); this.tryEnsureIndex({ agentId: 1 }); this.tryEnsureIndex({ username: 1 }); + + const collectionObj = this.model.rawCollection(); + this.findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj); } findByDepartmentId(departmentId) { @@ -183,10 +186,7 @@ export class LivechatDepartmentAgents extends Base { }, }; - const collectionObj = this.model.rawCollection(); - const findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj); - - const bot = findAndModify(query, sort, update); + const bot = this.findAndModify(query, sort, update); if (bot && bot.value) { return { agentId: bot.value.agentId, diff --git a/app/models/server/models/LivechatInquiry.js b/app/models/server/models/LivechatInquiry.js index 55fde68f2b90e..1994e5056cef6 100644 --- a/app/models/server/models/LivechatInquiry.js +++ b/app/models/server/models/LivechatInquiry.js @@ -48,7 +48,7 @@ export class LivechatInquiry extends Base { this.update({ _id: inquiryId, }, { - $set: { status: 'taken' }, + $set: { status: 'taken', takenAt: new Date() }, $unset: { defaultAgent: 1, estimatedInactivityCloseTimeAt: 1 }, }); } @@ -71,9 +71,17 @@ export class LivechatInquiry extends Base { return this.update({ _id: inquiryId, }, { - $set: { - status: 'queued', - }, + $set: { status: 'queued', queuedAt: new Date() }, + $unset: { takenAt: 1 }, + }); + } + + queueInquiryAndRemoveDefaultAgent(inquiryId) { + return this.update({ + _id: inquiryId, + }, { + $set: { status: 'queued', queuedAt: new Date() }, + $unset: { takenAt: 1, defaultAgent: 1 }, }); } @@ -249,14 +257,6 @@ export class LivechatInquiry extends Base { }); } - unsetEstimatedInactivityCloseTime() { - return this.update({ status: 'queued' }, { - $unset: { - estimatedInactivityCloseTimeAt: 1, - }, - }, { multi: true }); - } - // This is a better solution, but update pipelines are not supported until version 4.2 of mongo // leaving this here for when the time comes /* updateEstimatedInactivityCloseTime(milisecondsToAdd) { diff --git a/app/models/server/models/LivechatRooms.js b/app/models/server/models/LivechatRooms.js index 95128aa4f81fe..d899284b46863 100644 --- a/app/models/server/models/LivechatRooms.js +++ b/app/models/server/models/LivechatRooms.js @@ -1,4 +1,3 @@ -import { Meteor } from 'meteor/meteor'; import s from 'underscore.string'; import _ from 'underscore'; @@ -21,6 +20,8 @@ export class LivechatRooms extends Base { this.tryEnsureIndex({ 'v.token': 1 }, { sparse: true }); 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) { @@ -245,9 +246,6 @@ export class LivechatRooms extends Base { } updateRoomCount = function() { - const settingsRaw = Settings.model.rawCollection(); - const findAndModify = Meteor.wrapAsync(settingsRaw.findAndModify, settingsRaw); - const query = { _id: 'Livechat_Room_Count', }; @@ -258,7 +256,7 @@ export class LivechatRooms extends Base { }, }; - const livechatCount = findAndModify(query, null, update); + const livechatCount = Settings.findAndModify(query, null, update); return livechatCount.value.value; } @@ -283,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', @@ -567,6 +576,7 @@ export class LivechatRooms extends Base { open: '$open', servedBy: '$servedBy', metrics: '$metrics', + onHold: '$onHold', }, messagesCount: { $sum: 1, @@ -582,6 +592,7 @@ export class LivechatRooms extends Base { servedBy: '$_id.servedBy', metrics: '$_id.metrics', msgs: '$messagesCount', + onHold: '$_id.onHold', }, }, ]); @@ -721,9 +732,8 @@ export class LivechatRooms extends Base { t: 'l', }; const update = { - $unset: { - servedBy: 1, - }, + $set: { queuedAt: new Date() }, + $unset: { servedBy: 1 }, }; this.update(query, update); diff --git a/app/models/server/models/LivechatVisitors.js b/app/models/server/models/LivechatVisitors.js index 5c58a85141762..9e7f6c64b3c22 100644 --- a/app/models/server/models/LivechatVisitors.js +++ b/app/models/server/models/LivechatVisitors.js @@ -1,4 +1,3 @@ -import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import s from 'underscore.string'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -124,9 +123,6 @@ export class LivechatVisitors extends Base { * @return {string} The next visitor name */ getNextVisitorUsername() { - const settingsRaw = Settings.model.rawCollection(); - const findAndModify = Meteor.wrapAsync(settingsRaw.findAndModify, settingsRaw); - const query = { _id: 'Livechat_guest_count', }; @@ -137,7 +133,7 @@ export class LivechatVisitors extends Base { }, }; - const livechatCount = findAndModify(query, null, update); + const livechatCount = Settings.findAndModify(query, null, update); return `guest-${ livechatCount.value.value + 1 }`; } diff --git a/app/models/server/models/Messages.js b/app/models/server/models/Messages.js index bc5a4b22cdb3d..3bfff331c4ef4 100644 --- a/app/models/server/models/Messages.js +++ b/app/models/server/models/Messages.js @@ -76,6 +76,22 @@ export class Messages extends Base { return this.createWithTypeRoomIdMessageAndUser('room-unarchived', roomId, '', user); } + createRoomSetReadOnlyByRoomIdAndUser(roomId, user) { + return this.createWithTypeRoomIdMessageAndUser('room-set-read-only', roomId, '', user); + } + + createRoomRemovedReadOnlyByRoomIdAndUser(roomId, user) { + return this.createWithTypeRoomIdMessageAndUser('room-removed-read-only', roomId, '', user); + } + + createRoomAllowedReactingByRoomIdAndUser(roomId, user) { + return this.createWithTypeRoomIdMessageAndUser('room-allowed-reacting', roomId, '', user); + } + + createRoomDisallowedReactingByRoomIdAndUser(roomId, user) { + return this.createWithTypeRoomIdMessageAndUser('room-disallowed-reacting', roomId, '', user); + } + unsetReactions(messageId) { return this.update({ _id: messageId }, { $unset: { reactions: 1 } }); } @@ -91,17 +107,6 @@ export class Messages extends Base { return this.update(query, update); } - setGoogleVisionData(messageId, visionData) { - const updateObj = {}; - for (const index in visionData) { - if (visionData.hasOwnProperty(index)) { - updateObj[`attachments.0.${ index }`] = visionData[index]; - } - } - - return this.update({ _id: messageId }, { $set: updateObj }); - } - createRoomSettingsChangedWithTypeRoomIdMessageAndUser(type, roomId, message, user, extraData) { return this.createWithTypeRoomIdMessageAndUser(type, roomId, message, user, extraData); } @@ -241,6 +246,17 @@ export class Messages extends Base { return this.find(query, options); } + findVisibleByIds(ids, options) { + const query = { + _id: { $in: ids }, + _hidden: { + $ne: true, + }, + }; + + return this.find(query, options); + } + findVisibleThreadByThreadId(tmid, options) { const query = { _hidden: { diff --git a/app/models/server/models/NotificationQueue.js b/app/models/server/models/NotificationQueue.js deleted file mode 100644 index 32eb7524c2c29..0000000000000 --- 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 6aedffb63ae07..0000000000000 --- 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 db4383b9cd34c..0000000000000 --- 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 009f29d37f9df..0000000000000 --- 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 d830f400669f7..0000000000000 --- 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 4d2ab019f19b7..0000000000000 --- 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 d305c071d100d..0000000000000 --- a/app/models/server/models/Roles.js +++ /dev/null @@ -1,138 +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 query = { _id }; - - const update = { - $set: { - ...name && { name }, - ...scope && { scope }, - ...description && { description }, - ...mandatory2fa && { mandatory2fa }, - }, - }; - - return this.update(query, update); - } - - 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 05751f59cae5e..86076aaa43689 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': { @@ -334,6 +362,8 @@ 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 09b17ac510676..0000000000000 --- 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 df764ccc33513..0000000000000 --- a/app/models/server/models/Sessions.js +++ /dev/null @@ -1,693 +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 }, - }], - }, - }, { - $sort: { - _id: 1, - }, - }, { - $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 }, - }, - }, { - $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', - }, - }, - }, - }, { - $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', - }, - }, - }, { - $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', - }, - }, - }, { - $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', - }, - }, - }, { - $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', - }, - }, - }, { - $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', - }, - }, - }, { - $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 ac4b22bf7780b..0000000000000 --- 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/Settings.js b/app/models/server/models/Settings.js index 83169ac126f95..a58ea9f1c3e7d 100644 --- a/app/models/server/models/Settings.js +++ b/app/models/server/models/Settings.js @@ -1,3 +1,5 @@ +import { Meteor } from 'meteor/meteor'; + import { Base } from './_Base'; export class Settings extends Base { @@ -6,6 +8,9 @@ export class Settings extends Base { this.tryEnsureIndex({ blocked: 1 }, { sparse: 1 }); this.tryEnsureIndex({ hidden: 1 }, { sparse: 1 }); + + const collectionObj = this.model.rawCollection(); + this.findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj); } // FIND @@ -242,6 +247,10 @@ export class Settings extends Base { } } } + + insert(record, ...args) { + return super.insert({ createdAt: new Date(), ...record }, ...args); + } } export default new Settings('settings', true); diff --git a/app/models/server/models/SmarshHistory.js b/app/models/server/models/SmarshHistory.js deleted file mode 100644 index 9b2b7abc50433..0000000000000 --- 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 014f8c8180d2a..0000000000000 --- 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/Subscriptions.js b/app/models/server/models/Subscriptions.js index 5aa6ec0caabef..7dac1e85acb85 100644 --- a/app/models/server/models/Subscriptions.js +++ b/app/models/server/models/Subscriptions.js @@ -25,7 +25,6 @@ export class Subscriptions extends Base { this.tryEnsureIndex({ alert: 1 }); this.tryEnsureIndex({ ts: 1 }); this.tryEnsureIndex({ ls: 1 }); - this.tryEnsureIndex({ audioNotifications: 1 }, { sparse: 1 }); this.tryEnsureIndex({ desktopNotifications: 1 }, { sparse: 1 }); this.tryEnsureIndex({ mobilePushNotifications: 1 }, { sparse: 1 }); this.tryEnsureIndex({ emailNotifications: 1 }, { sparse: 1 }); @@ -34,6 +33,9 @@ export class Subscriptions extends Base { this.tryEnsureIndex({ 'userHighlights.0': 1 }, { sparse: 1 }); this.tryEnsureIndex({ prid: 1 }); this.tryEnsureIndex({ 'u._id': 1, open: 1, department: 1 }); + + const collectionObj = this.model.rawCollection(); + this.distinct = Meteor.wrapAsync(collectionObj.distinct, collectionObj); } findByRoomIds(roomIds) { @@ -98,14 +100,12 @@ export class Subscriptions extends Base { } getAutoTranslateLanguagesByRoomAndNotUser(rid, userId) { - const subscriptionsRaw = this.model.rawCollection(); - const distinct = Meteor.wrapAsync(subscriptionsRaw.distinct, subscriptionsRaw); const query = { rid, 'u._id': { $ne: userId }, autoTranslate: true, }; - return distinct('autoTranslateLanguage', query); + return this.distinct('autoTranslateLanguage', query); } roleBaseQuery(userId, scope) { @@ -145,6 +145,20 @@ export class Subscriptions extends Base { return this.update(query, update); } + clearAudioNotificationValueById(_id) { + const query = { + _id, + }; + + const update = { + $unset: { + audioNotificationValue: 1, + }, + }; + + return this.update(query, update); + } + updateNotificationsPrefById(_id, notificationPref, notificationField, notificationPrefOrigin) { const query = { _id, @@ -236,15 +250,6 @@ export class Subscriptions extends Base { this.update(query, update); } - findAlwaysNotifyAudioUsersByRoomId(roomId) { - const query = { - rid: roomId, - audioNotifications: 'all', - }; - - return this.find(query); - } - findAlwaysNotifyDesktopUsersByRoomId(roomId) { const query = { rid: roomId, @@ -292,57 +297,6 @@ export class Subscriptions extends Base { return this.find(query, { fields: { emailNotifications: 1, u: 1 } }); } - findNotificationPreferencesByRoom(query/* { roomId: rid, desktopFilter: desktopNotifications, mobileFilter: mobilePushNotifications, emailFilter: emailNotifications }*/) { - return this._db.find(query, { - fields: { - - // fields needed for notifications - rid: 1, - t: 1, - u: 1, - name: 1, - fname: 1, - code: 1, - - // fields to define if should send a notification - ignored: 1, - audioNotifications: 1, - audioNotificationValue: 1, - desktopNotifications: 1, - mobilePushNotifications: 1, - emailNotifications: 1, - disableNotifications: 1, - muteGroupMentions: 1, - userHighlights: 1, - }, - }); - } - - findAllMessagesNotificationPreferencesByRoom(roomId) { - const query = { - rid: roomId, - 'u._id': { $exists: true }, - $or: [ - { desktopNotifications: { $in: ['all', 'mentions'] } }, - { mobilePushNotifications: { $in: ['all', 'mentions'] } }, - { emailNotifications: { $in: ['all', 'mentions'] } }, - ], - }; - - return this._db.find(query, { - fields: { - 'u._id': 1, - audioNotifications: 1, - audioNotificationValue: 1, - desktopNotifications: 1, - mobilePushNotifications: 1, - emailNotifications: 1, - disableNotifications: 1, - muteGroupMentions: 1, - }, - }); - } - resetUserE2EKey(userId) { this.update({ 'u._id': userId }, { $unset: { diff --git a/app/models/server/models/Uploads.js b/app/models/server/models/Uploads.js deleted file mode 100644 index ce56ea6d0c0f6..0000000000000 --- 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 a877188ff03bc..0000000000000 --- 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 e58b15e157266..f33198de0abe6 100644 --- a/app/models/server/models/Users.js +++ b/app/models/server/models/Users.js @@ -9,12 +9,19 @@ import Subscriptions from './Subscriptions'; import { settings } from '../../../settings/server/functions/settings'; const queryStatusAgentOnline = (extraFilters = {}) => ({ - status: { - $exists: true, - $ne: 'offline', - }, statusLivechat: 'available', roles: 'livechat-agent', + $or: [{ + status: { + $exists: true, + $ne: 'offline', + }, + roles: { + $ne: 'bot', + }, + }, { + roles: 'bot', + }], ...extraFilters, ...settings.get('Livechat_enabled_when_agent_idle') === false && { statusConnection: { $ne: 'away' } }, }); @@ -47,6 +54,9 @@ export class Users extends Base { this.tryEnsureIndex({ openBusinessHours: 1 }, { sparse: true }); this.tryEnsureIndex({ statusLivechat: 1 }, { sparse: true }); this.tryEnsureIndex({ language: 1 }, { sparse: true }); + + const collectionObj = this.model.rawCollection(); + this.findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj); } getLoginTokensByUserId(userId) { @@ -189,9 +199,6 @@ export class Users extends Base { const query = queryStatusAgentOnline(extraFilters); - const collectionObj = this.model.rawCollection(); - const findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj); - const sort = { livechatCount: 1, username: 1, @@ -203,7 +210,7 @@ export class Users extends Base { }, }; - const user = findAndModify(query, sort, update); + const user = this.findAndModify(query, sort, update); if (user && user.value) { return { agentId: user.value._id, @@ -226,9 +233,6 @@ export class Users extends Base { ...ignoreAgentId && { _id: { $ne: ignoreAgentId } }, }; - const collectionObj = this.model.rawCollection(); - const findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj); - const sort = { livechatCount: 1, username: 1, @@ -240,7 +244,7 @@ export class Users extends Base { }, }; - const user = findAndModify(query, sort, update); + const user = this.findAndModify(query, sort, update); if (user && user.value) { return { agentId: user.value._id, @@ -258,6 +262,7 @@ export class Users extends Base { const update = { $set: { statusLivechat: status, + livechatStatusSystemModified: false, }, }; @@ -357,7 +362,6 @@ export class Users extends Base { }, }; const affectedRows = this.update(query, update); - console.log('[Mailer:Unsubscribe]', _id, createdAt, new Date(parseInt(createdAt)), affectedRows); return affectedRows; } @@ -566,6 +570,17 @@ export class Users extends Base { return this.find(query, options); } + findActiveUsersInRoles(roles, scope, options) { + roles = [].concat(roles); + + const query = { + roles: { $in: roles }, + active: true, + }; + + return this.find(query, options); + } + findOneByAppId(appId, options) { const query = { appId }; @@ -879,12 +894,6 @@ export class Users extends Base { return this.find(query, options); } - findLDAPUsers(options) { - const query = { ldap: true }; - - return this.find(query, options); - } - findCrowdUsers(options) { const query = { crowd: true }; @@ -1427,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: { @@ -1464,16 +1456,6 @@ export class Users extends Base { return this.update({ _id }, update); } - removeResumeService(_id) { - const update = { - $unset: { - 'services.resume': '', - }, - }; - - return this.update({ _id }, update); - } - removeSamlServiceSession(_id) { const update = { $unset: { @@ -1616,6 +1598,14 @@ Find users to send a message by email if: return this.find(query, options); } + + updateCustomFieldsById(userId, customFields) { + return this.update(userId, { + $set: { + customFields, + }, + }); + } } export default new Users(Meteor.users, true); diff --git a/app/models/server/models/UsersSessions.js b/app/models/server/models/UsersSessions.js deleted file mode 100644 index 43aec902d3432..0000000000000 --- 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 09df0b64a3a69..0000000000000 --- 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/_Base.js b/app/models/server/models/_Base.js index c668d3761ecff..88b063ba9f89d 100644 --- a/app/models/server/models/_Base.js +++ b/app/models/server/models/_Base.js @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import objectPath from 'object-path'; import _ from 'underscore'; import { BaseDb } from './_BaseDb'; @@ -202,152 +201,4 @@ export class Base { trashFindDeleted(...args) { return this._db.trashFindDeleted(...args); } - - processQueryOptionsOnResult(result, options = {}) { - if (result === undefined || result === null) { - return undefined; - } - - if (Array.isArray(result)) { - if (options.sort) { - result = result.sort((a, b) => { - let r = 0; - for (const field in options.sort) { - if (options.sort.hasOwnProperty(field)) { - const direction = options.sort[field]; - let valueA; - let valueB; - if (field.indexOf('.') > -1) { - valueA = objectPath.get(a, field); - valueB = objectPath.get(b, field); - } else { - valueA = a[field]; - valueB = b[field]; - } - if (valueA > valueB) { - r = direction; - break; - } - if (valueA < valueB) { - r = -direction; - break; - } - } - } - return r; - }); - } - - if (typeof options.skip === 'number') { - result.splice(0, options.skip); - } - - if (typeof options.limit === 'number' && options.limit !== 0) { - result.splice(options.limit); - } - } - - if (!options.fields) { - options.fields = {}; - } - - const fieldsToRemove = []; - const fieldsToGet = []; - - for (const field in options.fields) { - if (options.fields.hasOwnProperty(field)) { - if (options.fields[field] === 0) { - fieldsToRemove.push(field); - } else if (options.fields[field] === 1) { - fieldsToGet.push(field); - } - } - } - - if (fieldsToRemove.length > 0 && fieldsToGet.length > 0) { - console.warn('Can\'t mix remove and get fields'); - fieldsToRemove.splice(0, fieldsToRemove.length); - } - - if (fieldsToGet.length > 0 && fieldsToGet.indexOf('_id') === -1) { - fieldsToGet.push('_id'); - } - - const pickFields = (obj, fields) => { - const picked = {}; - fields.forEach((field) => { - if (field.indexOf('.') !== -1) { - objectPath.set(picked, field, objectPath.get(obj, field)); - } else { - picked[field] = obj[field]; - } - }); - return picked; - }; - - if (fieldsToRemove.length > 0 || fieldsToGet.length > 0) { - if (Array.isArray(result)) { - result = result.map((record) => { - if (fieldsToRemove.length > 0) { - return _.omit(record, ...fieldsToRemove); - } - - if (fieldsToGet.length > 0) { - return pickFields(record, fieldsToGet); - } - - return null; - }); - } else { - if (fieldsToRemove.length > 0) { - return _.omit(result, ...fieldsToRemove); - } - - if (fieldsToGet.length > 0) { - return pickFields(result, fieldsToGet); - } - } - } - - return result; - } - - // dinamicTrashFindAfter(method, deletedAt, ...args) { - // const scope = { - // find: (query={}) => { - // return this.trashFindDeletedAfter(deletedAt, query, { fields: {_id: 1, _deletedAt: 1} }); - // } - // }; - - // scope.model = { - // find: scope.find - // }; - - // return this[method].apply(scope, args); - // } - - // dinamicFindAfter(method, updatedAt, ...args) { - // const scope = { - // find: (query={}, options) => { - // query._updatedAt = { - // $gt: updatedAt - // }; - - // return this.find(query, options); - // } - // }; - - // scope.model = { - // find: scope.find - // }; - - // return this[method].apply(scope, args); - // } - - // dinamicFindChangesAfter(method, updatedAt, ...args) { - // return { - // update: this.dinamicFindAfter(method, updatedAt, ...args).fetch(), - // remove: this.dinamicTrashFindAfter(method, updatedAt, ...args).fetch() - // }; - // } } diff --git a/app/models/server/models/_BaseDb.js b/app/models/server/models/_BaseDb.js index 46a285b535608..f72bb6c845577 100644 --- a/app/models/server/models/_BaseDb.js +++ b/app/models/server/models/_BaseDb.js @@ -7,6 +7,7 @@ import _ from 'underscore'; import { setUpdatedAt } from '../lib/setUpdatedAt'; import { metrics } from '../../../metrics/server/lib/metrics'; import { getOplogHandle } from './_oplogHandle'; +import { SystemLogger } from '../../../../server/lib/logger/system'; const baseName = 'rocketchat_'; @@ -20,7 +21,7 @@ try { trash._ensureIndex({ rid: 1, __collection__: 1, _deletedAt: 1 }); } catch (e) { - console.log(e); + SystemLogger.error(e); } const actions = { @@ -29,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() { @@ -96,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; } @@ -203,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 a5b794611c94d..936881eeecaae 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'; @@ -67,7 +66,10 @@ class CustomOplogHandle { this.dbName = urlParsed.defaultDatabase; } + const mongoOptions = process.env.MONGO_OPTIONS ? JSON.parse(process.env.MONGO_OPTIONS) : null; + this.client = new MongoClient(oplogUrl, { + ...mongoOptions, useUnifiedTopology: true, useNewUrlParser: true, poolSize: this.usingChangeStream ? 15 : 1, @@ -190,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 da178a390c327..dd01197abbc94 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 81e0ad335c26f..0000000000000 --- 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 0000000000000..4c2b1fa46cc6c --- /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 0000000000000..cc9c5939f3b32 --- /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 3a6a596d272c4..a414332e2fa95 100644 --- a/app/models/server/raw/Banners.ts +++ b/app/models/server/raw/Banners.ts @@ -1,4 +1,4 @@ -import { Collection, Cursor, FindOneOptions, WithoutProjection } from 'mongodb'; +import { Collection, Cursor, FindOneOptions, UpdateWriteOpResult, WithoutProjection, InsertOneWriteOpResult } from 'mongodb'; import { BannerPlatform, IBanner } from '../../../../definition/IBanner'; import { BaseRaw } from './BaseRaw'; @@ -7,13 +7,37 @@ type T = IBanner; export class BannersRaw extends BaseRaw { constructor( public readonly col: Collection, - public readonly trash?: Collection, + trash?: Collection, ) { super(col, trash); this.col.createIndexes([ { key: { platform: 1, startAt: 1, expireAt: 1 } }, ]); + + this.col.createIndexes([ + { key: { platform: 1, startAt: 1, expireAt: 1, active: 1 } }, + ]); + } + + create(doc: IBanner): Promise> { + const invalidPlatform = doc.platform?.some((platform) => !Object.values(BannerPlatform).includes(platform)); + if (invalidPlatform) { + throw new Error('Invalid platform'); + } + + if (doc.startAt > doc.expireAt) { + throw new Error('Start date cannot be later than expire date'); + } + + if (doc.expireAt < new Date()) { + throw new Error('Cannot create banner already expired'); + } + + return this.insertOne({ + active: true, + ...doc, + }); } findActiveByRoleOrId(roles: string[], platform: BannerPlatform, bannerId?: string, options?: WithoutProjection>): Cursor { @@ -24,6 +48,7 @@ export class BannersRaw extends BaseRaw { platform, startAt: { $lte: today }, expireAt: { $gte: today }, + active: { $ne: false }, $or: [ { roles: { $in: roles } }, { roles: { $exists: false } }, @@ -32,4 +57,8 @@ export class BannersRaw extends BaseRaw { return this.col.find(query, options); } + + disable(bannerId: string): Promise { + return this.col.updateOne({ _id: bannerId, active: { $ne: false } }, { $set: { active: false, inactivedAt: new Date() } }); + } } diff --git a/app/models/server/raw/BannersDismiss.ts b/app/models/server/raw/BannersDismiss.ts index bea87b6876113..6c4e69f20b969 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 602d8a62542a7..47fd8bbdbdd17 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 0000000000000..eb6db2786682f --- /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 54e96f0645129..0000000000000 --- 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 0000000000000..c46b7f4b41411 --- /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 0ffc78d4b3961..0000000000000 --- 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 0000000000000..ad1d3df1ea10b --- /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 1d8d008242fa8..53b88792392f0 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 9201d1b3a344c..89c54e079ec0f 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 80b81d41958b2..0000000000000 --- 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 0000000000000..82f5f22fc97e2 --- /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 0000000000000..d470722d28000 --- /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 0000000000000..7ac06051e4fa5 --- /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 0000000000000..c559b64137701 --- /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/ImportData.ts b/app/models/server/raw/ImportData.ts new file mode 100644 index 0000000000000..175e64380f56c --- /dev/null +++ b/app/models/server/raw/ImportData.ts @@ -0,0 +1,18 @@ +import { Cursor } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; +import { IImportRecord, IImportUserRecord, IImportMessageRecord, IImportChannelRecord } from '../../../../definition/IImportRecord'; + +export class ImportDataRaw extends BaseRaw { + getAllUsers(): Cursor { + return this.find({ dataType: 'user' }) as Cursor; + } + + getAllMessages(): Cursor { + return this.find({ dataType: 'message' }) as Cursor; + } + + getAllChannels(): Cursor { + return this.find({ dataType: 'channel' }) as Cursor; + } +} diff --git a/app/models/server/raw/IntegrationHistory.ts b/app/models/server/raw/IntegrationHistory.ts index 53f7167db7962..923c11c6257e7 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 ab8e01a5ebae0..0000000000000 --- 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 0000000000000..521507d3bfeca --- /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 0000000000000..84d21e4e3e818 --- /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.js deleted file mode 100644 index 9d48cf45a8328..0000000000000 --- a/app/models/server/raw/LivechatAgentActivity.js +++ /dev/null @@ -1,123 +0,0 @@ -import moment from 'moment'; - -import { BaseRaw } from './BaseRaw'; - -export class LivechatAgentActivityRaw extends BaseRaw { - findAllAverageAvailableServiceTime({ date, departmentId }) { - const match = { $match: { date } }; - const lookup = { - $lookup: { - from: 'rocketchat_livechat_department_agents', - localField: 'agentId', - foreignField: 'agentId', - as: 'departments', - }, - }; - const unwind = { - $unwind: { - path: '$departments', - preserveNullAndEmptyArrays: true, - }, - }; - const departmentsMatch = { - $match: { - 'departments.departmentId': departmentId, - }, - }; - const sumAvailableTimeWithCurrentTime = { - $sum: [ - { $divide: [{ $subtract: [new Date(), '$lastStartedAt'] }, 1000] }, - '$availableTime', - ], - }; - const group = { - $group: { - _id: null, - allAvailableTimeInSeconds: { - $sum: { - $cond: [{ $ifNull: ['$lastStoppedAt', false] }, - '$availableTime', - sumAvailableTimeWithCurrentTime], - }, - }, - rooms: { $sum: 1 }, - }, - }; - const project = { - $project: { - averageAvailableServiceTimeInSeconds: { - $trunc: { - $cond: [ - { $eq: ['$rooms', 0] }, - 0, - { $divide: ['$allAvailableTimeInSeconds', '$rooms'] }, - ], - }, - }, - }, - }; - const params = [match]; - if (departmentId && departmentId !== 'undefined') { - params.push(lookup); - params.push(unwind); - params.push(departmentsMatch); - } - params.push(group); - params.push(project); - return this.col.aggregate(params).toArray(); - } - - findAvailableServiceTimeHistory({ start, end, fullReport, onlyCount = false, options = {} }) { - const match = { - $match: { - date: { - $gte: parseInt(moment(start).format('YYYYMMDD')), - $lte: parseInt(moment(end).format('YYYYMMDD')), - }, - }, - }; - const lookup = { - $lookup: { - from: 'users', - localField: 'agentId', - foreignField: '_id', - as: 'user', - }, - }; - const unwind = { - $unwind: { - path: '$user', - }, - }; - const group = { - $group: { - _id: { _id: '$user._id', username: '$user.username' }, - serviceHistory: { $first: '$serviceHistory' }, - availableTimeInSeconds: { $sum: '$availableTime' }, - }, - }; - const project = { - $project: { - _id: 0, - username: '$_id.username', - availableTimeInSeconds: 1, - }, - }; - if (fullReport) { - project.$project.serviceHistory = 1; - } - const sort = { $sort: options.sort || { username: 1 } }; - const params = [match, lookup, unwind, group, project, sort]; - if (onlyCount) { - params.push({ $count: 'total' }); - return this.col.aggregate(params); - } - if (options.offset) { - params.push({ $skip: options.offset }); - } - if (options.count) { - params.push({ $limit: options.count }); - } - return this.col.aggregate(params, { allowDiskUse: true }); - } -} diff --git a/app/models/server/raw/LivechatAgentActivity.ts b/app/models/server/raw/LivechatAgentActivity.ts new file mode 100644 index 0000000000000..7531dd30c3459 --- /dev/null +++ b/app/models/server/raw/LivechatAgentActivity.ts @@ -0,0 +1,136 @@ +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 }: { date: Date; departmentId: string }): Promise { + const match = { $match: { date } }; + const lookup = { + $lookup: { + from: 'rocketchat_livechat_department_agents', + localField: 'agentId', + foreignField: 'agentId', + as: 'departments', + }, + }; + const unwind = { + $unwind: { + path: '$departments', + preserveNullAndEmptyArrays: true, + }, + }; + const departmentsMatch = { + $match: { + 'departments.departmentId': departmentId, + }, + }; + const sumAvailableTimeWithCurrentTime = { + $sum: [ + { $divide: [{ $subtract: [new Date(), '$lastStartedAt'] }, 1000] }, + '$availableTime', + ], + }; + const group = { + $group: { + _id: null, + allAvailableTimeInSeconds: { + $sum: { + $cond: [{ $ifNull: ['$lastStoppedAt', false] }, + '$availableTime', + sumAvailableTimeWithCurrentTime], + }, + }, + rooms: { $sum: 1 }, + }, + }; + const project = { + $project: { + averageAvailableServiceTimeInSeconds: { + $trunc: { + $cond: [ + { $eq: ['$rooms', 0] }, + 0, + { $divide: ['$allAvailableTimeInSeconds', '$rooms'] }, + ], + }, + }, + }, + }; + const params = [match] as object[]; + if (departmentId && departmentId !== 'undefined') { + params.push(lookup); + params.push(unwind); + params.push(departmentsMatch); + } + params.push(group); + params.push(project); + return this.col.aggregate(params).toArray(); + } + + findAvailableServiceTimeHistory({ + start, + end, + fullReport, + onlyCount = false, + options = {}, + }: { + start: string; + end: string; + fullReport: boolean; + onlyCount: boolean; + options: any; + }): AggregationCursor { + const match = { + $match: { + date: { + $gte: parseInt(moment(start).format('YYYYMMDD')), + $lte: parseInt(moment(end).format('YYYYMMDD')), + }, + }, + }; + const lookup = { + $lookup: { + from: 'users', + localField: 'agentId', + foreignField: '_id', + as: 'user', + }, + }; + const unwind = { + $unwind: { + path: '$user', + }, + }; + const group = { + $group: { + _id: { _id: '$user._id', username: '$user.username' }, + serviceHistory: { $first: '$serviceHistory' }, + availableTimeInSeconds: { $sum: '$availableTime' }, + }, + }; + const project = { + $project: { + _id: 0, + username: '$_id.username', + availableTimeInSeconds: 1, + ...fullReport && { serviceHistory: 1 }, + }, + }; + + const sort = { $sort: options.sort || { username: 1 } }; + const params = [match, lookup, unwind, group, project, sort] as object[]; + if (onlyCount) { + params.push({ $count: 'total' }); + return this.col.aggregate(params); + } + if (options.offset) { + params.push({ $skip: options.offset }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + return this.col.aggregate(params, { allowDiskUse: true }); + } +} diff --git a/app/models/server/raw/LivechatBusinessHours.ts b/app/models/server/raw/LivechatBusinessHours.ts index 7e4cb0218002c..706fff8b19e8a 100644 --- a/app/models/server/raw/LivechatBusinessHours.ts +++ b/app/models/server/raw/LivechatBusinessHours.ts @@ -2,7 +2,6 @@ import { Collection, FindOneOptions, ObjectId, WithoutProjection } from 'mongodb import { BaseRaw } from './BaseRaw'; import { - IBusinessHourWorkHour, ILivechatBusinessHour, LivechatBusinessHourTypes, } from '../../../../definition/ILivechatBusinessHour'; @@ -63,20 +62,6 @@ export class LivechatBusinessHoursRaw extends BaseRaw { }); } - // TODO: Remove this function after remove the deprecated method livechat:saveOfficeHours - async updateDayOfGlobalBusinessHour(day: Omit): Promise { - return this.col.updateOne({ - type: LivechatBusinessHourTypes.DEFAULT, - 'workHours.day': day.day, - }, { - $set: { - 'workHours.$.start': day.start, - 'workHours.$.finish': day.finish, - 'workHours.$.open': day.open, - }, - }); - } - findHoursToScheduleJobs(): Promise { return this.col.aggregate([ { diff --git a/app/models/server/raw/LivechatCustomField.js b/app/models/server/raw/LivechatCustomField.js deleted file mode 100644 index 2e3ee77e85a7b..0000000000000 --- 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 0000000000000..6ca1ca5b0e238 --- /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.js deleted file mode 100644 index 7915f57724c35..0000000000000 --- a/app/models/server/raw/LivechatDepartment.js +++ /dev/null @@ -1,81 +0,0 @@ -import { escapeRegExp } from '@rocket.chat/string-helpers'; - -import { BaseRaw } from './BaseRaw'; - -export class LivechatDepartmentRaw extends BaseRaw { - findInIds(departmentsIds, options) { - const query = { _id: { $in: departmentsIds } }; - return this.find(query, options); - } - - findByNameRegexWithExceptionsAndConditions(searchTerm, exceptions = [], conditions = {}, options = {}) { - if (!Array.isArray(exceptions)) { - exceptions = [exceptions]; - } - - const nameRegex = new RegExp(`^${ escapeRegExp(searchTerm).trim() }`, 'i'); - - const query = { - name: nameRegex, - _id: { - $nin: exceptions, - }, - ...conditions, - }; - - return this.find(query, options); - } - - findByBusinessHourId(businessHourId, options) { - const query = { businessHourId }; - return this.find(query, options); - } - - findEnabledByBusinessHourId(businessHourId, options) { - const query = { businessHourId, enabled: true }; - return this.find(query, options); - } - - addBusinessHourToDepartmentsByIds(ids = [], businessHourId) { - const query = { - _id: { $in: ids }, - }; - - const update = { - $set: { - businessHourId, - }, - }; - - return this.col.update(query, update, { multi: true }); - } - - removeBusinessHourFromDepartmentsByIdsAndBusinessHourId(ids = [], businessHourId) { - const query = { - _id: { $in: ids }, - businessHourId, - }; - - const update = { - $unset: { - businessHourId: 1, - }, - }; - - return this.col.update(query, update, { multi: true }); - } - - removeBusinessHourFromDepartmentsByBusinessHourId(businessHourId) { - const query = { - businessHourId, - }; - - const update = { - $unset: { - businessHourId: 1, - }, - }; - - return this.col.update(query, update, { multi: true }); - } -} diff --git a/app/models/server/raw/LivechatDepartment.ts b/app/models/server/raw/LivechatDepartment.ts new file mode 100644 index 0000000000000..af4da4397da9f --- /dev/null +++ b/app/models/server/raw/LivechatDepartment.ts @@ -0,0 +1,83 @@ +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: string[], options: FindOneOptions): Cursor { + const query = { _id: { $in: departmentsIds } }; + return this.find(query, options); + } + + findByNameRegexWithExceptionsAndConditions(searchTerm: string, exceptions: string[] = [], conditions: FilterQuery = {}, options: FindOneOptions = {}): Cursor { + if (!Array.isArray(exceptions)) { + exceptions = [exceptions]; + } + + const nameRegex = new RegExp(`^${ escapeRegExp(searchTerm).trim() }`, 'i'); + + const query = { + name: nameRegex, + _id: { + $nin: exceptions, + }, + ...conditions, + }; + + return this.find(query, options); + } + + findByBusinessHourId(businessHourId: string, options: FindOneOptions): Cursor { + const query = { businessHourId }; + return this.find(query, options); + } + + findEnabledByBusinessHourId(businessHourId: string, options: FindOneOptions): Cursor { + const query = { businessHourId, enabled: true }; + return this.find(query, options); + } + + addBusinessHourToDepartmentsByIds(ids: string[] = [], businessHourId: string): Promise { + const query = { + _id: { $in: ids }, + }; + + const update = { + $set: { + businessHourId, + }, + }; + + return this.col.update(query, update, { multi: true }); + } + + removeBusinessHourFromDepartmentsByIdsAndBusinessHourId(ids: string[] = [], businessHourId: string): Promise { + const query = { + _id: { $in: ids }, + businessHourId, + }; + + const update = { + $unset: { + businessHourId: 1, + }, + }; + + return this.col.update(query, update, { multi: true }); + } + + removeBusinessHourFromDepartmentsByBusinessHourId(businessHourId: string): Promise { + const query = { + businessHourId, + }; + + const update = { + $unset: { + businessHourId: 1, + }, + }; + + return this.col.update(query, update, { multi: true }); + } +} diff --git a/app/models/server/raw/LivechatInquiry.js b/app/models/server/raw/LivechatInquiry.js deleted file mode 100644 index 5a3f4971786b5..0000000000000 --- 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 0000000000000..cae073044f89a --- /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 0c07e12add2be..c73a15e3ad5c2 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 af9bfbdcce749..0000000000000 --- 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 0000000000000..71035b1db1112 --- /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.js deleted file mode 100644 index dabd7124625cc..0000000000000 --- a/app/models/server/raw/LivechatVisitors.js +++ /dev/null @@ -1,77 +0,0 @@ -import { escapeRegExp } from '@rocket.chat/string-helpers'; - -import { BaseRaw } from './BaseRaw'; - -export class LivechatVisitorsRaw extends BaseRaw { - getVisitorsBetweenDate({ start, end, department }) { - const query = { - _updatedAt: { - $gte: new Date(start), - $lt: new Date(end), - }, - ...department && department !== 'undefined' && { department }, - }; - - return this.find(query, { fields: { _id: 1 } }); - } - - findByNameRegexWithExceptionsAndConditions(searchTerm, exceptions = [], conditions = {}, options = {}) { - if (!Array.isArray(exceptions)) { - exceptions = [exceptions]; - } - - const nameRegex = new RegExp(`^${ escapeRegExp(searchTerm).trim() }`, 'i'); - - const match = { - $match: { - name: nameRegex, - _id: { - $nin: exceptions, - }, - ...conditions, - }, - }; - - const { fields, sort, offset, count } = options; - const project = { - $project: { - custom_name: { $concat: ['$username', ' - ', '$name'] }, - ...fields, - }, - }; - - const order = { $sort: sort || { name: 1 } }; - const params = [match, project, order]; - - if (offset) { - params.push({ $skip: offset }); - } - - if (count) { - params.push({ $limit: count }); - } - - return this.col.aggregate(params); - } - - /** - * Find visitors by their email or phone or username or name - * @return [{object}] List of Visitors from db - */ - findVisitorsByEmailOrPhoneOrNameOrUsername(_emailOrPhoneOrNameOrUsername, options) { - const filter = new RegExp(_emailOrPhoneOrNameOrUsername, 'i'); - const query = { - $or: [{ - 'visitorEmails.address': filter, - }, { - 'phone.phoneNumber': _emailOrPhoneOrNameOrUsername, - }, { - name: filter, - }, { - username: filter, - }], - }; - - return this.find(query, options); - } -} diff --git a/app/models/server/raw/LivechatVisitors.ts b/app/models/server/raw/LivechatVisitors.ts new file mode 100644 index 0000000000000..dd3f17349c4f4 --- /dev/null +++ b/app/models/server/raw/LivechatVisitors.ts @@ -0,0 +1,95 @@ +import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { AggregationCursor, Cursor, FilterQuery, FindOneOptions, WithoutProjection } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; +import { ILivechatVisitor } from '../../../../definition/ILivechatVisitor'; + +export class LivechatVisitorsRaw extends BaseRaw { + findOneById(_id: string, options: WithoutProjection>): Promise { + const query = { + _id, + }; + + return this.findOne(query, options); + } + + getVisitorByToken(token: string, options: WithoutProjection>): Promise { + const query = { + token, + }; + + return this.findOne(query, options); + } + + getVisitorsBetweenDate({ start, end, department }: { start: Date; end: Date; department: string }): Cursor { + const query = { + _updatedAt: { + $gte: new Date(start), + $lt: new Date(end), + }, + ...department && department !== 'undefined' && { department }, + }; + + return this.find(query, { projection: { _id: 1 } }); + } + + findByNameRegexWithExceptionsAndConditions

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

= {}): AggregationCursor

{ + if (!Array.isArray(exceptions)) { + exceptions = [exceptions]; + } + + const nameRegex = new RegExp(`^${ escapeRegExp(searchTerm).trim() }`, 'i'); + + const match = { + $match: { + name: nameRegex, + _id: { + $nin: exceptions, + }, + ...conditions, + }, + }; + + const { projection, sort, skip, limit } = options; + const project = { + $project: { // TODO: move this logic to client + // eslint-disable-next-line @typescript-eslint/camelcase + custom_name: { $concat: ['$username', ' - ', '$name'] }, + ...projection, + }, + }; + + const order = { $sort: sort || { name: 1 } }; + const params: Record[] = [ + match, + order, + skip && { $skip: skip }, + limit && { $limit: limit }, + project].filter(Boolean) as Record[]; + + return this.col.aggregate(params); + } + + /** + * Find visitors by their email or phone or username or name + * @return [{object}] List of Visitors from db + */ + findVisitorsByEmailOrPhoneOrNameOrUsername(_emailOrPhoneOrNameOrUsername: string, options: FindOneOptions): Cursor { + const filter = new RegExp(_emailOrPhoneOrNameOrUsername, 'i'); + const query = { + $or: [{ + 'visitorEmails.address': filter, + }, { + 'phone.phoneNumber': _emailOrPhoneOrNameOrUsername, + }, { + name: filter, + }, { + username: filter, + }], + }; + + return this.find(query, options); + } +} diff --git a/app/models/server/raw/Messages.js b/app/models/server/raw/Messages.js index 06addaca1cdbc..f3704d6a53b79 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 9aedb96809028..cf80da0b747d8 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 715628e7146e6..f77e0b61bde34 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 f6ebb6dcc34eb..e215e1a925306 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 68c77a772cdda..0000000000000 --- 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 0000000000000..f70d88616b347 --- /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 0000000000000..586fb1d7040ca --- /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/OmnichannelQueue.ts b/app/models/server/raw/OmnichannelQueue.ts index 74a2e1d9f8029..bca8878d86098 100644 --- a/app/models/server/raw/OmnichannelQueue.ts +++ b/app/models/server/raw/OmnichannelQueue.ts @@ -32,12 +32,22 @@ export class OmnichannelQueueRaw extends BaseRaw { } async lockQueue() { + const date = new Date(); const result = await this.col.findOneAndUpdate({ _id: UNIQUE_QUEUE_ID, - locked: false, + $or: [{ + locked: true, + lockedAt: { + $lte: new Date(date.getTime() - 5000), + }, + }, { + locked: false, + }], }, { $set: { locked: true, + // apply 5 secs lock lifetime + lockedAt: new Date(), }, }, { sort: { @@ -55,6 +65,9 @@ export class OmnichannelQueueRaw extends BaseRaw { $set: { locked: false, }, + $unset: { + lockedAt: 1, + }, }, { sort: { _id: 1, diff --git a/app/models/server/raw/Permissions.ts b/app/models/server/raw/Permissions.ts index d5321c82c80b0..1b0bacacc53bb 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 0000000000000..12763332ca3e2 --- /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 0000000000000..9b47fdb8fe9f2 --- /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 7e06551fde567..0000000000000 --- 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 0000000000000..3d776945677ec --- /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 600d55c1fffa4..a4d83368a8b43 100644 --- a/app/models/server/raw/Rooms.js +++ b/app/models/server/raw/Rooms.js @@ -27,10 +27,8 @@ export class RoomsRaw extends BaseRaw { { $match: { t: 'l', - closedAt: { $exists: true }, - metrics: { $exists: true }, - 'metrics.chatDuration': { $exists: true }, ...department && { departmentId: department }, + closedAt: { $exists: true }, }, }, { $sort: { closedAt: -1 } }, @@ -183,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'); @@ -352,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 = {}) { @@ -399,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 f36b44983e193..1bb1342ed8850 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 965604fcd0b54..0000000000000 --- 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 91% rename from app/models/server/models/Sessions.tests.js rename to app/models/server/raw/Sessions.tests.js index 2b8c7703531f5..8b284009c5a4b 100644 --- a/app/models/server/models/Sessions.tests.js +++ b/app/models/server/raw/Sessions.tests.js @@ -1,10 +1,6 @@ -/* eslint-env mocha */ +import { expect } from 'chai'; +import { MongoMemoryServer } from 'mongodb-memory-server'; -import assert from 'assert'; - -import './Sessions.mocks.js'; - -const mongoUnit = require('mongo-unit'); const { MongoClient } = require('mongodb'); const { aggregates } = require('./Sessions'); @@ -238,25 +234,29 @@ const DATA = { lastActivityAt: new Date('2019-05-03T02:59:59.999Z'), }], sessions_dates, -}; // require('./fixtures/testData.json') +}; describe('Sessions Aggregates', () => { let db; if (!process.env.MONGO_URL) { - before(function() { + let mongod; + before(async function() { this.timeout(120000); - return mongoUnit.start({ version: '3.2.22' }) - .then((testMongoUrl) => { process.env.MONGO_URL = testMongoUrl; }); + const version = '5.0.0'; + console.log(`Starting mongo version ${ version }`); + mongod = await MongoMemoryServer.create({ binary: { version } }); + process.env.MONGO_URL = await mongod.getUri(); }); - after(() => { - mongoUnit.stop(); + after(async () => { + await mongod.stop(); }); } before(async () => { - const client = await MongoClient.connect(process.env.MONGO_URL, { useUnifiedTopology: true }); + console.log(`Connecting to mongo at ${ process.env.MONGO_URL }`); + const client = await MongoClient.connect(process.env.MONGO_URL, { useUnifiedTopology: true, useNewUrlParser: true }); db = client.db('test'); after(() => { @@ -267,6 +267,7 @@ describe('Sessions Aggregates', () => { const sessions = db.collection('sessions'); const sessions_dates = db.collection('sessions_dates'); + return Promise.all([ sessions.insertMany(DATA.sessions), sessions_dates.insertMany(DATA.sessions_dates), @@ -276,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.equal(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.deepEqual($match, { + expect($match).to.be.deep.equal({ $and: [{ $or: [ { year: { $gt: 2018 } }, @@ -303,8 +304,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 31); - assert.deepEqual(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 }, @@ -344,7 +345,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 2, day: 10 }); - assert.deepEqual($match, { + expect($match).to.be.deep.equal({ year: 2019, $and: [{ $or: [ @@ -363,8 +364,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 31); - assert.deepEqual(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 }, @@ -404,7 +405,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 5, day: 31 }); - assert.deepEqual($match, { + expect($match).to.be.deep.equal({ year: 2019, month: 5, day: { $gte: 1, $lte: 31 }, @@ -414,8 +415,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 31); - assert.deepEqual(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 }, @@ -455,7 +456,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 4, day: 30 }); - assert.deepEqual($match, { + expect($match).to.be.deep.equal({ year: 2019, month: 4, day: { $gte: 1, $lte: 30 }, @@ -465,8 +466,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 30); - assert.deepEqual(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 }, @@ -505,7 +506,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 2, day: 28 }); - assert.deepEqual($match, { + expect($match).to.be.deep.equal({ year: 2019, month: 2, day: { $gte: 1, $lte: 28 }, @@ -515,8 +516,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 28); - assert.deepEqual(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 }, @@ -553,7 +554,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 2, day: 27 }); - assert.deepEqual($match, { + expect($match).to.be.deep.equal({ year: 2019, $and: [{ $or: [ @@ -572,8 +573,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 31); - assert.deepEqual(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 }, @@ -612,36 +613,25 @@ describe('Sessions Aggregates', () => { it('should have sessions data saved', () => { const collection = db.collection('sessions'); return collection.find().toArray() - .then((docs) => assert.equal(docs.length, DATA.sessions.length)); + .then((docs) => expect(docs.length).to.be.equal(DATA.sessions.length)); }); it('should generate daily sessions', () => { const collection = db.collection('sessions'); return aggregates.dailySessionsOfYesterday(collection, { year: 2019, month: 5, day: 2 }).toArray() - .then((docs) => { + .then(async (docs) => { docs.forEach((doc) => { doc._id = `${ doc.userId }-${ doc.year }-${ doc.month }-${ doc.day }`; }); - assert.equal(docs.length, 3); - assert.deepEqual(docs, [{ + await collection.insertMany(docs); + + expect(docs.length).to.be.equal(3); + expect(docs).to.be.deep.equal([{ _id: 'xPZXw9xqM3kKshsse-2019-5-2', time: 5814, sessions: 3, devices: [{ - sessions: 1, - time: 286, - device: { - type: 'browser', - name: 'Firefox', - longVersion: '66.0.3', - os: { - name: 'Linux', - version: '12', - }, - version: '66.0.3', - }, - }, { sessions: 2, time: 5528, device: { @@ -654,6 +644,19 @@ describe('Sessions Aggregates', () => { }, version: '73.0.3683', }, + }, { + sessions: 1, + time: 286, + device: { + type: 'browser', + name: 'Firefox', + longVersion: '66.0.3', + os: { + name: 'Linux', + version: '12', + }, + version: '66.0.3', + }, }], type: 'user_daily', _computedAt: docs[0]._computedAt, @@ -713,8 +716,6 @@ describe('Sessions Aggregates', () => { userId: 'xPZXw9xqM3kKshsse2', mostImportantRole: 'admin', }]); - - return collection.insertMany(docs); }); }); @@ -722,8 +723,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueUsersOfLastMonthOrWeek(collection, { year: 2019, month: 5, day: 31 }) .then((docs) => { - assert.equal(docs.length, 1); - assert.deepEqual(docs, [{ + expect(docs.length).to.be.equal(1); + expect(docs).to.be.deep.equal([{ count: 2, roles: [{ count: 1, @@ -746,8 +747,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueUsersOfYesterday(collection, { year: 2019, month: 5, day: 1 }) .then((docs) => { - assert.equal(docs.length, 1); - assert.deepEqual(docs, [{ + expect(docs.length).to.be.equal(1); + expect(docs).to.be.deep.equal([{ count: 1, roles: [{ count: 1, @@ -765,8 +766,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueUsersOfYesterday(collection, { year: 2019, month: 5, day: 2 }) .then((docs) => { - assert.equal(docs.length, 1); - assert.deepEqual(docs, [{ + expect(docs.length).to.be.equal(1); + expect(docs).to.be.deep.equal([{ count: 1, roles: [{ count: 1, @@ -784,8 +785,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueDevicesOfLastMonthOrWeek(collection, { year: 2019, month: 5, day: 31 }) .then((docs) => { - assert.equal(docs.length, 2); - assert.deepEqual(docs, [{ + expect(docs.length).to.be.equal(2); + expect(docs).to.be.deep.equal([{ count: 3, time: 9695, type: 'browser', @@ -805,8 +806,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueDevicesOfYesterday(collection, { year: 2019, month: 5, day: 2 }) .then((docs) => { - assert.equal(docs.length, 2); - assert.deepEqual(docs, [{ + expect(docs.length).to.be.equal(2); + expect(docs).to.be.deep.equal([{ count: 2, time: 5528, type: 'browser', @@ -826,8 +827,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueOSOfLastMonthOrWeek(collection, { year: 2019, month: 5, day: 31 }) .then((docs) => { - assert.equal(docs.length, 2); - assert.deepEqual(docs, [{ + expect(docs.length).to.be.equal(2); + expect(docs).to.be.deep.equal([{ count: 3, time: 9695, name: 'Mac OS', @@ -845,8 +846,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueOSOfYesterday(collection, { year: 2019, month: 5, day: 2 }) .then((docs) => { - assert.equal(docs.length, 2); - assert.deepEqual(docs, [{ + expect(docs.length).to.be.equal(2); + expect(docs).to.be.deep.equal([{ count: 2, time: 5528, name: 'Mac OS', @@ -864,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.deepEqual($match, { + expect($match).to.be.deep.equal({ $and: [{ $or: [ { year: { $gt: 2018 } }, @@ -884,8 +885,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 7); - assert.deepEqual(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 }, @@ -901,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.deepEqual($match, { + expect($match).to.be.deep.equal({ year: 2019, $and: [{ $or: [ @@ -920,8 +921,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 7); - assert.deepEqual(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 }, @@ -937,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.deepEqual($match, { + expect($match).to.be.deep.equal({ year: 2019, month: 5, day: { $gte: 1, $lte: 7 }, @@ -947,8 +948,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 7); - assert.deepEqual(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 }, @@ -964,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.deepEqual($match, { + expect($match).to.be.deep.equal({ year: 2019, month: 5, day: { $gte: 8, $lte: 14 }, @@ -974,8 +975,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 7); - assert.deepEqual(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 }, @@ -991,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.equal(docs.length, 0); + expect(docs.length).to.be.equal(0); }); }); @@ -999,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.equal(docs.length, 1); - assert.deepEqual(docs, [{ + expect(docs.length).to.be.equal(1); + expect(docs).to.be.deep.equal([{ count: 2, roles: [{ count: 1, @@ -1023,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.equal(docs.length, 2); - assert.deepEqual(docs, [{ + expect(docs.length).to.be.equal(2); + expect(docs).to.be.deep.equal([{ count: 3, time: 9695, type: 'browser', @@ -1044,8 +1045,8 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions'); return aggregates.getUniqueOSOfLastMonthOrWeek(collection, { year: 2019, month: 5, day: 7 }) .then((docs) => { - assert.equal(docs.length, 2); - assert.deepEqual(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 0000000000000..64d0c5cca7ff0 --- /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 dd475ed9a1312..7e84d539b05eb 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 0000000000000..70c2e3df482d7 --- /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 15b3cf39404a0..0000000000000 --- 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 0000000000000..b3b915a9ebcea --- /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 544af563af91a..2877c147312e1 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 8c8d51b724fc5..f8d1874797cb0 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 e82e124e5ee0d..c65fcea6e533e 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); @@ -74,7 +74,7 @@ export class TeamMemberRaw extends BaseRaw { findByUserIdAndTeamIds(userId: string, teamIds: Array, options: FindOneOptions = {}): Cursor { const query = { - 'u._id': userId, + userId, teamId: { $in: teamIds, }, diff --git a/app/models/server/raw/Uploads.ts b/app/models/server/raw/Uploads.ts new file mode 100644 index 0000000000000..ad2fd67247c91 --- /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 0000000000000..684135c9d57a3 --- /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 89b491ab2f9ec..dc2cab7b6232b 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); @@ -138,6 +156,36 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } + async findOneByLDAPId(id, attribute = undefined) { + const query = { + 'services.ldap.id': id, + }; + + if (attribute) { + query['services.ldap.idAttribute'] = attribute; + } + + return this.findOne(query); + } + + findLDAPUsers(options) { + const query = { ldap: true }; + + return this.find(query, options); + } + + findConnectedLDAPUsers(options) { + const query = { + ldap: true, + 'services.resume.loginTokens': { + $exists: true, + $ne: [], + }, + }; + + return this.find(query, options); + } + isUserInRole(userId, roleName) { const query = { _id: userId, @@ -229,6 +277,22 @@ export class UsersRaw extends BaseRaw { return result.value; } + setLivechatStatusIf(userId, status, conditions = {}, extraFields = {}) { // TODO: Create class Agent + const query = { + _id: userId, + ...conditions, + }; + + const update = { + $set: { + statusLivechat: status, + ...extraFields, + }, + }; + + return this.update(query, update); + } + async getAgentAndAmountOngoingChats(userId) { const aggregate = [ { $match: { _id: userId, status: { $exists: true, $ne: 'offline' }, statusLivechat: 'available', roles: 'livechat-agent' } }, @@ -587,6 +651,24 @@ export class UsersRaw extends BaseRaw { return this.update(query, update, { multi: true }); } + setLivechatStatusActiveBasedOnBusinessHours(userId) { + const query = { + _id: userId, + openBusinessHours: { + $exists: true, + $not: { $size: 0 }, + }, + }; + + const update = { + $set: { + statusLivechat: 'available', + }, + }; + + return this.update(query, update); + } + async isAgentWithinBusinessHours(agentId) { return await this.find({ _id: agentId, @@ -624,12 +706,12 @@ export class UsersRaw extends BaseRaw { }); } - removeResumeService(userId) { + unsetLoginTokens(userId) { return this.col.updateOne({ _id: userId, }, { - $unset: { - 'services.resume': 1, + $set: { + 'services.resume.loginTokens': [], }, }); } @@ -642,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 b89feaa9deb3f..3560f1e175d1a 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 bcd87761c2674..0000000000000 --- 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 0000000000000..1a7fea7114e6f --- /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 0000000000000..891392ee3f7e4 --- /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 87ecbad097d1b..34789c62ce2fe 100644 --- a/app/models/server/raw/index.ts +++ b/app/models/server/raw/index.ts @@ -1,81 +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 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); @@ -86,43 +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) { @@ -145,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/nextcloud/server/addWebdavServer.js b/app/nextcloud/server/addWebdavServer.js index fa331312705f6..22cdd337da5d5 100644 --- a/app/nextcloud/server/addWebdavServer.js +++ b/app/nextcloud/server/addWebdavServer.js @@ -1,10 +1,11 @@ import { Meteor } from 'meteor/meteor'; -import { callbacks } from '../../callbacks'; -import { settings } from '../../settings'; +import { callbacks } from '../../callbacks/server'; +import { settings } from '../../settings/server'; +import { SystemLogger } from '../../../server/lib/logger/system'; Meteor.startup(() => { - settings.get('Webdav_Integration_Enabled', (key, value) => { + settings.watch('Webdav_Integration_Enabled', (value) => { if (value) { return callbacks.add('afterValidateLogin', (login) => { const { user } = login; @@ -25,7 +26,7 @@ Meteor.startup(() => { try { Meteor.runAsUser(user._id, () => Meteor.call('addWebdavAccountByToken', data)); } catch (error) { - console.log(error); + SystemLogger.error(error); } }, callbacks.priority.MEDIUM, 'add-webdav-server'); } diff --git a/app/nextcloud/server/startup.js b/app/nextcloud/server/startup.js deleted file mode 100644 index f995a2a460aec..0000000000000 --- a/app/nextcloud/server/startup.js +++ /dev/null @@ -1,19 +0,0 @@ -import { settings } from '../../settings'; - -settings.addGroup('OAuth', function() { - this.section('Nextcloud', function() { - const enableQuery = { - _id: 'Accounts_OAuth_Nextcloud', - value: true, - }; - - this.add('Accounts_OAuth_Nextcloud', false, { type: 'boolean', public: true }); - this.add('Accounts_OAuth_Nextcloud_URL', '', { type: 'string', enableQuery, public: true }); - this.add('Accounts_OAuth_Nextcloud_id', '', { type: 'string', enableQuery }); - this.add('Accounts_OAuth_Nextcloud_secret', '', { type: 'string', enableQuery }); - this.add('Accounts_OAuth_Nextcloud_callback_url', '_oauth/nextcloud', { type: 'relativeUrl', readonly: true, force: true, enableQuery }); - this.add('Accounts_OAuth_Nextcloud_button_label_text', 'Nextcloud', { type: 'string', public: true, i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Text', persistent: true }); - this.add('Accounts_OAuth_Nextcloud_button_label_color', '#ffffff', { type: 'string', public: true, i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Color', persistent: true }); - this.add('Accounts_OAuth_Nextcloud_button_color', '#0082c9', { type: 'string', public: true, i18nLabel: 'Accounts_OAuth_Custom_Button_Color', persistent: true }); - }); -}); diff --git a/app/nextcloud/server/startup.ts b/app/nextcloud/server/startup.ts new file mode 100644 index 0000000000000..9a5f3cff0e14f --- /dev/null +++ b/app/nextcloud/server/startup.ts @@ -0,0 +1,19 @@ +import { settingsRegistry } from '../../settings/server'; + +settingsRegistry.addGroup('OAuth', function() { + this.section('Nextcloud', function() { + const enableQuery = { + _id: 'Accounts_OAuth_Nextcloud', + value: true, + }; + + this.add('Accounts_OAuth_Nextcloud', false, { type: 'boolean', public: true }); + this.add('Accounts_OAuth_Nextcloud_URL', '', { type: 'string', enableQuery, public: true }); + this.add('Accounts_OAuth_Nextcloud_id', '', { type: 'string', enableQuery }); + this.add('Accounts_OAuth_Nextcloud_secret', '', { type: 'string', enableQuery }); + this.add('Accounts_OAuth_Nextcloud_callback_url', '_oauth/nextcloud', { type: 'relativeUrl', readonly: true, enableQuery }); + this.add('Accounts_OAuth_Nextcloud_button_label_text', 'Nextcloud', { type: 'string', public: true, i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Text', persistent: true }); + this.add('Accounts_OAuth_Nextcloud_button_label_color', '#ffffff', { type: 'string', public: true, i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Color', persistent: true }); + this.add('Accounts_OAuth_Nextcloud_button_color', '#0082c9', { type: 'string', public: true, i18nLabel: 'Accounts_OAuth_Custom_Button_Color', persistent: true }); + }); +}); diff --git a/app/notification-queue/server/NotificationQueue.ts b/app/notification-queue/server/NotificationQueue.ts index 42f05827b919e..d16da1cc71eeb 100644 --- a/app/notification-queue/server/NotificationQueue.ts +++ b/app/notification-queue/server/NotificationQueue.ts @@ -5,6 +5,7 @@ import { NotificationQueue, Users } from '../../models/server/raw'; import { sendEmailFromData } from '../../lib/server/functions/notifications/email'; import { PushNotification } from '../../push-notifications/server'; import { IUser } from '../../../definition/IUser'; +import { SystemLogger } from '../../../server/lib/logger/system'; const { NOTIFICATIONS_WORKER_TIMEOUT = 2000, @@ -45,7 +46,7 @@ class NotificationClass { try { this.worker(); } catch (e) { - console.error('Error sending notification', e); + SystemLogger.error('Error sending notification', e); this.executeWorkerLater(); } }, this.cyclePause); @@ -81,7 +82,7 @@ class NotificationClass { NotificationQueue.removeById(notification._id); } catch (e) { - console.error(e); + SystemLogger.error(e); await NotificationQueue.setErrorById(notification._id, e.message); } diff --git a/app/notifications/client/lib/Notifications.js b/app/notifications/client/lib/Notifications.js index cc9f8f9ebb4b3..bf601bb7878d3 100644 --- a/app/notifications/client/lib/Notifications.js +++ b/app/notifications/client/lib/Notifications.js @@ -1,6 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; +import './Presence'; + class Notifications { constructor(...args) { this.logged = Meteor.userId() !== null; @@ -17,6 +19,7 @@ class Notifications { this.streamRoom = new Meteor.Streamer('notify-room'); this.streamRoomUsers = new Meteor.Streamer('notify-room-users'); this.streamUser = new Meteor.Streamer('notify-user'); + if (this.debug === true) { this.onAll(function() { return console.log('RocketChat.Notifications: onAll', args); @@ -75,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) { @@ -92,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/client/lib/Presence.ts b/app/notifications/client/lib/Presence.ts new file mode 100644 index 0000000000000..2c222d28d6438 --- /dev/null +++ b/app/notifications/client/lib/Presence.ts @@ -0,0 +1,13 @@ +import { Meteor } from 'meteor/meteor'; + +import { Presence, STATUS_MAP } from '../../../../client/lib/presence'; + +// TODO implement API on Streamer to be able to listen to all streamed data +// this is a hacky way to listen to all streamed data from user-presense Streamer +(Meteor as any).StreamerCentral.on('stream-user-presence', (uid: string, args: unknown) => { + if (!Array.isArray(args)) { + throw new Error('Presence event must be an array'); + } + const [username, status, statusText] = args as [string, number, string | undefined]; + Presence.notify({ _id: uid, username, status: STATUS_MAP[status], statusText }); +}); diff --git a/app/notifications/server/lib/Notifications.ts b/app/notifications/server/lib/Notifications.ts index 0bd8eaf7d0573..214d642d5f417 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'; @@ -11,6 +10,7 @@ import { Users as UsersRaw, Settings as SettingsRaw, } from '../../../models/server/raw'; +import './Presence'; // TODO: Replace this in favor of the api.broadcast // StreamerCentral.on('broadcast', (name, eventName, args) => { diff --git a/app/notifications/server/lib/Presence.ts b/app/notifications/server/lib/Presence.ts new file mode 100644 index 0000000000000..19ccba63461a0 --- /dev/null +++ b/app/notifications/server/lib/Presence.ts @@ -0,0 +1,103 @@ +import { Emitter } from '@rocket.chat/emitter'; +import type { IPublication, IStreamerConstructor, Connection, IStreamer } from 'meteor/rocketchat:streamer'; + +import type { IUser } from '../../../../definition/IUser'; + +export type UserPresenseStreamProps = { + added: IUser['_id'][]; + removed: IUser['_id'][]; +} + +export type UserPresenseStreamArgs = { + 'uid': string; + args: unknown; +} + +const e = new Emitter<{ + [key: string]: UserPresenseStreamArgs; +}>(); + + +const clients = new WeakMap(); + + +export class UserPresence { + private readonly streamer: IStreamer; + + private readonly publication: IPublication; + + private readonly listeners: Set; + + constructor(publication: IPublication, streamer: IStreamer) { + this.listeners = new Set(); + this.publication = publication; + this.streamer = streamer; + } + + listen(uid: string): void { + if (this.listeners.has(uid)) { + return; + } + e.on(uid, this.run); + this.listeners.add(uid); + } + + off = (uid: string): void => { + e.off(uid, this.run); + this.listeners.delete(uid); + } + + run = (args: UserPresenseStreamArgs): void => { + const payload = this.streamer.changedPayload(this.streamer.subscriptionName, args.uid, { ...args, eventName: args.uid }); // there is no good explanation to keep eventName, I just want to save one 'DDPCommon.parseDDP' on the client side, so I'm trying to fit the Meteor Streamer's payload + (this.publication as any)._session.socket.send(payload); + } + + stop(): void { + this.listeners.forEach(this.off); + clients.delete(this.publication.connection); + } + + static getClient(publication: IPublication, streamer: IStreamer): [UserPresence, boolean] { + const { connection } = publication; + const stored = clients.get(connection); + + const client = stored || new UserPresence(publication, streamer); + + const main = Boolean(!stored); + + clients.set(connection, client); + + return [client, main]; + } +} + +export class StreamPresence { + static getInstance(Streamer: IStreamerConstructor, name = 'user-presence'): IStreamer { + return new class StreamPresence extends Streamer { + async _publish(publication: IPublication, _eventName: string, options: boolean | {useCollection?: boolean; args?: any} = false): Promise { + const { added, removed } = (typeof options !== 'boolean' ? options : {}) as unknown as UserPresenseStreamProps; + + + const [client, main] = UserPresence.getClient(publication, this); + + added?.forEach((uid) => client.listen(uid)); + removed?.forEach((uid) => client.off(uid)); + + + if (!main) { + publication.stop(); + return; + } + + publication.ready(); + + publication.onStop(() => client.stop()); + } + }(name); + } +} + + +export const emit = (uid: string, args: UserPresenseStreamArgs): void => { + e.emit(uid, { uid, args }); +}; diff --git a/app/nrr/client/nrr.js b/app/nrr/client/nrr.js index d783880c2770c..5ec665da590d9 100644 --- a/app/nrr/client/nrr.js +++ b/app/nrr/client/nrr.js @@ -17,7 +17,7 @@ const makeCursorReactive = function(obj) { } }; -Blaze.toHTMLWithDataNonReactive = function(content, data) { +const toHTMLWithDataNonReactive = function(content, data) { makeCursorReactive(data); if (data instanceof Spacebars.kw && Object.keys(data.hash).length > 0) { @@ -27,20 +27,14 @@ Blaze.toHTMLWithDataNonReactive = function(content, data) { return Tracker.nonreactive(() => Blaze.toHTMLWithData(content, data)); }; -Blaze.registerHelper('nrrargs', function(...args) { - return { - _arguments: args, - }; -}); - -Blaze.renderNonReactive = function(templateName, data) { +const renderNonReactive = function(templateName, data) { const { _arguments } = this.parentView.dataVar.get(); [templateName, data] = _arguments; return Tracker.nonreactive(() => { console.warn('Nrr template is deprecated'); - const view = new Blaze.View('nrr', () => HTML.Raw(Blaze.toHTMLWithDataNonReactive(Template[templateName], data))); + const view = new Blaze.View('nrr', () => HTML.Raw(toHTMLWithDataNonReactive(Template[templateName], data))); view.onViewReady(() => { const { onViewReady } = Template[templateName]; @@ -56,4 +50,4 @@ Blaze.renderNonReactive = function(templateName, data) { }); }; -Blaze.registerHelper('nrr', Blaze.Template('nrr', Blaze.renderNonReactive)); +Template.nrr = new Blaze.Template('nrr', renderNonReactive); diff --git a/app/oauth2-server-config/server/admin/methods/addOAuthApp.js b/app/oauth2-server-config/server/admin/methods/addOAuthApp.js index cb4d73e19f27c..688409e5ddf4c 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 6c0b1e665de64..d1df82d95704a 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 007f5be2e95c4..3a7f88dda09e7 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 d39489c9ec850..0000000000000 --- 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 0000000000000..05fd8f5c5d350 --- /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 f1c51982c7606..c801074db4d50 100644 --- a/app/oauth2-server-config/server/oauth/oauth2-server.js +++ b/app/oauth2-server-config/server/oauth/oauth2-server.js @@ -1,18 +1,28 @@ 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, }); +// https://github.com/RocketChat/rocketchat-oauth2-server/blob/e758fd7ef69348c7ceceabe241747a986c32d036/model.coffee#L27-L27 +function getAccessToken(accessToken) { + return oauth2server.oauth.model.AccessTokens.findOne({ + accessToken, + }); +} + oauth2server.app.disable('x-powered-by'); oauth2server.routes.disable('x-powered-by'); @@ -23,9 +33,7 @@ oauth2server.routes.get('/oauth/userinfo', function(req, res) { return res.sendStatus(401).send('No token'); } const accessToken = req.headers.authorization.replace('Bearer ', ''); - const token = oauth2server.oauth.model.AccessTokens.findOne({ - accessToken, - }); + const token = getAccessToken(accessToken); if (token == null) { return res.sendStatus(401).send('Invalid Token'); } @@ -61,7 +69,7 @@ API.v1.addAuthMethod(function() { if (bearerToken == null) { return; } - const getAccessToken = Meteor.wrapAsync(oauth2server.oauth.model.getAccessToken, oauth2server.oauth.model); + const accessToken = getAccessToken(bearerToken); if (accessToken == null) { return; diff --git a/app/oembed/server/providers.js b/app/oembed/server/providers.js index f6ba54a3d3ba9..09415153a64bd 100644 --- a/app/oembed/server/providers.js +++ b/app/oembed/server/providers.js @@ -4,7 +4,8 @@ import QueryString from 'querystring'; import { camelCase } from 'change-case'; import _ from 'underscore'; -import { callbacks } from '../../callbacks'; +import { callbacks } from '../../callbacks/server'; +import { SystemLogger } from '../../../server/lib/logger/system'; class Providers { constructor() { @@ -146,7 +147,7 @@ callbacks.add('oembed:afterParseContent', function(data) { } }); } catch (error) { - console.log(error); + SystemLogger.error(error); } return data; }, callbacks.priority.MEDIUM, 'oembed-providers-after'); diff --git a/app/oembed/server/server.js b/app/oembed/server/server.js index 34aa1a8eee1a6..a3b32443fd1db 100644 --- a/app/oembed/server/server.js +++ b/app/oembed/server/server.js @@ -10,10 +10,12 @@ 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'; +import { SystemLogger } from '../../../server/lib/logger/system'; const request = HTTPInternals.NpmModules.request.module; const OEmbed = {}; @@ -213,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; @@ -222,9 +224,9 @@ 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) { - console.error('OEmbed duplicated record', url); + SystemLogger.error('OEmbed duplicated record', url); } return data; } @@ -261,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) { @@ -290,7 +292,7 @@ OEmbed.rocketUrlParser = function(message) { item.parsedUrl = data.parsedUrl; changed = true; } - }); + } if (attachments.length) { Messages.setMessageAttachments(message._id, attachments); } @@ -301,9 +303,9 @@ OEmbed.rocketUrlParser = function(message) { return message; }; -settings.get('API_Embed', function(key, value) { +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/otr/client/rocketchat.otr.js b/app/otr/client/rocketchat.otr.js index 76d48a1bf4e6e..41a54ce1b138b 100644 --- a/app/otr/client/rocketchat.otr.js +++ b/app/otr/client/rocketchat.otr.js @@ -3,9 +3,10 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { Tracker } from 'meteor/tracker'; import { Subscriptions } from '../../models'; -import { promises } from '../../promises/client'; import { Notifications } from '../../notifications'; import { t } from '../../utils'; +import { onClientMessageReceived } from '../../../client/lib/onClientMessageReceived'; +import { onClientBeforeSendMessage } from '../../../client/lib/onClientBeforeSendMessage'; class OTRClass { constructor() { @@ -54,7 +55,7 @@ Meteor.startup(function() { } }); - promises.add('onClientBeforeSendMessage', function(message) { + onClientBeforeSendMessage.use(function(message) { if (message.rid && OTR.getInstanceByRoomId(message.rid) && OTR.getInstanceByRoomId(message.rid).established.get()) { return OTR.getInstanceByRoomId(message.rid).encrypt(message) .then((msg) => { @@ -64,9 +65,9 @@ Meteor.startup(function() { }); } return Promise.resolve(message); - }, promises.priority.HIGH); + }); - promises.add('onClientMessageReceived', function(message) { + onClientMessageReceived.use(function(message) { if (message.rid && OTR.getInstanceByRoomId(message.rid) && OTR.getInstanceByRoomId(message.rid).established.get()) { if (message.notification) { message.msg = t('Encrypted_message'); @@ -105,5 +106,5 @@ Meteor.startup(function() { message.msg = ''; } return Promise.resolve(message); - }, promises.priority.HIGH); + }); }); diff --git a/app/otr/client/rocketchat.otr.room.js b/app/otr/client/rocketchat.otr.room.js index 6dac9aaa08587..410acb041bc96 100644 --- a/app/otr/client/rocketchat.otr.room.js +++ b/app/otr/client/rocketchat.otr.room.js @@ -6,15 +6,15 @@ import { Tracker } from 'meteor/tracker'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { TimeSync } from 'meteor/mizzao:timesync'; import _ from 'underscore'; -import toastr from 'toastr'; import { OTR } from './rocketchat.otr'; import { Notifications } from '../../notifications'; -import { getUidDirectMessage } from '../../ui-utils/client/lib/getUidDirectMessage'; +import { getUidDirectMessage } from '../../../client/lib/utils/getUidDirectMessage'; import { Presence } from '../../../client/lib/presence'; -import { goToRoomById } from '../../../client/lib/goToRoomById'; +import { goToRoomById } from '../../../client/lib/utils/goToRoomById'; import { imperativeModal } from '../../../client/lib/imperativeModal'; import GenericModal from '../../../client/components/GenericModal'; +import { dispatchToastMessage } from '../../../client/lib/toast'; OTR.Room = class { constructor(userId, roomId) { @@ -95,7 +95,7 @@ OTR.Room = class { Meteor.call('deleteOldOTRMessages', this.roomId); }) .catch((e) => { - toastr.error(e); + dispatchToastMessage({ type: 'error', message: e }); }); } @@ -174,7 +174,7 @@ OTR.Room = class { return data; }) .catch((e) => { - toastr.error(e); + dispatchToastMessage({ type: 'error', message: e }); return message; }); } diff --git a/app/otr/server/settings.js b/app/otr/server/settings.js deleted file mode 100644 index fbecb9c7519f0..0000000000000 --- a/app/otr/server/settings.js +++ /dev/null @@ -1,9 +0,0 @@ -import { settings } from '../../settings'; - -settings.addGroup('OTR', function() { - this.add('OTR_Enable', true, { - type: 'boolean', - i18nLabel: 'Enabled', - public: true, - }); -}); diff --git a/app/otr/server/settings.ts b/app/otr/server/settings.ts new file mode 100644 index 0000000000000..6d42a3e29fd2f --- /dev/null +++ b/app/otr/server/settings.ts @@ -0,0 +1,9 @@ +import { settingsRegistry } from '../../settings/server'; + +settingsRegistry.addGroup('OTR', function() { + this.add('OTR_Enable', true, { + type: 'boolean', + i18nLabel: 'Enabled', + public: true, + }); +}); diff --git a/app/promises/client/index.js b/app/promises/client/index.js deleted file mode 100644 index 999b1f62bc34e..0000000000000 --- a/app/promises/client/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import { promises } from '../lib/promises'; - -export { - promises, -}; diff --git a/app/promises/lib/promises.js b/app/promises/lib/promises.js deleted file mode 100644 index 62ad6b51af37c..0000000000000 --- a/app/promises/lib/promises.js +++ /dev/null @@ -1,84 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Random } from 'meteor/random'; -import _ from 'underscore'; - -/* -* Callback hooks provide an easy way to add extra steps to common operations. -* @namespace RocketChat.promises -*/ - -export const promises = {}; - - -/* -* Callback priorities -*/ - -promises.priority = { - HIGH: -1000, - MEDIUM: 0, - LOW: 1000, -}; - -const getHook = (hookName) => promises[hookName] || []; - -/* -* Add a callback function to a hook -* @param {String} hook - The name of the hook -* @param {Function} callback - The callback function -*/ - -promises.add = function(hook, callback, p = promises.priority.MEDIUM, id) { - callback.priority = _.isNumber(p) ? p : promises.priority.MEDIUM; - callback.id = id || Random.id(); - promises[hook] = getHook(hook); - if (promises[hook].find((cb) => cb.id === callback.id)) { - return; - } - promises[hook].push(callback); - promises[hook] = _.sortBy(promises[hook], (callback) => callback.priority || promises.priority.MEDIUM); -}; - - -/* -* Remove a callback from a hook -* @param {string} hook - The name of the hook -* @param {string} id - The callback's id -*/ - -promises.remove = function(hook, id) { - promises[hook] = getHook(hook).filter((callback) => callback.id !== id); -}; - - -/* -* Successively run all of a hook's callbacks on an item -* @param {String} hook - The name of the hook -* @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks -* @param {Object} [constant] - An optional constant that will be passed along to each callback -* @returns {Object} Returns the item after it's been through all the callbacks for this hook -*/ - -promises.run = function(hook, item, constant) { - const callbacks = promises[hook]; - if (callbacks == null || callbacks.length === 0) { - return Promise.resolve(item); - } - return callbacks.reduce((previousPromise, callback) => previousPromise.then((result) => callback(result, constant)), Promise.resolve(item)); -}; - - -/* -* Successively run all of a hook's callbacks on an item, in async mode (only works on server) -* @param {String} hook - The name of the hook -* @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks -* @param {Object} [constant] - An optional constant that will be passed along to each callback -*/ - -promises.runAsync = function(hook, item, constant) { - const callbacks = promises[hook]; - if (!Meteor.isServer || callbacks == null || callbacks.length === 0) { - return item; - } - Meteor.defer(() => callbacks.forEach((callback) => callback(item, constant))); -}; diff --git a/app/promises/server/index.js b/app/promises/server/index.js deleted file mode 100644 index 999b1f62bc34e..0000000000000 --- a/app/promises/server/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import { promises } from '../lib/promises'; - -export { - promises, -}; diff --git a/app/push-notifications/server/methods/saveNotificationSettings.js b/app/push-notifications/server/methods/saveNotificationSettings.js index 56537abd76834..5ec16221e1415 100644 --- a/app/push-notifications/server/methods/saveNotificationSettings.js +++ b/app/push-notifications/server/methods/saveNotificationSettings.js @@ -2,7 +2,11 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import { Subscriptions } from '../../../models/server'; -import { getUserNotificationPreference } from '../../../utils'; +import { getUserNotificationPreference } from '../../../utils/server'; + +const saveAudioNotificationValue = (subId, value) => (value === 'default' + ? Subscriptions.clearAudioNotificationValueById(subId) + : Subscriptions.updateAudioNotificationValueById(subId, value)); Meteor.methods({ saveNotificationSettings(roomId, field, value) { @@ -22,9 +26,6 @@ Meteor.methods({ }; const notifications = { - audioNotifications: { - updateMethod: (subscription, value) => Subscriptions.updateNotificationsPrefById(subscription._id, getNotificationPrefValue('audio', value), 'audioNotifications', 'audioPrefOrigin'), - }, desktopNotifications: { updateMethod: (subscription, value) => Subscriptions.updateNotificationsPrefById(subscription._id, getNotificationPrefValue('desktop', value), 'desktopNotifications', 'desktopPrefOrigin'), }, @@ -47,12 +48,12 @@ Meteor.methods({ updateMethod: (subscription, value) => Subscriptions.updateMuteGroupMentions(subscription._id, value === '1'), }, audioNotificationValue: { - updateMethod: (subscription, value) => Subscriptions.updateAudioNotificationValueById(subscription._id, value), + updateMethod: (subscription, value) => saveAudioNotificationValue(subscription._id, value), }, }; const isInvalidNotification = !Object.keys(notifications).includes(field); const basicValuesForNotifications = ['all', 'mentions', 'nothing', 'default']; - const fieldsMustHaveBasicValues = ['emailNotifications', 'audioNotifications', 'mobilePushNotifications', 'desktopNotifications']; + const fieldsMustHaveBasicValues = ['emailNotifications', 'mobilePushNotifications', 'desktopNotifications']; if (isInvalidNotification) { throw new Meteor.Error('error-invalid-settings', 'Invalid settings field', { method: 'saveNotificationSettings' }); @@ -77,7 +78,7 @@ Meteor.methods({ if (!subscription) { throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', { method: 'saveAudioNotificationValue' }); } - Subscriptions.updateAudioNotificationValueById(subscription._id, value); + saveAudioNotificationValue(subscription._id, value); return true; }, }); diff --git a/app/push/server/apn.js b/app/push/server/apn.js index 693f8dcc05297..d84846fd592e2 100644 --- a/app/push/server/apn.js +++ b/app/push/server/apn.js @@ -70,7 +70,7 @@ export const initAPN = ({ options, absoluteUrl }) => { // Give the user warnings about development settings if (options.apn.development) { // This flag is normally set by the configuration file - console.warn('WARNING: Push APN is using development key and certificate'); + logger.warn('WARNING: Push APN is using development key and certificate'); } else if (options.apn.gateway) { // We check the apn gateway i the options, we could risk shipping // server into production while using the production configuration. @@ -83,39 +83,39 @@ export const initAPN = ({ options, absoluteUrl }) => { if (options.apn.gateway === 'gateway.sandbox.push.apple.com') { // Using the development sandbox - console.warn('WARNING: Push APN is in development mode'); + logger.warn('WARNING: Push APN is in development mode'); } else if (options.apn.gateway === 'gateway.push.apple.com') { // In production - but warn if we are running on localhost if (/http:\/\/localhost/.test(absoluteUrl)) { - console.warn('WARNING: Push APN is configured to production mode - but server is running from localhost'); + logger.warn('WARNING: Push APN is configured to production mode - but server is running from localhost'); } } else { // Warn about gateways we dont know about - console.warn(`WARNING: Push APN unknown gateway "${ options.apn.gateway }"`); + logger.warn(`WARNING: Push APN unknown gateway "${ options.apn.gateway }"`); } } else if (options.apn.production) { if (/http:\/\/localhost/.test(absoluteUrl)) { - console.warn('WARNING: Push APN is configured to production mode - but server is running from localhost'); + logger.warn('WARNING: Push APN is configured to production mode - but server is running from localhost'); } } else { - console.warn('WARNING: Push APN is in development mode'); + logger.warn('WARNING: Push APN is in development mode'); } // Check certificate data if (!options.apn.cert || !options.apn.cert.length) { - console.error('ERROR: Push server could not find cert'); + logger.error('ERROR: Push server could not find cert'); } // Check key data if (!options.apn.key || !options.apn.key.length) { - console.error('ERROR: Push server could not find key'); + logger.error('ERROR: Push server could not find key'); } // Rig apn connection try { apnConnection = new apn.Provider(options.apn); } catch (e) { - console.error('Error trying to initialize APN'); - console.error(e); + logger.error('Error trying to initialize APN'); + logger.error(e); } }; diff --git a/app/push/server/gcm.js b/app/push/server/gcm.js index a677e630b1968..dc3890825593e 100644 --- a/app/push/server/gcm.js +++ b/app/push/server/gcm.js @@ -92,7 +92,7 @@ export const sendGCM = function({ userTokens, notification, _replaceToken, _remo sender.send(message, userTokens, 5, function(err, result) { if (err) { - logger.debug(`ANDROID ERROR: result of sender: ${ result }`); + logger.debug({ msg: 'ANDROID ERROR: result of sender', result }); return; } @@ -101,14 +101,14 @@ export const sendGCM = function({ userTokens, notification, _replaceToken, _remo return; } - logger.debug(`ANDROID: Result of sender: ${ JSON.stringify(result) }`); + logger.debug({ msg: 'ANDROID: Result of sender', result }); if (result.canonical_ids === 1 && userToken) { // This is an old device, token is replaced try { _replaceToken({ gcm: userToken }, { gcm: result.results[0].registration_id }); } catch (err) { - logger.error('Error replacing token', err); + logger.error({ msg: 'Error replacing token', err }); } } // We cant send to that token - might not be registered @@ -118,7 +118,7 @@ export const sendGCM = function({ userTokens, notification, _replaceToken, _remo try { _removeToken({ gcm: userToken }); } catch (err) { - logger.error('Error removing token', err); + logger.error({ msg: 'Error removing token', err }); } } }); diff --git a/app/push/server/logger.js b/app/push/server/logger.js index 553e253eee470..f770aef378f6d 100644 --- a/app/push/server/logger.js +++ b/app/push/server/logger.js @@ -1,4 +1,3 @@ -import { Logger, LoggerManager } from '../../logger/server'; +import { Logger } from '../../../server/lib/logger/Logger'; export const logger = new Logger('Push'); -export { LoggerManager }; diff --git a/app/push/server/push.js b/app/push/server/push.js index e95ca8f88716c..2d576fe3ea345 100644 --- a/app/push/server/push.js +++ b/app/push/server/push.js @@ -6,7 +6,7 @@ import _ from 'underscore'; import { initAPN, sendAPN } from './apn'; import { sendGCM } from './gcm'; -import { logger, LoggerManager } from './logger'; +import { logger } from './logger'; import { settings } from '../../settings/server'; export const _matchToken = Match.OneOf({ apn: String }, { gcm: String }); @@ -208,7 +208,7 @@ export class PushClass { return this.sendNotificationNative(app, notification, countApn, countGcm); }); - if (LoggerManager.logLevel === 2) { + if (settings.get('Log_Level') === '2') { logger.debug(`Sent message "${ notification.title }" to ${ countApn.length } ios apps ${ countGcm.length } android apps`); // Add some verbosity about the send result, making sure the developer diff --git a/app/reactions/client/stylesheets/reaction.css b/app/reactions/client/stylesheets/reaction.css index b8d6e3897130d..7bb7a232b0fc3 100644 --- a/app/reactions/client/stylesheets/reaction.css +++ b/app/reactions/client/stylesheets/reaction.css @@ -1,6 +1,6 @@ .message { & .reactions { - margin-top: 4px; + margin-top: 8px; padding: 0; & > li { diff --git a/app/reactions/server/setReaction.js b/app/reactions/server/setReaction.js index 53de3fe70c1fd..e5f2a885b3083 100644 --- a/app/reactions/server/setReaction.js +++ b/app/reactions/server/setReaction.js @@ -2,11 +2,12 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import _ from 'underscore'; -import { Messages, EmojiCustom, Rooms } from '../../models'; -import { callbacks } from '../../callbacks'; -import { emoji } from '../../emoji'; -import { isTheLastMessage, msgStream } from '../../lib'; -import { hasPermission } from '../../authorization/server/functions/hasPermission'; +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'; +import { canAccessRoom, hasPermission } from '../../authorization/server'; import { api } from '../../../server/sdk/api'; const removeUserReaction = (message, reaction, username) => { @@ -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' }); } @@ -91,17 +92,19 @@ export const executeSetReaction = async function(reaction, messageId, shouldReac } const message = Messages.findOneById(messageId); - if (!message) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' }); } - const room = Meteor.call('canAccessRoom', message.rid, Meteor.userId()); - + const room = Rooms.findOneById(message.rid); if (!room) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' }); } + if (!canAccessRoom(room, user)) { + throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'setReaction' }); + } + return setReaction(room, user, message, reaction, shouldReact); }; diff --git a/app/retention-policy/server/cronPruneMessages.js b/app/retention-policy/server/cronPruneMessages.js index 5508747d1f369..63cb51d207422 100644 --- a/app/retention-policy/server/cronPruneMessages.js +++ b/app/retention-policy/server/cronPruneMessages.js @@ -1,6 +1,4 @@ -import { Meteor } from 'meteor/meteor'; import { SyncedCron } from 'meteor/littledata:synced-cron'; -import { debounce } from 'underscore'; import { settings } from '../../settings/server'; import { Rooms } from '../../models/server'; @@ -79,7 +77,16 @@ function deployCron(precision) { }); } -const reloadPolicy = debounce(Meteor.bindEnvironment(function reloadPolicy() { +settings.watchMultiple(['RetentionPolicy_Enabled', + 'RetentionPolicy_AppliesToChannels', + 'RetentionPolicy_AppliesToGroups', + 'RetentionPolicy_AppliesToDMs', + 'RetentionPolicy_MaxAge_Channels', + 'RetentionPolicy_MaxAge_Groups', + 'RetentionPolicy_MaxAge_DMs', + 'RetentionPolicy_Advanced_Precision', + 'RetentionPolicy_Advanced_Precision_Cron', + 'RetentionPolicy_Precision'], function reloadPolicy() { types = []; if (!settings.get('RetentionPolicy_Enabled')) { @@ -105,11 +112,4 @@ const reloadPolicy = debounce(Meteor.bindEnvironment(function reloadPolicy() { const precision = (settings.get('RetentionPolicy_Advanced_Precision') && settings.get('RetentionPolicy_Advanced_Precision_Cron')) || getSchedule(settings.get('RetentionPolicy_Precision')); return deployCron(precision); -}), 500); - -Meteor.startup(function() { - Meteor.defer(function() { - settings.get(/^RetentionPolicy_/, reloadPolicy); - reloadPolicy(); - }); }); diff --git a/app/retention-policy/server/startup/settings.js b/app/retention-policy/server/startup/settings.js deleted file mode 100644 index 94696f7651f4a..0000000000000 --- a/app/retention-policy/server/startup/settings.js +++ /dev/null @@ -1,127 +0,0 @@ -import { settings } from '../../../settings'; - -settings.addGroup('RetentionPolicy', function() { - const globalQuery = { - _id: 'RetentionPolicy_Enabled', - value: true, - }; - - this.add('RetentionPolicy_Enabled', false, { - type: 'boolean', - public: true, - i18nLabel: 'RetentionPolicy_Enabled', - alert: 'Watch out! Tweaking these settings without utmost care can destroy all message history. Please read the documentation before turning the feature on at rocket.chat/docs/administrator-guides/retention-policies/', - }); - - this.add('RetentionPolicy_Precision', '0', { - type: 'select', - values: [ - { - key: '0', - i18nLabel: 'every_30_minutes', - }, { - key: '1', - i18nLabel: 'every_hour', - }, { - key: '2', - i18nLabel: 'every_six_hours', - }, { - key: '3', - i18nLabel: 'every_day', - }, - ], - public: true, - i18nLabel: 'RetentionPolicy_Precision', - i18nDescription: 'RetentionPolicy_Precision_Description', - enableQuery: [globalQuery, { - _id: 'RetentionPolicy_Advanced_Precision', - value: false, - }], - }); - - this.add('RetentionPolicy_Advanced_Precision', false, { - type: 'boolean', - public: true, - i18nLabel: 'RetentionPolicy_Advanced_Precision', - i18nDescription: 'RetentionPolicy_Advanced_Precision_Description', - enableQuery: globalQuery, - }); - - this.add('RetentionPolicy_Advanced_Precision_Cron', '*/30 * * * *', { - type: 'string', - public: true, - i18nLabel: 'RetentionPolicy_Advanced_Precision_Cron', - i18nDescription: 'RetentionPolicy_Advanced_Precision_Cron_Description', - enableQuery: [globalQuery, { _id: 'RetentionPolicy_Advanced_Precision', value: true }], - }); - - - this.section('Global Policy', function() { - this.add('RetentionPolicy_AppliesToChannels', false, { - type: 'boolean', - public: true, - i18nLabel: 'RetentionPolicy_AppliesToChannels', - enableQuery: globalQuery, - }); - this.add('RetentionPolicy_MaxAge_Channels', 30, { - type: 'int', - public: true, - i18nLabel: 'RetentionPolicy_MaxAge_Channels', - i18nDescription: 'RetentionPolicy_MaxAge_Description', - enableQuery: [{ - _id: 'RetentionPolicy_AppliesToChannels', - value: true, - }, globalQuery], - }); - - this.add('RetentionPolicy_AppliesToGroups', false, { - type: 'boolean', - public: true, - i18nLabel: 'RetentionPolicy_AppliesToGroups', - enableQuery: globalQuery, - }); - this.add('RetentionPolicy_MaxAge_Groups', 30, { - type: 'int', - public: true, - i18nLabel: 'RetentionPolicy_MaxAge_Groups', - i18nDescription: 'RetentionPolicy_MaxAge_Description', - enableQuery: [{ - _id: 'RetentionPolicy_AppliesToGroups', - value: true, - }, globalQuery], - }); - - this.add('RetentionPolicy_AppliesToDMs', false, { - type: 'boolean', - public: true, - i18nLabel: 'RetentionPolicy_AppliesToDMs', - enableQuery: globalQuery, - }); - - this.add('RetentionPolicy_MaxAge_DMs', 30, { - type: 'int', - public: true, - i18nLabel: 'RetentionPolicy_MaxAge_DMs', - i18nDescription: 'RetentionPolicy_MaxAge_Description', - enableQuery: [{ - _id: 'RetentionPolicy_AppliesToDMs', - value: true, - }, globalQuery], - }); - - this.add('RetentionPolicy_DoNotPrunePinned', false, { - type: 'boolean', - public: true, - i18nLabel: 'RetentionPolicy_DoNotPrunePinned', - enableQuery: globalQuery, - }); - - this.add('RetentionPolicy_FilesOnly', false, { - type: 'boolean', - public: true, - i18nLabel: 'RetentionPolicy_FilesOnly', - i18nDescription: 'RetentionPolicy_FilesOnly_Description', - enableQuery: globalQuery, - }); - }); -}); diff --git a/app/retention-policy/server/startup/settings.ts b/app/retention-policy/server/startup/settings.ts new file mode 100644 index 0000000000000..327c5e438fb7d --- /dev/null +++ b/app/retention-policy/server/startup/settings.ts @@ -0,0 +1,127 @@ +import { settingsRegistry } from '../../../settings/server'; + +settingsRegistry.addGroup('RetentionPolicy', function() { + const globalQuery = { + _id: 'RetentionPolicy_Enabled', + value: true, + }; + + this.add('RetentionPolicy_Enabled', false, { + type: 'boolean', + public: true, + i18nLabel: 'RetentionPolicy_Enabled', + alert: 'Watch out! Tweaking these settings without utmost care can destroy all message history. Please read the documentation before turning the feature on at rocket.chat/docs/administrator-guides/retention-policies/', + }); + + this.add('RetentionPolicy_Precision', '0', { + type: 'select', + values: [ + { + key: '0', + i18nLabel: 'every_30_minutes', + }, { + key: '1', + i18nLabel: 'every_hour', + }, { + key: '2', + i18nLabel: 'every_six_hours', + }, { + key: '3', + i18nLabel: 'every_day', + }, + ], + public: true, + i18nLabel: 'RetentionPolicy_Precision', + i18nDescription: 'RetentionPolicy_Precision_Description', + enableQuery: [globalQuery, { + _id: 'RetentionPolicy_Advanced_Precision', + value: false, + }], + }); + + this.add('RetentionPolicy_Advanced_Precision', false, { + type: 'boolean', + public: true, + i18nLabel: 'RetentionPolicy_Advanced_Precision', + i18nDescription: 'RetentionPolicy_Advanced_Precision_Description', + enableQuery: globalQuery, + }); + + this.add('RetentionPolicy_Advanced_Precision_Cron', '*/30 * * * *', { + type: 'string', + public: true, + i18nLabel: 'RetentionPolicy_Advanced_Precision_Cron', + i18nDescription: 'RetentionPolicy_Advanced_Precision_Cron_Description', + enableQuery: [globalQuery, { _id: 'RetentionPolicy_Advanced_Precision', value: true }], + }); + + + this.section('Global Policy', function() { + this.add('RetentionPolicy_AppliesToChannels', false, { + type: 'boolean', + public: true, + i18nLabel: 'RetentionPolicy_AppliesToChannels', + enableQuery: globalQuery, + }); + this.add('RetentionPolicy_MaxAge_Channels', 30, { + type: 'int', + public: true, + i18nLabel: 'RetentionPolicy_MaxAge_Channels', + i18nDescription: 'RetentionPolicy_MaxAge_Description', + enableQuery: [{ + _id: 'RetentionPolicy_AppliesToChannels', + value: true, + }, globalQuery], + }); + + this.add('RetentionPolicy_AppliesToGroups', false, { + type: 'boolean', + public: true, + i18nLabel: 'RetentionPolicy_AppliesToGroups', + enableQuery: globalQuery, + }); + this.add('RetentionPolicy_MaxAge_Groups', 30, { + type: 'int', + public: true, + i18nLabel: 'RetentionPolicy_MaxAge_Groups', + i18nDescription: 'RetentionPolicy_MaxAge_Description', + enableQuery: [{ + _id: 'RetentionPolicy_AppliesToGroups', + value: true, + }, globalQuery], + }); + + this.add('RetentionPolicy_AppliesToDMs', false, { + type: 'boolean', + public: true, + i18nLabel: 'RetentionPolicy_AppliesToDMs', + enableQuery: globalQuery, + }); + + this.add('RetentionPolicy_MaxAge_DMs', 30, { + type: 'int', + public: true, + i18nLabel: 'RetentionPolicy_MaxAge_DMs', + i18nDescription: 'RetentionPolicy_MaxAge_Description', + enableQuery: [{ + _id: 'RetentionPolicy_AppliesToDMs', + value: true, + }, globalQuery], + }); + + this.add('RetentionPolicy_DoNotPrunePinned', false, { + type: 'boolean', + public: true, + i18nLabel: 'RetentionPolicy_DoNotPrunePinned', + enableQuery: globalQuery, + }); + + this.add('RetentionPolicy_FilesOnly', false, { + type: 'boolean', + public: true, + i18nLabel: 'RetentionPolicy_FilesOnly', + i18nDescription: 'RetentionPolicy_FilesOnly_Description', + enableQuery: globalQuery, + }); + }); +}); diff --git a/app/search/client/provider/result.js b/app/search/client/provider/result.js index ff2cc972bf174..c5e71b6df3b79 100644 --- a/app/search/client/provider/result.js +++ b/app/search/client/provider/result.js @@ -11,7 +11,7 @@ import { MessageAction, RoomHistoryManager } from '../../../ui-utils'; import { messageArgs } from '../../../ui-utils/client/lib/messageArgs'; import { Rooms } from '../../../models/client'; import { getCommonRoomEvents } from '../../../ui/client/views/app/lib/getCommonRoomEvents'; -import { goToRoomById } from '../../../../client/lib/goToRoomById'; +import { goToRoomById } from '../../../../client/lib/utils/goToRoomById'; Meteor.startup(function() { MessageAction.addButton({ diff --git a/app/search/client/search/search.js b/app/search/client/search/search.js index a71cc3270ec19..f2688e33d2581 100644 --- a/app/search/client/search/search.js +++ b/app/search/client/search/search.js @@ -4,9 +4,10 @@ import { Session } from 'meteor/session'; import { Template } from 'meteor/templating'; import { ReactiveVar } from 'meteor/reactive-var'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import toastr from 'toastr'; import _ from 'underscore'; +import { dispatchToastMessage } from '../../../../client/lib/toast'; + Template.RocketSearch.onCreated(function() { this.provider = new ReactiveVar(); this.isActive = new ReactiveVar(false); @@ -32,7 +33,7 @@ Template.RocketSearch.onCreated(function() { Meteor.call('rocketchatSearch.search', this.scope.text.get(), { rid: Session.get('openedRoom'), uid: Meteor.userId() }, _p, (err, result) => { if (err) { - toastr.error(TAPi18n.__('Search_message_search_failed')); + dispatchToastMessage({ type: 'error', message: TAPi18n.__('Search_message_search_failed') }); this.scope.searching.set(false); } else { this.scope.searching.set(false); diff --git a/app/search/client/style/style.css b/app/search/client/style/style.css index 87dadc8c355f6..e7c5361afcc23 100644 --- a/app/search/client/style/style.css +++ b/app/search/client/style/style.css @@ -1,5 +1,4 @@ .rocket-search { - display: flex; flex: 1; diff --git a/app/search/server/logger/logger.js b/app/search/server/logger/logger.js index 86464e1c6acd3..bd0ddbf10f4da 100644 --- a/app/search/server/logger/logger.js +++ b/app/search/server/logger/logger.js @@ -1,4 +1,4 @@ import { Logger } from '../../../logger'; -const SearchLogger = new Logger('Search Logger', {}); +const SearchLogger = new Logger('Search Logger'); export default SearchLogger; diff --git a/app/search/server/search.internalService.ts b/app/search/server/search.internalService.ts index b41e988c2eefb..1e5efaa435698 100644 --- a/app/search/server/search.internalService.ts +++ b/app/search/server/search.internalService.ts @@ -1,5 +1,3 @@ -import _ from 'underscore'; - import { Users } from '../../models/server'; import { settings } from '../../settings/server'; import { searchProviderService } from './service/providerService'; @@ -38,10 +36,10 @@ class Search extends ServiceClass { const service = new Search(); -settings.get('Search.Provider', _.debounce(() => { +settings.watch('Search.Provider', () => { if (searchProviderService.activeProvider?.on) { api.registerService(service); } else { api.destroyService(service); } -}, 1000)); +}); diff --git a/app/search/server/service/providerService.js b/app/search/server/service/providerService.js index ac913f8cfb462..504c78067aa81 100644 --- a/app/search/server/service/providerService.js +++ b/app/search/server/service/providerService.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { validationService } from './validationService'; -import { settings } from '../../../settings'; +import { settings, settingsRegistry } from '../../../settings/server'; import SearchLogger from '../logger/logger'; class SearchProviderService { @@ -72,7 +72,7 @@ class SearchProviderService { const { providers } = this; // add settings for admininistration - settings.addGroup('Search', function() { + settingsRegistry.addGroup('Search', function() { const self = this; self.add('Search.Provider', 'defaultProvider', { @@ -114,7 +114,7 @@ class SearchProviderService { } }), 1000); - settings.get(/^Search\./, configProvider); + settings.watchByRegex(/^Search\./, configProvider); } } @@ -142,7 +142,7 @@ Meteor.methods({ throw new Error('Provider currently not active'); } - SearchLogger.debug('search: ', `\n\tText:${ text }\n\tContext:${ JSON.stringify(context) }\n\tPayload:${ JSON.stringify(payload) }`); + SearchLogger.debug({ msg: 'search', text, context, payload }); searchProviderService.activeProvider.search(text, context, payload, (error, data) => { if (error) { @@ -163,7 +163,7 @@ Meteor.methods({ try { if (!searchProviderService.activeProvider) { throw new Error('Provider currently not active'); } - SearchLogger.debug('suggest: ', `\n\tText:${ text }\n\tContext:${ JSON.stringify(context) }\n\tPayload:${ JSON.stringify(payload) }`); + SearchLogger.debug({ msg: 'suggest', text, context, payload }); searchProviderService.activeProvider.suggest(text, context, payload, (error, data) => { if (error) { diff --git a/app/search/server/service/validationService.js b/app/search/server/service/validationService.js index 633d88b7697eb..120207bd0e97d 100644 --- a/app/search/server/service/validationService.js +++ b/app/search/server/service/validationService.js @@ -1,42 +1,46 @@ import { Meteor } from 'meteor/meteor'; +import mem from 'mem'; import SearchLogger from '../logger/logger'; -import { Users } from '../../../models'; +import { canAccessRoom } from '../../../authorization/server'; +import { Users, Rooms } from '../../../models/server'; class ValidationService { validateSearchResult(result) { - const subscriptionCache = {}; + const getSubscription = mem((rid, uid) => { + if (!rid) { + return; + } - const getSubscription = (rid, uid) => { - if (!subscriptionCache.hasOwnProperty(rid)) { - subscriptionCache[rid] = Meteor.call('canAccessRoom', rid, uid); + const room = Rooms.findOneById(rid); + if (!room) { + return; } - return subscriptionCache[rid]; - }; + if (!canAccessRoom(room, { _id: uid })) { + return; + } - const userCache = {}; + return room; + }); - const getUsername = (uid) => { - if (!userCache.hasOwnProperty(uid)) { - try { - userCache[uid] = Users.findById(uid).fetch()[0].username; - } catch (e) { - userCache[uid] = undefined; - } + const getUser = mem((uid) => { + if (!uid) { + return; } - return userCache[uid]; - }; + return Users.findOneById(uid, { fields: { username: 1 } }); + }); const uid = Meteor.userId(); // get subscription for message if (result.message) { result.message.docs.forEach((msg) => { + const user = getUser(msg.user); const subscription = getSubscription(msg.rid, uid); if (subscription) { msg.r = { name: subscription.name, t: subscription.t }; - msg.username = getUsername(msg.user); + msg.username = user?.username; msg.valid = true; SearchLogger.debug(`user ${ uid } can access ${ msg.rid } ( ${ subscription.t === 'd' ? subscription.username : subscription.name } )`); } else { @@ -44,7 +48,7 @@ class ValidationService { } }); - result.message.docs.filter((msg) => msg.valid); + result.message.docs = result.message.docs.filter((msg) => msg.valid); } if (result.room) { @@ -60,7 +64,7 @@ class ValidationService { } }); - result.room.docs.filter((room) => room.valid); + result.room.docs = result.room.docs.filter((room) => room.valid); } return result; diff --git a/app/settings/client/lib/settings.ts b/app/settings/client/lib/settings.ts index c0e8e02191855..1c413cbc7848c 100644 --- a/app/settings/client/lib/settings.ts +++ b/app/settings/client/lib/settings.ts @@ -12,10 +12,13 @@ class Settings extends SettingsBase { dict = new ReactiveDict('settings'); - get(_id: string | RegExp): any { + get(_id: string | RegExp, ...args: []): any { if (_id instanceof RegExp) { throw new Error('RegExp Settings.get(RegExp)'); } + if (args.length > 0) { + throw new Error('settings.get(String, callback) only works on backend'); + } return this.dict.get(_id); } diff --git a/app/settings/lib/settings.ts b/app/settings/lib/settings.ts index 588250fb16a69..109ed5fbb6513 100644 --- a/app/settings/lib/settings.ts +++ b/app/settings/lib/settings.ts @@ -3,7 +3,7 @@ import _ from 'underscore'; import { SettingValue } from '../../../definition/ISetting'; -export type SettingComposedValue = {key: string; value: SettingValue}; +export type SettingComposedValue = {key: string; value: T}; export type SettingCallback = (key: string, value: SettingValue, initialLoad?: boolean) => void; interface ISettingRegexCallbacks { @@ -17,11 +17,15 @@ export class SettingsBase { private regexCallbacks = new Map(); // private ts = new Date() - public get(_id: RegExp, callback?: SettingCallback): SettingComposedValue[]; + public get(_id: RegExp, callback: SettingCallback): void; - public get(_id: string, callback?: SettingCallback): SettingValue | void; + public get(_id: string, callback: SettingCallback): void; - public get(_id: string | RegExp, callback?: SettingCallback): SettingValue | SettingComposedValue[] | void { + public get(_id: RegExp): SettingComposedValue[]; + + public get(_id: string): T | undefined; + + public get(_id: string | RegExp, callback?: SettingCallback): T | undefined | SettingComposedValue[] | void { if (callback != null) { this.onload(_id, callback); if (!Meteor.settings) { @@ -44,7 +48,11 @@ export class SettingsBase { } if (typeof _id === 'string') { - return Meteor.settings[_id] != null && callback(_id, Meteor.settings[_id]); + const value = Meteor.settings[_id]; + if (value != null) { + callback(_id, Meteor.settings[_id]); + } + return; } } @@ -53,7 +61,7 @@ export class SettingsBase { } if (_.isRegExp(_id)) { - return Object.keys(Meteor.settings).reduce((items: SettingComposedValue[], key) => { + return Object.keys(Meteor.settings).reduce((items: SettingComposedValue[], key) => { const value = Meteor.settings[key]; if (_id.test(key)) { items.push({ diff --git a/app/settings/server/CachedSettings.ts b/app/settings/server/CachedSettings.ts new file mode 100644 index 0000000000000..95665e3e53d9d --- /dev/null +++ b/app/settings/server/CachedSettings.ts @@ -0,0 +1,439 @@ +import { Emitter } from '@rocket.chat/emitter'; +import _ from 'underscore'; + +import { ISetting, SettingValue } from '../../../definition/ISetting'; +import { SystemLogger } from '../../../server/lib/logger/system'; + +const warn = process.env.NODE_ENV === 'development' || process.env.TEST_MODE; + +type SettingsConfig = { + debounce: number; +} + +type OverCustomSettingsConfig = Partial; + +export interface ICachedSettings { + /* + * @description: The settings object as ready + */ + initilized(): void; + + /* + * returns if the setting is defined + * @param _id - The setting id + * @returns {boolean} + */ + has(_id: ISetting['_id']): boolean; + + + /* + * Gets the current Object of the setting + * @param _id - The setting id + * @returns {ISetting} - The current Object of the setting + */ + getSetting(_id: ISetting['_id']): ISetting | undefined; + + /* + * Gets the current value of the setting + * @remarks + * - In development mode if you are trying to get the value of a setting that is not defined, it will give an warning, in theory it makes sense, there no reason to do that + * @param _id - The setting id + * @returns {SettingValue} - The current value of the setting + */ + get(_id: ISetting['_id']): T; + + /* + * Gets the current value of the setting + * @remarks + * - In development mode if you are trying to get the value of a setting that is not defined, it will give an warning, in theory it makes sense, there no reason to do that + * @param _id - The setting id + * @returns {SettingValue} - The current value of the setting + * + */ + /* @deprecated */ + getByRegexp(_id: RegExp): [string, T][]; + + /* + * Get the current value of the settings, and keep track of changes + * @remarks + * - This callback is debounced + * - The callback is not fire until the settings got initialized + * @param _ids - Array of setting id + * @param callback - The callback to run + * @returns {() => void} - A function that can be used to cancel the observe + */ + watchMultiple(_id: ISetting['_id'][], callback: (settings: T[]) => void): () => void; + + /* + * Get the current value of the setting, and keep track of changes + * @remarks + * - This callback is debounced + * - The callback is not fire until the settings got initialized + * @param _id - The setting id + * @param callback - The callback to run + * @returns {() => void} - A function that can be used to cancel the observe + */ + watch(_id: ISetting['_id'], cb: (args: T) => void, config?: OverCustomSettingsConfig): () => void; + + /* + * Get the current value of the setting, or wait until the initialized + * @remarks + * - This is a one time run + * - This callback is debounced + * - The callback is not fire until the settings got initialized + * @param _id - The setting id + * @param callback - The callback to run + * @returns {() => void} - A function that can be used to cancel the observe + */ + watchOnce(_id: ISetting['_id'], cb: (args: T) => void, config?: OverCustomSettingsConfig): () => void; + + + /* + * Observes the given setting by id and keep track of changes + * @remarks + * - This callback is debounced + * - The callback is not fire until the setting is changed + * - The callback is not fire until all the settings get initialized + * @param _id - The setting id + * @param callback - The callback to run + * @returns {() => void} - A function that can be used to cancel the observe + */ + change(_id: ISetting['_id'], callback: (args: T) => void, config?: OverCustomSettingsConfig): () => void; + + /* + * Observes multiple settings and keep track of changes + * @remarks + * - This callback is debounced + * - The callback is not fire until the setting is changed + * - The callback is not fire until all the settings get initialized + * @param _ids - Array of setting id + * @param callback - The callback to run + * @returns {() => void} - A function that can be used to cancel the observe + */ + changeMultiple(_ids: ISetting['_id'][], callback: (settings: T[]) => void, config?: OverCustomSettingsConfig): () => void; + + /* + * Observes the setting and fires only if there is a change. Runs only once + * @remarks + * - This is a one time run + * - This callback is debounced + * - The callback is not fire until the setting is changed + * - The callback is not fire until all the settings get initialized + * @param _id - The setting id + * @param callback - The callback to run + * @returns {() => void} - A function that can be used to cancel the observe + */ + changeOnce(_id: ISetting['_id'], callback: (args: T) => void, config?: OverCustomSettingsConfig): () => void; + + /* + * Sets the value of the setting + * @remarks + * - if the value set is the same as the current value, the change will not be fired + * - if the value is set before the initialization, the emit will be queued and will be fired after initialization + * @param _id - The setting id + * @param value - The value to set + * @returns {void} + */ + set(record: ISetting): void ; + + getConfig(config?: OverCustomSettingsConfig): SettingsConfig; + + /* @deprecated */ + watchByRegex(regex: RegExp, cb: (...args: [string, SettingValue]) => void, config?: OverCustomSettingsConfig): () => void; + + /* @deprecated */ + changeByRegex(regex: RegExp, callback: (...args: [string, SettingValue]) => void, config?: OverCustomSettingsConfig): () => void; + + /* + * @description: Wait until the settings get ready then run the callback + */ + onReady(cb: () => void): void; +} + + +/** + * Class responsible for setting up the settings, cache and propagation changes + * Should be agnostic to the actual settings implementation, running on meteor or standalone + * + * You should not instantiate this class directly, only for testing purposes + * + * @extends Emitter + * @alpha + */ +export class CachedSettings extends Emitter< +{ + '*': [string, SettingValue]; +} +& +{ + ready: undefined; + [k: string]: SettingValue; +}> implements ICachedSettings { + ready = false; + + store = new Map(); + + initilized(): void { + if (this.ready) { + return; + } + this.ready = true; + this.emit('ready'); + SystemLogger.debug('Settings initalized'); + } + + /* + * returns if the setting is defined + * @param _id - The setting id + * @returns {boolean} + */ + public has(_id: ISetting['_id']): boolean { + if (!this.ready && warn) { + SystemLogger.warn(`Settings not initialized yet. getting: ${ _id }`); + } + return this.store.has(_id); + } + + public getSetting(_id: ISetting['_id']): ISetting | undefined { + if (!this.ready && warn) { + SystemLogger.warn(`Settings not initialized yet. getting: ${ _id }`); + } + return this.store.get(_id); + } + + + /* + * Gets the current value of the setting + * @remarks + * - In development mode if you are trying to get the value of a setting that is not defined, it will give an warning, in theory it makes sense, there no reason to do that + * @param _id - The setting id + * @returns {SettingValue} - The current value of the setting + */ + public get(_id: ISetting['_id']): T { + if (!this.ready && warn) { + SystemLogger.warn(`Settings not initialized yet. getting: ${ _id }`); + } + return this.store.get(_id)?.value as T; + } + + /* + * Gets the current value of the setting + * @remarks + * - In development mode if you are trying to get the value of a setting that is not defined, it will give an warning, in theory it makes sense, there no reason to do that + * @param _id - The setting id + * @returns {SettingValue} - The current value of the setting + * + */ + /* @deprecated */ + public getByRegexp(_id: RegExp): [string, T][] { + if (!this.ready && warn) { + SystemLogger.warn(`Settings not initialized yet. getting: ${ _id }`); + } + + return [...this.store.entries()].filter(([key]) => _id.test(key)).map(([key, setting]) => [key, setting.value]) as [string, T][]; + } + + + /* + * Get the current value of the settings, and keep track of changes + * @remarks + * - This callback is debounced + * - The callback is not fire until the settings got initialized + * @param _ids - Array of setting id + * @param callback - The callback to run + * @returns {() => void} - A function that can be used to cancel the observe + */ + public watchMultiple(_id: ISetting['_id'][], callback: (settings: T[]) => void): () => void { + if (!this.ready) { + const cancel = new Set<() => void>(); + + cancel.add(this.once('ready', (): void => { + cancel.clear(); + cancel.add(this.watchMultiple(_id, callback)); + })); + return (): void => { + cancel.forEach((fn) => fn()); + }; + } + + if (_id.every((id) => this.store.has(id))) { + const settings = _id.map((id) => this.store.get(id)?.value); + callback(settings as T[]); + } + const mergeFunction = _.debounce((): void => { + callback(_id.map((id) => this.store.get(id)?.value) as T[]); + }, 100); + + const fns = _id.map((id) => this.on(id, mergeFunction)); + return (): void => { + fns.forEach((fn) => fn()); + }; + } + + /* + * Get the current value of the setting, and keep track of changes + * @remarks + * - This callback is debounced + * - The callback is not fire until the settings got initialized + * @param _id - The setting id + * @param callback - The callback to run + * @returns {() => void} - A function that can be used to cancel the observe + */ + public watch(_id: ISetting['_id'], cb: (args: T) => void, config?: OverCustomSettingsConfig): () => void { + if (!this.ready) { + const cancel = new Set<() => void>(); + cancel.add(this.once('ready', (): void => { + cancel.clear(); + cancel.add(this.watch(_id, cb, config)); + })); + return (): void => { + cancel.forEach((fn) => fn()); + }; + } + + this.store.has(_id) && cb(this.store.get(_id)?.value as T); + return this.change(_id, cb, config); + } + + /* + * Get the current value of the setting, or wait until the initialized + * @remarks + * - This is a one time run + * - This callback is debounced + * - The callback is not fire until the settings got initialized + * @param _id - The setting id + * @param callback - The callback to run + * @returns {() => void} - A function that can be used to cancel the observe + */ + public watchOnce(_id: ISetting['_id'], cb: (args: T) => void, config?: OverCustomSettingsConfig): () => void { + if (this.store.has(_id)) { + cb(this.store.get(_id)?.value as T); + return (): void => undefined; + } + return this.changeOnce(_id, cb, config); + } + + + /* + * Observes the given setting by id and keep track of changes + * @remarks + * - This callback is debounced + * - The callback is not fire until the setting is changed + * - The callback is not fire until all the settings get initialized + * @param _id - The setting id + * @param callback - The callback to run + * @returns {() => void} - A function that can be used to cancel the observe + */ + public change(_id: ISetting['_id'], callback: (args: T) => void, config?: OverCustomSettingsConfig): () => void { + const { debounce } = this.getConfig(config); + return this.on(_id, _.debounce(callback, debounce) as any); + } + + /* + * Observes multiple settings and keep track of changes + * @remarks + * - This callback is debounced + * - The callback is not fire until the setting is changed + * - The callback is not fire until all the settings get initialized + * @param _ids - Array of setting id + * @param callback - The callback to run + * @returns {() => void} - A function that can be used to cancel the observe + */ + public changeMultiple(_ids: ISetting['_id'][], callback: (settings: T[]) => void, config?: OverCustomSettingsConfig): () => void { + const fns = _ids.map((id) => this.change(id, (): void => { + callback(_ids.map((id) => this.store.get(id)?.value) as T[]); + }, config)); + return (): void => { + fns.forEach((fn) => fn()); + }; + } + + /* + * Observes the setting and fires only if there is a change. Runs only once + * @remarks + * - This is a one time run + * - This callback is debounced + * - The callback is not fire until the setting is changed + * - The callback is not fire until all the settings get initialized + * @param _id - The setting id + * @param callback - The callback to run + * @returns {() => void} - A function that can be used to cancel the observe + */ + public changeOnce(_id: ISetting['_id'], callback: (args: T) => void, config?: OverCustomSettingsConfig): () => void { + const { debounce } = this.getConfig(config); + return this.once(_id, _.debounce(callback, debounce) as any); + } + + /* + * Sets the value of the setting + * @remarks + * - if the value set is the same as the current value, the change will not be fired + * - if the value is set before the initialization, the emit will be queued and will be fired after initialization + * @param _id - The setting id + * @param value - The value to set + * @returns {void} + */ + public set(record: ISetting): void { + if (this.store.has(record._id) && this.store.get(record._id)?.value === record.value) { + return; + } + + this.store.set(record._id, record); + if (!this.ready) { + this.once('ready', () => { + this.emit(record._id, this.store.get(record._id)?.value); + this.emit('*', [record._id, this.store.get(record._id)?.value]); + }); + return; + } + this.emit(record._id, this.store.get(record._id)?.value); + this.emit('*', [record._id, this.store.get(record._id)?.value]); + } + + public getConfig = (config?: OverCustomSettingsConfig): SettingsConfig => ({ + debounce: 500, + ...config, + }) + + /* @deprecated */ + public watchByRegex(regex: RegExp, cb: (...args: [string, SettingValue]) => void, config?: OverCustomSettingsConfig): () => void { + if (!this.ready) { + const cancel = new Set<() => void>(); + cancel.add(this.once('ready', (): void => { + cancel.clear(); + cancel.add(this.watchByRegex(regex, cb, config)); + })); + return (): void => { + cancel.forEach((fn) => fn()); + }; + } + [...this.store.entries()].forEach(([key, setting]) => { + if (regex.test(key)) { + cb(key, setting.value); + } + }); + + return this.changeByRegex(regex, cb, config); + } + + /* @deprecated */ + public changeByRegex(regex: RegExp, callback: (...args: [string, SettingValue]) => void, config?: OverCustomSettingsConfig): () => void { + const store: Map void> = new Map(); + return this.on('*', ([_id, value]) => { + if (regex.test(_id)) { + const { debounce } = this.getConfig(config); + const cb = store.get(_id) || _.debounce(callback, debounce); + cb(_id, value); + store.set(_id, cb); + } + regex.lastIndex = 0; + }); + } + + public onReady(cb: () => void): void { + if (this.ready) { + return cb(); + } + this.once('ready', cb); + } +} diff --git a/app/settings/server/Middleware.ts b/app/settings/server/Middleware.ts new file mode 100644 index 0000000000000..6f523feb6fbcd --- /dev/null +++ b/app/settings/server/Middleware.ts @@ -0,0 +1,10 @@ +type Next any> = (...args: Parameters) => ReturnType + +type Middleware any> = (context: Parameters, next: Next) => ReturnType + +const pipe = any>(fn: T) => (...args: Parameters): ReturnType => fn(...args); + + +export const use = any>(fn: T, middleware: Middleware): T => function(this: unknown, ...context: Parameters): ReturnType { + return middleware(context, pipe(fn.bind(this))); +} as T; diff --git a/app/settings/server/SettingsRegistry.ts b/app/settings/server/SettingsRegistry.ts new file mode 100644 index 0000000000000..fa1f4ff3b9d85 --- /dev/null +++ b/app/settings/server/SettingsRegistry.ts @@ -0,0 +1,236 @@ +import { Emitter } from '@rocket.chat/emitter'; +import { isEqual } from 'underscore'; + +import type SettingsModel from '../../models/server/models/Settings'; +import { ISetting, ISettingGroup, isSettingEnterprise, SettingValue } from '../../../definition/ISetting'; +import { SystemLogger } from '../../../server/lib/logger/system'; +import { overwriteSetting } from './functions/overwriteSetting'; +import { overrideSetting } from './functions/overrideSetting'; +import { getSettingDefaults } from './functions/getSettingDefaults'; +import { validateSetting } from './functions/validateSetting'; +import type { ICachedSettings } from './CachedSettings'; + +export const blockedSettings = new Set(); +export const hiddenSettings = new Set(); +export const wizardRequiredSettings = new Set(); + +if (process.env.SETTINGS_BLOCKED) { + process.env.SETTINGS_BLOCKED.split(',').forEach((settingId) => blockedSettings.add(settingId.trim())); +} + +if (process.env.SETTINGS_HIDDEN) { + process.env.SETTINGS_HIDDEN.split(',').forEach((settingId) => hiddenSettings.add(settingId.trim())); +} + +if (process.env.SETTINGS_REQUIRED_ON_WIZARD) { + process.env.SETTINGS_REQUIRED_ON_WIZARD.split(',').forEach((settingId) => wizardRequiredSettings.add(settingId.trim())); +} + +const IS_DEVELOPMENT = process.env.NODE_ENV === 'development'; + +/* +* @deprecated +* please do not use event emitter to mutate values +*/ +export const SettingsEvents = new Emitter<{ + 'store-setting-value': [ISetting, { value: SettingValue }]; + 'fetch-settings': ISetting[]; + 'remove-setting-value': ISetting; +}>(); + + +const getGroupDefaults = (_id: string, options: ISettingAddGroupOptions = {}): ISettingGroup => ({ + _id, + i18nLabel: _id, + i18nDescription: `${ _id }_Description`, + ...options, + sorter: options.sorter || 0, + blocked: blockedSettings.has(_id), + hidden: hiddenSettings.has(_id), + type: 'group', + ...options.displayQuery && { displayQuery: JSON.stringify(options.displayQuery) }, +}); + +export type ISettingAddGroupOptions = Partial; + +type addSectionCallback = (this: { + add(id: string, value: SettingValue, options: ISettingAddOptions): void; + with(options: ISettingAddOptions, cb: addSectionCallback): void; +}) => void; + +type addGroupCallback = (this: { + add(id: string, value: SettingValue, options: ISettingAddOptions): void; + section(section: string, cb: addSectionCallback): void; + with(options: ISettingAddOptions, cb: addGroupCallback): void; +}) => void; + + +type ISettingAddOptions = Partial; + + +const compareSettingsIgnoringKeys = (keys: Array) => + (a: ISetting, b: ISetting): boolean => + [...new Set([...Object.keys(a), ...Object.keys(b)])] + .filter((key) => !keys.includes(key as keyof ISetting)) + .every((key) => isEqual(a[key as keyof ISetting], b[key as keyof ISetting])); + +const compareSettings = compareSettingsIgnoringKeys(['value', 'ts', 'createdAt', 'valueSource', 'packageValue', 'processEnvValue', '_updatedAt']); + +export class SettingsRegistry { + private model: typeof SettingsModel; + + private store: ICachedSettings; + + private _sorter: {[key: string]: number} = {}; + + constructor({ store, model }: { store: ICachedSettings; model: typeof SettingsModel }) { + this.store = store; + this.model = model; + } + + /* + * Add a setting + */ + add(_id: string, value: SettingValue, { sorter, section, group, ...options }: ISettingAddOptions = {}): void { + if (!_id || value == null) { + throw new Error('Invalid arguments'); + } + + const sorterKey = group && section ? `${ group }_${ section }` : group; + + if (sorterKey && this._sorter[sorterKey] == null) { + if (group && section) { + const currentGroupValue = this._sorter[group] ?? 0; + this._sorter[sorterKey] = currentGroupValue * 1000; + } + } + + if (sorterKey) { + this._sorter[sorterKey] = this._sorter[sorterKey] ?? -1; + } + + const settingFromCode = getSettingDefaults({ + _id, + type: 'string', + value, + sorter: sorter ?? (sorterKey?.length && this._sorter[sorterKey]++), + group, + section, + ...options, + }, blockedSettings, hiddenSettings, wizardRequiredSettings); + + if (isSettingEnterprise(settingFromCode) && !('invalidValue' in settingFromCode)) { + SystemLogger.error(`Enterprise setting ${ _id } is missing the invalidValue option`); + throw new Error(`Enterprise setting ${ _id } is missing the invalidValue option`); + } + + const settingStored = this.store.getSetting(_id); + const settingOverwritten = overwriteSetting(settingFromCode); + + try { + validateSetting(settingFromCode._id, settingFromCode.type, settingFromCode.value); + } catch (e) { + IS_DEVELOPMENT && SystemLogger.error(`Invalid setting code ${ _id }: ${ (e as Error).message }`); + } + + const isOverwritten = settingFromCode !== settingOverwritten; + + const { _id: _, ...settingProps } = settingOverwritten; + + if (settingStored && !compareSettings(settingStored, settingOverwritten)) { + const { value: _value, ...settingOverwrittenProps } = settingOverwritten; + + const overwrittenKeys = Object.keys(settingOverwritten); + const removedKeys = Object.keys(settingStored).filter((key) => !['_updatedAt'].includes(key) && !overwrittenKeys.includes(key)); + + this.model.upsert({ _id }, { + $set: { ...settingOverwrittenProps }, + ...removedKeys.length && { + $unset: removedKeys.reduce((unset, key) => ({ ...unset, [key]: 1 }), {}), + }, + }); + return; + } + + if (settingStored && isOverwritten) { + if (settingStored.value !== settingOverwritten.value) { + this.model.upsert({ _id }, settingProps); + } + return; + } + + if (settingStored) { + try { + validateSetting(settingFromCode._id, settingFromCode.type, settingStored?.value); + } catch (e) { + IS_DEVELOPMENT && SystemLogger.error(`Invalid setting stored ${ _id }: ${ (e as Error).message }`); + } + return; + } + + const settingOverwrittenDefault = overrideSetting(settingFromCode); + + const setting = isOverwritten ? settingOverwritten : settingOverwrittenDefault; + + this.model.insert(setting); // no need to emit unless we remove the oplog + + this.store.set(setting); + } + + /* + * Add a setting group + */ + addGroup(_id: string, cb: addGroupCallback): void; + + // eslint-disable-next-line no-dupe-class-members + addGroup(_id: string, groupOptions: ISettingAddGroupOptions | addGroupCallback = {}, cb?: addGroupCallback): void { + if (!_id || (groupOptions instanceof Function && cb)) { + throw new Error('Invalid arguments'); + } + + const callback = groupOptions instanceof Function ? groupOptions : cb; + + const options = groupOptions instanceof Function ? getGroupDefaults(_id, { sorter: this._sorter[_id] }) : getGroupDefaults(_id, { sorter: this._sorter[_id], ...groupOptions }); + + if (!this.store.has(_id)) { + options.ts = new Date(); + this.model.insert(options); + this.store.set(options as ISetting); + } + + if (!callback) { + return; + } + + const addWith = (preset: ISettingAddOptions) => (id: string, value: SettingValue, options: ISettingAddOptions = {}): void => { + const mergedOptions = { ...preset, ...options }; + this.add(id, value, mergedOptions); + }; + const sectionSetWith = (preset: ISettingAddOptions) => (options: ISettingAddOptions, cb: addSectionCallback): void => { + const mergedOptions = { ...preset, ...options }; + cb.call({ + add: addWith(mergedOptions), + with: sectionSetWith(mergedOptions), + }); + }; + const sectionWith = (preset: ISettingAddOptions) => (section: string, cb: addSectionCallback): void => { + const mergedOptions = { ...preset, section }; + cb.call({ + add: addWith(mergedOptions), + with: sectionSetWith(mergedOptions), + }); + }; + + const groupSetWith = (preset: ISettingAddOptions) => (options: ISettingAddOptions, cb: addGroupCallback): void => { + const mergedOptions = { ...preset, ...options }; + + cb.call({ + add: addWith(mergedOptions), + section: sectionWith(mergedOptions), + with: groupSetWith(mergedOptions), + }); + }; + + return groupSetWith({ group: _id })({}, callback); + } +} diff --git a/app/settings/server/functions/convertValue.ts b/app/settings/server/functions/convertValue.ts new file mode 100644 index 0000000000000..0b8782bb0213d --- /dev/null +++ b/app/settings/server/functions/convertValue.ts @@ -0,0 +1,14 @@ +import { ISetting, SettingValue } from '../../../../definition/ISetting'; + +export const convertValue = (value: 'true' | 'false' | string, type: ISetting['type']): SettingValue => { + if (value.toLowerCase() === 'true') { + return true; + } + if (value.toLowerCase() === 'false') { + return false; + } + if (type === 'int') { + return parseInt(value); + } + return value; +}; diff --git a/app/settings/server/functions/getSettingDefaults.tests.ts b/app/settings/server/functions/getSettingDefaults.tests.ts new file mode 100644 index 0000000000000..63581c43ce69d --- /dev/null +++ b/app/settings/server/functions/getSettingDefaults.tests.ts @@ -0,0 +1,96 @@ +import { expect } from 'chai'; + +import { getSettingDefaults } from './getSettingDefaults'; + +describe('getSettingDefaults', () => { + it('should return based on _id type value', () => { + const setting = getSettingDefaults({ _id: 'test', value: 'test', type: 'string' }); + + expect(setting).to.be.an('object'); + expect(setting).to.have.property('_id'); + + expect(setting).to.have.property('i18nDescription').to.be.equal('test_Description'); + + expect(setting).to.have.property('value').to.be.equal('test'); + expect(setting).to.have.property('packageValue').to.be.equal('test'); + + expect(setting).to.have.property('type').to.be.equal('string'); + + expect(setting).to.have.property('blocked').to.be.equal(false); + + + expect(setting).to.not.have.property('multiline'); + expect(setting).to.not.have.property('values'); + + + expect(setting).to.not.have.property('group'); + expect(setting).to.not.have.property('section'); + expect(setting).to.not.have.property('tab'); + }); + + it('should return a sorter value', () => { + const setting = getSettingDefaults({ _id: 'test', value: 'test', type: 'string', sorter: 1 }); + + expect(setting).to.be.an('object'); + expect(setting).to.have.property('_id'); + + expect(setting).to.have.property('sorter').to.be.equal(1); + }); + + it('should return a private setting', () => { + const setting = getSettingDefaults({ _id: 'test', value: 'test', type: 'string', public: false }); + + expect(setting).to.be.an('object'); + expect(setting).to.have.property('_id'); + + expect(setting).to.have.property('public').to.be.equal(false); + }); + + it('should return a public setting', () => { + const setting = getSettingDefaults({ _id: 'test', value: 'test', type: 'string', public: true }); + + expect(setting).to.be.an('object'); + expect(setting).to.have.property('_id'); + + expect(setting).to.have.property('public').to.be.equal(true); + }); + + it('should return a blocked setting', () => { + const setting = getSettingDefaults({ _id: 'test', value: 'test', type: 'string', blocked: true }); + + expect(setting).to.be.an('object'); + expect(setting).to.have.property('_id'); + + + expect(setting).to.have.property('blocked').to.be.equal(true); + }); + + it('should return a blocked setting set by env', () => { + const setting = getSettingDefaults({ _id: 'test', value: 'test', type: 'string' }, new Set(['test'])); + + expect(setting).to.be.an('object'); + expect(setting).to.have.property('_id'); + + expect(setting).to.have.property('blocked').to.be.equal(true); + }); + + + it('should return a package value', () => { + const setting = getSettingDefaults({ _id: 'test', value: true, type: 'string' }, new Set(['test'])); + + expect(setting).to.be.an('object'); + expect(setting).to.have.property('_id'); + + expect(setting).to.have.property('blocked').to.be.equal(true); + }); + + it('should not return undefined options', () => { + const setting = getSettingDefaults({ _id: 'test', value: true, type: 'string', section: undefined, group: undefined }, new Set(['test'])); + + expect(setting).to.be.an('object'); + expect(setting).to.have.property('_id'); + + expect(setting).to.not.have.property('section'); + expect(setting).to.not.have.property('group'); + }); +}); diff --git a/app/settings/server/functions/getSettingDefaults.ts b/app/settings/server/functions/getSettingDefaults.ts new file mode 100644 index 0000000000000..e17c97790b1e3 --- /dev/null +++ b/app/settings/server/functions/getSettingDefaults.ts @@ -0,0 +1,39 @@ +import { ISetting, ISettingColor, isSettingColor } from '../../../../definition/ISetting'; + +export const getSettingDefaults = ( + setting: Partial & Pick, + blockedSettings: Set = new Set(), + hiddenSettings: Set = new Set(), + wizardRequiredSettings: Set = new Set(), +): ISetting => { + const { _id, value, sorter, ...props } = setting; + + const options = Object.fromEntries(Object.entries(props).filter(([, value]) => value !== undefined)); + + return { + _id, + value, + packageValue: value, + valueSource: 'packageValue', + secret: false, + enterprise: false, + i18nDescription: `${ _id }_Description`, + autocomplete: true, + sorter: sorter || 0, + ts: new Date(), + createdAt: new Date(), + ...options, + ...options.enableQuery && { enableQuery: JSON.stringify(options.enableQuery) }, + i18nLabel: options.i18nLabel || _id, + hidden: options.hidden || hiddenSettings.has(_id), + blocked: options.blocked || blockedSettings.has(_id), + requiredOnWizard: options.requiredOnWizard || wizardRequiredSettings.has(_id), + type: options.type || 'string', + env: options.env || false, + public: options.public || false, + ...options.displayQuery && { displayQuery: JSON.stringify(options.displayQuery) }, + ...isSettingColor(setting as ISetting) && { + packageEditor: (setting as ISettingColor).editor, + }, + }; +}; diff --git a/app/settings/server/functions/overrideGenerator.tests.ts b/app/settings/server/functions/overrideGenerator.tests.ts new file mode 100644 index 0000000000000..776cf8ce2cf95 --- /dev/null +++ b/app/settings/server/functions/overrideGenerator.tests.ts @@ -0,0 +1,32 @@ +import { expect } from 'chai'; + +import { getSettingDefaults } from './getSettingDefaults'; +import { overrideGenerator } from './overrideGenerator'; + +describe('overrideGenerator', () => { + it('should return a new object with the new value', () => { + const overwrite = overrideGenerator(() => 'value'); + + const setting = getSettingDefaults({ _id: 'test', value: 'test', type: 'string' }); + const overwritten = overwrite(setting); + + + expect(overwritten).to.be.an('object'); + expect(overwritten).to.have.property('_id'); + + expect(setting).to.be.not.equal(overwritten); + + expect(overwritten).to.have.property('value').that.equals('value'); + expect(overwritten).to.have.property('valueSource').that.equals('processEnvValue'); + }); + + + it('should return the same object since the value didnt change', () => { + const overwrite = overrideGenerator(() => 'test'); + + const setting = getSettingDefaults({ _id: 'test', value: 'test', type: 'string' }); + const overwritten = overwrite(setting); + + expect(setting).to.be.equal(overwritten); + }); +}); diff --git a/app/settings/server/functions/overrideGenerator.ts b/app/settings/server/functions/overrideGenerator.ts new file mode 100644 index 0000000000000..c4875286ec809 --- /dev/null +++ b/app/settings/server/functions/overrideGenerator.ts @@ -0,0 +1,22 @@ +import { ISetting } from '../../../../definition/ISetting'; +import { convertValue } from './convertValue'; + +export const overrideGenerator = (fn: (key: string) => string | undefined) => (setting: ISetting): ISetting => { + const overwriteValue = fn(setting._id); + if (overwriteValue === null || overwriteValue === undefined) { + return setting; + } + + const value = convertValue(overwriteValue, setting.type); + + if (value === setting.value) { + return setting; + } + + return { + ...setting, + value, + processEnvValue: value, + valueSource: 'processEnvValue', + }; +}; diff --git a/app/settings/server/functions/overrideSetting.ts b/app/settings/server/functions/overrideSetting.ts new file mode 100644 index 0000000000000..d51b74e4e60bd --- /dev/null +++ b/app/settings/server/functions/overrideSetting.ts @@ -0,0 +1,3 @@ +import { overrideGenerator } from './overrideGenerator'; + +export const overrideSetting = overrideGenerator((key: string) => process.env[key]); diff --git a/app/settings/server/functions/overwriteSetting.ts b/app/settings/server/functions/overwriteSetting.ts new file mode 100644 index 0000000000000..990b55edd9d19 --- /dev/null +++ b/app/settings/server/functions/overwriteSetting.ts @@ -0,0 +1,3 @@ +import { overrideGenerator } from './overrideGenerator'; + +export const overwriteSetting = overrideGenerator((key: string) => process.env[`OVERWRITE_SETTING_${ key }`]); diff --git a/app/settings/server/functions/settings.mocks.ts b/app/settings/server/functions/settings.mocks.ts index d28383b142524..3e22e52bbe473 100644 --- a/app/settings/server/functions/settings.mocks.ts +++ b/app/settings/server/functions/settings.mocks.ts @@ -1,23 +1,51 @@ -import { Meteor } from 'meteor/meteor'; import mock from 'mock-require'; +import { ISetting } from '../../../../definition/ISetting'; +import { ICachedSettings } from '../CachedSettings'; + type Dictionary = { [index: string]: any; } class SettingsClass { + settings: ICachedSettings; + + find(): any[] { + return []; + } + + public data = new Map() public upsertCalls = 0; + public insertCalls = 0; + + private checkQueryMatch(key: string, data: Dictionary, queryValue: any): boolean { + if (typeof queryValue === 'object') { + if (queryValue.$exists !== undefined) { + return (data.hasOwnProperty(key) && data[key] !== undefined) === queryValue.$exists; + } + } + + return queryValue === data[key]; + } + findOne(query: Dictionary): any { - return [...this.data.values()].find((data) => Object.entries(query).every(([key, value]) => data[key] === value)); + return [...this.data.values()].find((data) => Object.entries(query).every(([key, value]) => this.checkQueryMatch(key, data, value))); + } + + insert(doc: any): void { + this.data.set(doc._id, doc); + // eslint-disable-next-line @typescript-eslint/no-var-requires + this.settings.set(doc); + this.insertCalls++; } upsert(query: any, update: any): void { const existent = this.findOne(query); - const data = { ...existent, ...query, ...update.$set }; + const data = { ...existent, ...query, ...update, ...update.$set }; if (!existent) { Object.assign(data, update.$setOnInsert); @@ -25,12 +53,10 @@ class SettingsClass { // console.log(query, data); this.data.set(query._id, data); - Meteor.settings[query._id] = data.value; // Can't import before the mock command on end of this file! // eslint-disable-next-line @typescript-eslint/no-var-requires - const { settings } = require('./settings'); - settings.load(query._id, data.value, !existent); + this.settings.set(data); this.upsertCalls++; } @@ -40,9 +66,7 @@ class SettingsClass { // Can't import before the mock command on end of this file! // eslint-disable-next-line @typescript-eslint/no-var-requires - const { settings } = require('./settings'); - Meteor.settings[id] = value; - settings.load(id, value, false); + this.settings.set(this.data.get(id) as ISetting); } } diff --git a/app/settings/server/functions/settings.tests.ts b/app/settings/server/functions/settings.tests.ts index 65eb7ae8324e2..55692c7dc6d55 100644 --- a/app/settings/server/functions/settings.tests.ts +++ b/app/settings/server/functions/settings.tests.ts @@ -1,24 +1,23 @@ /* eslint-disable @typescript-eslint/camelcase */ -/* eslint-env mocha */ -import { Meteor } from 'meteor/meteor'; -import chai, { expect } from 'chai'; -import spies from 'chai-spies'; +import { expect, spy } from 'chai'; import { Settings } from './settings.mocks'; -import { settings } from './settings'; - -chai.use(spies); +import { SettingsRegistry } from '../SettingsRegistry'; +import { CachedSettings } from '../CachedSettings'; describe('Settings', () => { beforeEach(() => { + Settings.insertCalls = 0; Settings.upsertCalls = 0; - Settings.data.clear(); - Meteor.settings = { public: {} }; process.env = {}; }); it('should not insert the same setting twice', () => { - settings.addGroup('group', function() { + const settings = new CachedSettings(); + Settings.settings = settings; + settings.initilized(); + const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); + settingsRegistry.addGroup('group', function() { this.section('section', function() { this.add('my_setting', true, { type: 'boolean', @@ -27,8 +26,9 @@ describe('Settings', () => { }); }); - expect(Settings.data.size).to.be.equal(2); - expect(Settings.upsertCalls).to.be.equal(2); + expect(Settings.upsertCalls).to.be.equal(0); + expect(Settings.insertCalls).to.be.equal(2); + expect(Settings.findOne({ _id: 'my_setting' })).to.be.include({ type: 'boolean', sorter: 0, @@ -45,7 +45,7 @@ describe('Settings', () => { autocomplete: true, }); - settings.addGroup('group', function() { + settingsRegistry.addGroup('group', function() { this.section('section', function() { this.add('my_setting', true, { type: 'boolean', @@ -54,11 +54,12 @@ describe('Settings', () => { }); }); - expect(Settings.data.size).to.be.equal(2); - expect(Settings.upsertCalls).to.be.equal(2); + expect(Settings.upsertCalls).to.be.equal(0); + expect(Settings.insertCalls).to.be.equal(2); + expect(Settings.findOne({ _id: 'my_setting' }).value).to.be.equal(true); - settings.addGroup('group', function() { + settingsRegistry.addGroup('group', function() { this.section('section', function() { this.add('my_setting2', false, { type: 'boolean', @@ -67,16 +68,22 @@ describe('Settings', () => { }); }); - expect(Settings.data.size).to.be.equal(3); - expect(Settings.upsertCalls).to.be.equal(3); + expect(Settings.upsertCalls).to.be.equal(0); + expect(Settings.insertCalls).to.be.equal(3); + expect(Settings.findOne({ _id: 'my_setting' }).value).to.be.equal(true); expect(Settings.findOne({ _id: 'my_setting2' }).value).to.be.equal(false); }); it('should respect override via environment as int', () => { + const settings = new CachedSettings(); + Settings.settings = settings; + settings.initilized(); + const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); + process.env.OVERWRITE_SETTING_my_setting = '1'; - settings.addGroup('group', function() { + settingsRegistry.addGroup('group', function() { this.section('section', function() { this.add('my_setting', 0, { type: 'int', @@ -102,13 +109,13 @@ describe('Settings', () => { autocomplete: true, }; - expect(Settings.data.size).to.be.equal(2); - expect(Settings.upsertCalls).to.be.equal(2); + expect(Settings).to.have.property('insertCalls').to.be.equal(2); + expect(Settings).to.have.property('upsertCalls').to.be.equal(0); expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); process.env.OVERWRITE_SETTING_my_setting = '2'; - settings.addGroup('group', function() { + settingsRegistry.addGroup('group', function() { this.section('section', function() { this.add('my_setting', 0, { type: 'int', @@ -117,18 +124,19 @@ describe('Settings', () => { }); }); - expectedSetting.value = 2; - expectedSetting.processEnvValue = 2; - - expect(Settings.data.size).to.be.equal(2); - expect(Settings.upsertCalls).to.be.equal(3); - expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); + expect(Settings).to.have.property('insertCalls').to.be.equal(2); + expect(Settings).to.have.property('upsertCalls').to.be.equal(1); + expect(Settings.findOne({ _id: 'my_setting' })).to.include({ ...expectedSetting, value: 2, processEnvValue: 2 }); }); it('should respect override via environment as boolean', () => { process.env.OVERWRITE_SETTING_my_setting_bool = 'true'; - settings.addGroup('group', function() { + const settings = new CachedSettings(); + Settings.settings = settings; + settings.initilized(); + const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); + settingsRegistry.addGroup('group', function() { this.section('section', function() { this.add('my_setting_bool', false, { type: 'boolean', @@ -154,33 +162,39 @@ describe('Settings', () => { autocomplete: true, }; - expect(Settings.data.size).to.be.equal(2); - expect(Settings.upsertCalls).to.be.equal(2); + expect(Settings.insertCalls).to.be.equal(2); + expect(Settings.upsertCalls).to.be.equal(0); expect(Settings.findOne({ _id: 'my_setting_bool' })).to.include(expectedSetting); process.env.OVERWRITE_SETTING_my_setting_bool = 'false'; - settings.addGroup('group', function() { + settingsRegistry.addGroup('group', function() { this.section('section', function() { - this.add('my_setting_bool', false, { + this.add('my_setting_bool', true, { type: 'boolean', sorter: 0, }); }); }); - expectedSetting.value = false; - expectedSetting.processEnvValue = false; - expect(Settings.data.size).to.be.equal(2); - expect(Settings.upsertCalls).to.be.equal(3); - expect(Settings.findOne({ _id: 'my_setting_bool' })).to.include(expectedSetting); + expect(Settings.insertCalls).to.be.equal(2); + expect(Settings.upsertCalls).to.be.equal(1); + expect(Settings.findOne({ _id: 'my_setting_bool' })).to.include({ + value: false, + processEnvValue: false, + packageValue: true, + }); }); it('should respect override via environment as string', () => { process.env.OVERWRITE_SETTING_my_setting_str = 'hey'; - settings.addGroup('group', function() { + const settings = new CachedSettings(); + Settings.settings = settings; + settings.initilized(); + const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); + settingsRegistry.addGroup('group', function() { this.section('section', function() { this.add('my_setting_str', '', { type: 'string', @@ -206,13 +220,13 @@ describe('Settings', () => { autocomplete: true, }; - expect(Settings.data.size).to.be.equal(2); - expect(Settings.upsertCalls).to.be.equal(2); + expect(Settings.insertCalls).to.be.equal(2); + expect(Settings.upsertCalls).to.be.equal(0); expect(Settings.findOne({ _id: 'my_setting_str' })).to.include(expectedSetting); process.env.OVERWRITE_SETTING_my_setting_str = 'hey ho'; - settings.addGroup('group', function() { + settingsRegistry.addGroup('group', function() { this.section('section', function() { this.add('my_setting_str', 'hey', { type: 'string', @@ -221,19 +235,23 @@ describe('Settings', () => { }); }); - expectedSetting.value = 'hey ho'; - expectedSetting.processEnvValue = 'hey ho'; - expectedSetting.packageValue = 'hey'; - - expect(Settings.data.size).to.be.equal(2); - expect(Settings.upsertCalls).to.be.equal(3); - expect(Settings.findOne({ _id: 'my_setting_str' })).to.include(expectedSetting); + expect(Settings.insertCalls).to.be.equal(2); + expect(Settings.upsertCalls).to.be.equal(1); + expect(Settings.findOne({ _id: 'my_setting_str' })).to.include({ ...expectedSetting, + value: 'hey ho', + processEnvValue: 'hey ho', + packageValue: 'hey', + }); }); it('should respect initial value via environment', () => { process.env.my_setting = '1'; + const settings = new CachedSettings(); + Settings.settings = settings; + settings.initilized(); + const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); - settings.addGroup('group', function() { + settingsRegistry.addGroup('group', function() { this.section('section', function() { this.add('my_setting', 0, { type: 'int', @@ -259,13 +277,11 @@ describe('Settings', () => { autocomplete: true, }; - expect(Settings.data.size).to.be.equal(2); - expect(Settings.upsertCalls).to.be.equal(2); + expect(Settings.insertCalls).to.be.equal(2); + expect(Settings.upsertCalls).to.be.equal(0); expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); - process.env.my_setting = '2'; - - settings.addGroup('group', function() { + settingsRegistry.addGroup('group', function() { this.section('section', function() { this.add('my_setting', 0, { type: 'int', @@ -274,160 +290,22 @@ describe('Settings', () => { }); }); - expectedSetting.processEnvValue = 2; - - expect(Settings.data.size).to.be.equal(2); - expect(Settings.upsertCalls).to.be.equal(3); - expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); + expect(Settings.insertCalls).to.be.equal(2); + expect(Settings.upsertCalls).to.be.equal(0); + expect(Settings.findOne({ _id: 'my_setting' })).to.include({ ...expectedSetting }); }); - it('should respect initial value via Meteor.settings', () => { - Meteor.settings.my_setting = 1; - - settings.addGroup('group', function() { - this.section('section', function() { - this.add('my_setting', 0, { - type: 'int', - sorter: 0, - }); - }); - }); - - const expectedSetting = { - value: 1, - meteorSettingsValue: 1, - valueSource: 'meteorSettingsValue', - type: 'int', - sorter: 0, - group: 'group', - section: 'section', - packageValue: 0, - hidden: false, - blocked: false, - secret: false, - i18nLabel: 'my_setting', - i18nDescription: 'my_setting_Description', - autocomplete: true, - }; - - expect(Settings.data.size).to.be.equal(2); - expect(Settings.upsertCalls).to.be.equal(2); - expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); - - Meteor.settings.my_setting = 2; - - settings.addGroup('group', function() { - this.section('section', function() { - this.add('my_setting', 0, { - type: 'int', - sorter: 0, - }); - }); - }); - expectedSetting.meteorSettingsValue = 2; + it('should call `settings.get` callback on setting added', (done) => { + const settings = new CachedSettings(); + Settings.settings = settings; + settings.initilized(); + const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); - expect(Settings.data.size).to.be.equal(2); - expect(Settings.upsertCalls).to.be.equal(3); - expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); - }); + const spiedCallback1 = spy(); + const spiedCallback2 = spy(); - it('should keep original value if value on code was changed', () => { - settings.addGroup('group', function() { - this.section('section', function() { - this.add('my_setting', 0, { - type: 'int', - sorter: 0, - }); - }); - }); - - const expectedSetting = { - value: 0, - valueSource: 'packageValue', - type: 'int', - sorter: 0, - group: 'group', - section: 'section', - packageValue: 0, - hidden: false, - blocked: false, - secret: false, - i18nLabel: 'my_setting', - i18nDescription: 'my_setting_Description', - autocomplete: true, - }; - - expect(Settings.data.size).to.be.equal(2); - expect(Settings.upsertCalls).to.be.equal(2); - expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); - - // Can't reset setting because the Meteor.setting will have the first value and will act to enforce his value - // settings.addGroup('group', function() { - // this.section('section', function() { - // this.add('my_setting', 1, { - // type: 'int', - // sorter: 0, - // }); - // }); - // }); - - // expectedSetting.packageValue = 1; - - // expect(Settings.data.size).to.be.equal(2); - // expect(Settings.upsertCalls).to.be.equal(3); - // expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); - }); - - it('should change group and section', () => { - settings.addGroup('group', function() { - this.section('section', function() { - this.add('my_setting', 0, { - type: 'int', - sorter: 0, - }); - }); - }); - - const expectedSetting = { - value: 0, - valueSource: 'packageValue', - type: 'int', - sorter: 0, - group: 'group', - section: 'section', - packageValue: 0, - hidden: false, - blocked: false, - secret: false, - i18nLabel: 'my_setting', - i18nDescription: 'my_setting_Description', - autocomplete: true, - }; - - expect(Settings.data.size).to.be.equal(2); - expect(Settings.upsertCalls).to.be.equal(2); - expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); - - settings.addGroup('group2', function() { - this.section('section2', function() { - this.add('my_setting', 0, { - type: 'int', - sorter: 0, - }); - }); - }); - - expectedSetting.group = 'group2'; - expectedSetting.section = 'section2'; - - expect(Settings.data.size).to.be.equal(3); - expect(Settings.upsertCalls).to.be.equal(4); - expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); - }); - - it('should call `settings.get` callback on setting added', () => { - settings.addGroup('group', function() { + settingsRegistry.addGroup('group', function() { this.section('section', function() { this.add('setting_callback', 'value1', { type: 'string', @@ -435,31 +313,45 @@ describe('Settings', () => { }); }); - const spy = chai.spy(); - settings.get('setting_callback', spy); - settings.get(/setting_callback/, spy); + settings.watch('setting_callback', spiedCallback1, { debounce: 10 }); + settings.watchByRegex(/setting_callback/, spiedCallback2, { debounce: 10 }); - expect(spy).to.have.been.called.exactly(2); - expect(spy).to.have.been.called.always.with('setting_callback', 'value1'); + setTimeout(() => { + 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.get` callback on setting changed', () => { - const spy = chai.spy(); - settings.get('setting_callback', spy); - settings.get(/setting_callback/, spy); + it('should call `settings.watch` callback on setting changed registering before initialized', (done) => { + const spiedCallback1 = spy(); + const spiedCallback2 = spy(); + const settings = new CachedSettings(); + Settings.settings = settings; + const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); - settings.addGroup('group', function() { + settings.watch('setting_callback', spiedCallback1, { debounce: 1 }); + settings.watchByRegex(/setting_callback/ig, spiedCallback2, { debounce: 1 }); + + settings.initilized(); + settingsRegistry.addGroup('group', function() { this.section('section', function() { this.add('setting_callback', 'value2', { type: 'string', }); }); }); - - settings.updateById('setting_callback', 'value3'); - - expect(spy).to.have.been.called.exactly(6); - expect(spy).to.have.been.called.with('setting_callback', 'value2'); - expect(spy).to.have.been.called.with('setting_callback', 'value3'); + setTimeout(() => { + Settings.updateValueById('setting_callback', 'value3'); + setTimeout(() => { + 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/settings.ts b/app/settings/server/functions/settings.ts index 72ad2a970f737..32dd0a0da8643 100644 --- a/app/settings/server/functions/settings.ts +++ b/app/settings/server/functions/settings.ts @@ -1,395 +1,14 @@ -import { EventEmitter } from 'events'; - -import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; - -import { SettingsBase } from '../../lib/settings'; import SettingsModel from '../../../models/server/models/Settings'; -import { updateValue } from '../raw'; -import { ISetting, SettingValue } from '../../../../definition/ISetting'; - -const blockedSettings = new Set(); -const hiddenSettings = new Set(); -const wizardRequiredSettings = new Set(); - -if (process.env.SETTINGS_BLOCKED) { - process.env.SETTINGS_BLOCKED.split(',').forEach((settingId) => blockedSettings.add(settingId.trim())); -} - -if (process.env.SETTINGS_HIDDEN) { - process.env.SETTINGS_HIDDEN.split(',').forEach((settingId) => hiddenSettings.add(settingId.trim())); -} - -if (process.env.SETTINGS_REQUIRED_ON_WIZARD) { - process.env.SETTINGS_REQUIRED_ON_WIZARD.split(',').forEach((settingId) => wizardRequiredSettings.add(settingId.trim())); -} - -export const SettingsEvents = new EventEmitter(); - -const overrideSetting = (_id: string, value: SettingValue, options: ISettingAddOptions): SettingValue => { - const envValue = process.env[_id]; - if (envValue) { - if (envValue.toLowerCase() === 'true') { - value = true; - } else if (envValue.toLowerCase() === 'false') { - value = false; - } else if (options.type === 'int') { - value = parseInt(envValue); - } else { - value = envValue; - } - options.processEnvValue = value; - options.valueSource = 'processEnvValue'; - } else if (Meteor.settings[_id] != null && Meteor.settings[_id] !== value) { - value = Meteor.settings[_id]; - options.meteorSettingsValue = value; - options.valueSource = 'meteorSettingsValue'; - } - - const overwriteValue = process.env[`OVERWRITE_SETTING_${ _id }`]; - if (overwriteValue) { - if (overwriteValue.toLowerCase() === 'true') { - value = true; - } else if (overwriteValue.toLowerCase() === 'false') { - value = false; - } else if (options.type === 'int') { - value = parseInt(overwriteValue); - } else { - value = overwriteValue; - } - options.value = value; - options.processEnvValue = value; - options.valueSource = 'processEnvValue'; - } - - return value; -}; - -export interface ISettingAddOptions extends Partial { - force?: boolean; -} - -export interface ISettingAddGroupOptions { - hidden?: boolean; - blocked?: boolean; - ts?: Date; - i18nLabel?: string; - i18nDescription?: string; -} - - -interface IUpdateOperator { - $set: ISettingAddOptions; - $setOnInsert: ISettingAddOptions & { - createdAt: Date; - }; - $unset?: { - section?: 1; - }; -} - -type QueryExpression = { - $exists: boolean; -} - -type Query = { - [P in keyof T]?: T[P] | QueryExpression; -} - -type addSectionCallback = (this: { - add(id: string, value: SettingValue, options: ISettingAddOptions): void; -}) => void; - -type addGroupCallback = (this: { - add(id: string, value: SettingValue, options: ISettingAddOptions): void; - section(section: string, cb: addSectionCallback): void; -}) => void; - -class Settings extends SettingsBase { - private afterInitialLoad: Array<(settings: Meteor.Settings) => void> = []; - - private _sorter: {[key: string]: number} = {}; - - private initialLoad = false; - - /* - * Add a setting - */ - add(_id: string, value: SettingValue, { editor, ...options }: ISettingAddOptions = {}): boolean { - if (!_id || value == null) { - return false; - } - if (options.group && this._sorter[options.group] == null) { - this._sorter[options.group] = 0; - } - options.packageValue = value; - options.valueSource = 'packageValue'; - options.hidden = options.hidden || false; - options.blocked = options.blocked || false; - options.requiredOnWizard = options.requiredOnWizard || false; - options.secret = options.secret || false; - options.enterprise = options.enterprise || false; - - if (options.enterprise && !('invalidValue' in options)) { - console.error(`Enterprise setting ${ _id } is missing the invalidValue option`); - throw new Error(`Enterprise setting ${ _id } is missing the invalidValue option`); - } - - if (options.group && options.sorter == null) { - options.sorter = this._sorter[options.group]++; - } - if (options.enableQuery != null) { - options.enableQuery = JSON.stringify(options.enableQuery); - } - if (options.i18nLabel == null) { - options.i18nLabel = _id; - } - if (options.i18nDescription == null) { - options.i18nDescription = `${ _id }_Description`; - } - if (blockedSettings.has(_id)) { - options.blocked = true; - } - if (hiddenSettings.has(_id)) { - options.hidden = true; - } - if (wizardRequiredSettings.has(_id)) { - options.requiredOnWizard = true; - } - if (options.autocomplete == null) { - options.autocomplete = true; - } - - value = overrideSetting(_id, value, options); - - const updateOperations: IUpdateOperator = { - $set: options, - $setOnInsert: { - createdAt: new Date(), - }, - }; - if (editor != null) { - updateOperations.$setOnInsert.editor = editor; - updateOperations.$setOnInsert.packageEditor = editor; - } - - if (options.value == null) { - if (options.force === true) { - updateOperations.$set.value = options.packageValue; - } else { - updateOperations.$setOnInsert.value = value; - } - } - - const query: Query = { - _id, - ...updateOperations.$set, - }; - - if (options.section == null) { - updateOperations.$unset = { - section: 1, - }; - query.section = { - $exists: false, - }; - } - - const existentSetting = SettingsModel.findOne(query); - if (existentSetting) { - if (existentSetting.editor || !updateOperations.$setOnInsert.editor) { - return true; - } - - updateOperations.$set.editor = updateOperations.$setOnInsert.editor; - delete updateOperations.$setOnInsert.editor; - } - - updateOperations.$set.ts = new Date(); - - SettingsModel.upsert({ - _id, - }, updateOperations); - - const record = { - _id, - value, - type: options.type || 'string', - env: options.env || false, - i18nLabel: options.i18nLabel, - public: options.public || false, - packageValue: options.packageValue, - blocked: options.blocked, - }; - - this.storeSettingValue(record, this.initialLoad); - - return true; - } - - /* - * Add a setting group - */ - addGroup(_id: string, cb?: addGroupCallback): boolean; - - // eslint-disable-next-line no-dupe-class-members - addGroup(_id: string, options: ISettingAddGroupOptions | addGroupCallback = {}, cb?: addGroupCallback): boolean { - if (!_id) { - return false; - } - if (_.isFunction(options)) { - cb = options; - options = {}; - } - if (options.i18nLabel == null) { - options.i18nLabel = _id; - } - if (options.i18nDescription == null) { - options.i18nDescription = `${ _id }_Description`; - } - - options.blocked = false; - options.hidden = false; - if (blockedSettings.has(_id)) { - options.blocked = true; - } - if (hiddenSettings.has(_id)) { - options.hidden = true; - } - - const existentGroup = SettingsModel.findOne({ - _id, - type: 'group', - ...options, - }); - - if (!existentGroup) { - options.ts = new Date(); - - SettingsModel.upsert({ - _id, - }, { - $set: options, - $setOnInsert: { - type: 'group', - createdAt: new Date(), - }, - }); - } - - if (cb != null) { - cb.call({ - add: (id: string, value: SettingValue, options: ISettingAddOptions = {}) => { - options.group = _id; - return this.add(id, value, options); - }, - section: (section: string, cb: addSectionCallback) => cb.call({ - add: (id: string, value: SettingValue, options: ISettingAddOptions = {}) => { - options.group = _id; - options.section = section; - return this.add(id, value, options); - }, - }), - }); - } - return true; - } - - /* - * Remove a setting by id - */ - removeById(_id: string): boolean { - if (!_id) { - return false; - } - return SettingsModel.removeById(_id); - } - - /* - * Update a setting by id - */ - updateById(_id: string, value: SettingValue, editor?: string): boolean { - if (!_id || value == null) { - return false; - } - if (editor != null) { - return SettingsModel.updateValueAndEditorById(_id, value, editor); - } - return SettingsModel.updateValueById(_id, value); - } - - /* - * Update options of a setting by id - */ - updateOptionsById(_id: string, options: ISettingAddOptions): boolean { - if (!_id || options == null) { - return false; - } - - return SettingsModel.updateOptionsById(_id, options); - } - - /* - * Update a setting by id - */ - clearById(_id: string): boolean { - if (_id == null) { - return false; - } - return SettingsModel.updateValueById(_id, undefined); - } - - /* - * Change a setting value on the Meteor.settings object - */ - storeSettingValue(record: ISetting, initialLoad: boolean): void { - const newData = { - value: record.value, - }; - SettingsEvents.emit('store-setting-value', record, newData); - const { value } = newData; - - Meteor.settings[record._id] = value; - if (record.env === true) { - process.env[record._id] = String(value); - } - - this.load(record._id, value, initialLoad); - } - - /* - * Remove a setting value on the Meteor.settings object - */ - removeSettingValue(record: ISetting, initialLoad: boolean): void { - SettingsEvents.emit('remove-setting-value', record); - - delete Meteor.settings[record._id]; - if (record.env === true) { - delete process.env[record._id]; - } +import { CachedSettings } from '../CachedSettings'; +import { SettingsRegistry } from '../SettingsRegistry'; +import { ISetting } from '../../../../definition/ISetting'; - this.load(record._id, undefined, initialLoad); - } - /* - * Update a setting by id - */ - init(): void { - this.initialLoad = true; - SettingsModel.find().fetch().forEach((record: ISetting) => { - this.storeSettingValue(record, this.initialLoad); - updateValue(record._id, { value: record.value }); - }); - this.initialLoad = false; - this.afterInitialLoad.forEach((fn) => fn(Meteor.settings)); - } +export const settings = new CachedSettings(); +SettingsModel.find().forEach((record: ISetting) => { + settings.set(record); +}); - onAfterInitialLoad(fn: (settings: Meteor.Settings) => void): void { - this.afterInitialLoad.push(fn); - if (this.initialLoad === false) { - fn(Meteor.settings); - } - } -} +settings.initilized(); -export const settings = new Settings(); +export const settingsRegistry = new SettingsRegistry({ store: settings, model: SettingsModel }); diff --git a/app/settings/server/functions/validateSetting.ts b/app/settings/server/functions/validateSetting.ts new file mode 100644 index 0000000000000..10667d00ece14 --- /dev/null +++ b/app/settings/server/functions/validateSetting.ts @@ -0,0 +1,57 @@ + + +import { ISetting } from '../../../../definition/ISetting'; + +export const validateSetting = (_id: T['_id'], type: T['type'], value: T['value'] | unknown): boolean => { + switch (type) { + case 'asset': + if (typeof value !== 'object') { + throw new Error(`Setting ${ _id } is of type ${ type } but got ${ typeof value }`); + } + break; + case 'string': + case 'relativeUrl': + case 'password': + case 'language': + case 'color': + case 'font': + case 'code': + case 'action': + case 'roomPick': + case 'group': + if (typeof value !== 'string') { + throw new Error(`Setting ${ _id } is of type ${ type } but got ${ typeof value }`); + } + break; + case 'boolean': + if (typeof value !== 'boolean') { + throw new Error(`Setting ${ _id } is of type boolean but got ${ typeof value }`); + } + break; + case 'int': + if (typeof value !== 'number') { + throw new Error(`Setting ${ _id } is of type int but got ${ typeof value }`); + } + break; + case 'multiSelect': + if (!Array.isArray(value)) { + throw new Error(`Setting ${ _id } is of type array but got ${ typeof value }`); + } + break; + case 'select': + + if (typeof value !== 'string' && typeof value !== 'number') { + throw new Error(`Setting ${ _id } is of type select but got ${ typeof value }`); + } + break; + case 'date': + if (!(value instanceof Date)) { + throw new Error(`Setting ${ _id } is of type date but got ${ typeof value }`); + } + break; + default: + return true; + } + + return true; +}; diff --git a/app/settings/server/functions/validateSettings.tests.ts b/app/settings/server/functions/validateSettings.tests.ts new file mode 100644 index 0000000000000..ba7d28f793f89 --- /dev/null +++ b/app/settings/server/functions/validateSettings.tests.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { expect } from 'chai'; + +import { validateSetting } from './validateSetting'; + +describe('validateSettings', () => { + it('should validate the type string', () => { + expect(() => validateSetting('test', 'string', 'value')).to.not.throw(); + }); + it('should throw an error expecting string receiving int', () => { + expect(() => validateSetting('test', 'string', 10)).to.throw(); + }); + it('should validate the type int', () => { + expect(() => validateSetting('test', 'int', 10)).to.not.throw(); + }); + it('should throw an error expecting int receiving string', () => { + expect(() => validateSetting('test', 'int', '10')).to.throw(); + }); + it('should validate the type boolean', () => { + expect(() => validateSetting('test', 'boolean', true)).to.not.throw(); + }); + it('should throw an error expecting boolean receiving string', () => { + expect(() => validateSetting('test', 'boolean', 'true')).to.throw(); + }); + it('should validate the type date', () => { + expect(() => validateSetting('test', 'date', new Date())).to.not.throw(); + }); + it('should throw an error expecting date receiving string', () => { + expect(() => validateSetting('test', 'date', '2019-01-01')).to.throw(); + }); + it('should validate the type multiSelect', () => { + expect(() => validateSetting('test', 'multiSelect', [])).to.not.throw(); + }); + it('should throw an error expecting multiSelect receiving string', () => { + expect(() => validateSetting('test', 'multiSelect', '[]')).to.throw(); + }); +}); diff --git a/app/settings/server/index.ts b/app/settings/server/index.ts index 3adfad5409b3b..f1a68d2790d07 100644 --- a/app/settings/server/index.ts +++ b/app/settings/server/index.ts @@ -1,6 +1,8 @@ -import { settings, SettingsEvents } from './functions/settings'; +import { settings, settingsRegistry } from './functions/settings'; +import { SettingsEvents } from './SettingsRegistry'; export { settings, + settingsRegistry, SettingsEvents, }; diff --git a/app/settings/server/raw.tests.js b/app/settings/server/raw.tests.js index 7a9d6bccd6cf9..ecd1aeada491f 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/settings/server/startup.ts b/app/settings/server/startup.ts new file mode 100644 index 0000000000000..4809dd9e83337 --- /dev/null +++ b/app/settings/server/startup.ts @@ -0,0 +1,60 @@ +import { Meteor } from 'meteor/meteor'; + +import { use } from './Middleware'; +import { settings } from './functions/settings'; + + +const getProcessingTimeInMS = (time: [number, number]): number => time[0] * 1000 + time[1] / 1e6; + +settings.watch = use(settings.watch, (context, next) => { + const [_id, callback, ...args] = context; + return next(_id, Meteor.bindEnvironment(callback), ...args); +}); + +if (process.env.DEBUG_SETTINGS === 'true') { + settings.watch = use(settings.watch, function watch(context, next) { + const [_id, callback, options] = context; + return next(_id, (...args) => { + const start = process.hrtime(); + callback(...args); + const elapsed = process.hrtime(start); + console.log(`settings.watch: ${ _id } ${ getProcessingTimeInMS(elapsed) }ms`); + }, options); + }); +} +settings.watchMultiple = use(settings.watchMultiple, (context, next) => { + const [_id, callback, ...args] = context; + return next(_id, Meteor.bindEnvironment(callback), ...args); +}); +settings.watchOnce = use(settings.watchOnce, (context, next) => { + const [_id, callback, ...args] = context; + return next(_id, Meteor.bindEnvironment(callback), ...args); +}); + +settings.watchByRegex = use(settings.watchByRegex, (context, next) => { + const [_id, callback, ...args] = context; + return next(_id, Meteor.bindEnvironment(callback), ...args); +}); + +settings.change = use(settings.change, (context, next) => { + const [_id, callback, ...args] = context; + return next(_id, Meteor.bindEnvironment(callback), ...args); +}); +settings.changeMultiple = use(settings.changeMultiple, (context, next) => { + const [_id, callback, ...args] = context; + return next(_id, Meteor.bindEnvironment(callback), ...args); +}); +settings.changeOnce = use(settings.changeOnce, (context, next) => { + const [_id, callback, ...args] = context; + return next(_id, Meteor.bindEnvironment(callback), ...args); +}); + +settings.changeByRegex = use(settings.changeByRegex, (context, next) => { + const [_id, callback, ...args] = context; + return next(_id, Meteor.bindEnvironment(callback), ...args); +}); + +settings.onReady = use(settings.onReady, (context, next) => { + const [callback, ...args] = context; + return next(Meteor.bindEnvironment(callback), ...args); +}); diff --git a/app/slackbridge/server/RocketAdapter.js b/app/slackbridge/server/RocketAdapter.js index e84efba8d201e..db674910be447 100644 --- a/app/slackbridge/server/RocketAdapter.js +++ b/app/slackbridge/server/RocketAdapter.js @@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; import { Random } from 'meteor/random'; -import { logger } from './logger'; +import { rocketLogger } from './logger'; import { callbacks } from '../../callbacks'; import { settings } from '../../settings'; import { Messages, Rooms, Users } from '../../models'; @@ -13,7 +13,7 @@ import { createRoom, sendMessage, setUserAvatar } from '../../lib'; export default class RocketAdapter { constructor(slackBridge) { - logger.rocket.debug('constructor'); + rocketLogger.debug('constructor'); this.slackBridge = slackBridge; this.util = util; this.userTags = {}; @@ -39,7 +39,7 @@ export default class RocketAdapter { } registerForEvents() { - logger.rocket.debug('Register for events'); + rocketLogger.debug('Register for events'); callbacks.add('afterSaveMessage', this.onMessage.bind(this), callbacks.priority.LOW, 'SlackBridge_Out'); callbacks.add('afterDeleteMessage', this.onMessageDelete.bind(this), callbacks.priority.LOW, 'SlackBridge_Delete'); callbacks.add('setReaction', this.onSetReaction.bind(this), callbacks.priority.LOW, 'SlackBridge_SetReaction'); @@ -47,7 +47,7 @@ export default class RocketAdapter { } unregisterForEvents() { - logger.rocket.debug('Unregister for events'); + rocketLogger.debug('Unregister for events'); callbacks.remove('afterSaveMessage', 'SlackBridge_Out'); callbacks.remove('afterDeleteMessage', 'SlackBridge_Delete'); callbacks.remove('setReaction', 'SlackBridge_SetReaction'); @@ -61,10 +61,10 @@ export default class RocketAdapter { // This is on a channel that the rocket bot is not subscribed on this slack server return; } - logger.rocket.debug('onRocketMessageDelete', rocketMessageDeleted); + rocketLogger.debug('onRocketMessageDelete', rocketMessageDeleted); slack.postDeleteMessage(rocketMessageDeleted); } catch (err) { - logger.rocket.error('Unhandled error onMessageDelete', err); + rocketLogger.error('Unhandled error onMessageDelete', err); } }); } @@ -75,7 +75,7 @@ export default class RocketAdapter { return; } - logger.rocket.debug('onRocketSetReaction'); + rocketLogger.debug('onRocketSetReaction'); if (rocketMsgID && reaction) { if (this.slackBridge.reactionsMap.delete(`set${ rocketMsgID }${ reaction }`)) { @@ -94,7 +94,7 @@ export default class RocketAdapter { } } } catch (err) { - logger.rocket.error('Unhandled error onSetReaction', err); + rocketLogger.error('Unhandled error onSetReaction', err); } } @@ -104,7 +104,7 @@ export default class RocketAdapter { return; } - logger.rocket.debug('onRocketUnSetReaction'); + rocketLogger.debug('onRocketUnSetReaction'); if (rocketMsgID && reaction) { if (this.slackBridge.reactionsMap.delete(`unset${ rocketMsgID }${ reaction }`)) { @@ -124,7 +124,7 @@ export default class RocketAdapter { } } } catch (err) { - logger.rocket.error('Unhandled error onUnSetReaction', err); + rocketLogger.error('Unhandled error onUnSetReaction', err); } } @@ -135,7 +135,7 @@ export default class RocketAdapter { // This is on a channel that the rocket bot is not subscribed return; } - logger.rocket.debug('onRocketMessage', rocketMessage); + rocketLogger.debug('onRocketMessage', rocketMessage); if (rocketMessage.editedAt) { // This is an Edit Event @@ -154,7 +154,7 @@ export default class RocketAdapter { // A new message from Rocket.Chat this.processSendMessage(rocketMessage, slack); } catch (err) { - logger.rocket.error('Unhandled error onMessage', err); + rocketLogger.error('Unhandled error onMessage', err); } }); @@ -168,7 +168,7 @@ export default class RocketAdapter { } else { // They want to limit to certain groups const outSlackChannels = _.pluck(settings.get('SlackBridge_Out_Channels'), '_id') || []; - // logger.rocket.debug('Out SlackChannels: ', outSlackChannels); + // rocketLogger.debug('Out SlackChannels: ', outSlackChannels); if (outSlackChannels.indexOf(rocketMessage.rid) !== -1) { slack.postMessage(slack.getSlackChannel(rocketMessage.rid), rocketMessage); } @@ -260,7 +260,7 @@ export default class RocketAdapter { } addChannel(slackChannelID, hasRetried = false) { - logger.rocket.debug('Adding Rocket.Chat channel from Slack', slackChannelID); + rocketLogger.debug('Adding Rocket.Chat channel from Slack', slackChannelID); let addedRoom; this.slackAdapters.forEach((slack) => { @@ -272,7 +272,7 @@ export default class RocketAdapter { if (slackChannel) { const members = slack.slackAPI.getMembers(slackChannelID); if (!members) { - logger.rocket.error('Could not fetch room members'); + rocketLogger.error('Could not fetch room members'); return; } @@ -286,7 +286,7 @@ export default class RocketAdapter { const rocketUserCreator = this.getRocketUserCreator(slackChannel); if (!rocketUserCreator) { - logger.rocket.error('Could not fetch room creator information', slackChannel.creator); + rocketLogger.error('Could not fetch room creator information', slackChannel.creator); return; } @@ -296,12 +296,12 @@ export default class RocketAdapter { rocketChannel.rocketId = rocketChannel.rid; } catch (e) { if (!hasRetried) { - logger.rocket.debug('Error adding channel from Slack. Will retry in 1s.', e.message); + rocketLogger.debug('Error adding channel from Slack. Will retry in 1s.', e.message); // If first time trying to create channel fails, could be because of multiple messages received at the same time. Try again once after 1s. Meteor._sleepForMs(1000); return this.findChannel(slackChannelID) || this.addChannel(slackChannelID, true); } - console.log(e.message); + rocketLogger.error(e.message); } const roomUpdate = { @@ -327,7 +327,7 @@ export default class RocketAdapter { }); if (!addedRoom) { - logger.rocket.debug('Channel not added'); + rocketLogger.debug('Channel not added'); } return addedRoom; } @@ -341,7 +341,7 @@ export default class RocketAdapter { } addUser(slackUserID) { - logger.rocket.debug('Adding Rocket.Chat user from Slack', slackUserID); + rocketLogger.debug('Adding Rocket.Chat user from Slack', slackUserID); let addedUser; this.slackAdapters.forEach((slack) => { if (addedUser) { @@ -408,7 +408,7 @@ export default class RocketAdapter { try { setUserAvatar(user, url, null, 'url'); } catch (error) { - logger.rocket.debug('Error setting user avatar', error.message); + rocketLogger.debug('Error setting user avatar', error.message); } } } @@ -426,7 +426,7 @@ export default class RocketAdapter { }); if (!addedUser) { - logger.rocket.debug('User not added'); + rocketLogger.debug('User not added'); } return addedUser; @@ -495,7 +495,7 @@ export default class RocketAdapter { } }, 500); } else { - logger.rocket.debug('Send message to Rocket.Chat'); + rocketLogger.debug('Send message to Rocket.Chat'); sendMessage(rocketUser, rocketMsgObj, rocketChannel, true); } } diff --git a/app/slackbridge/server/SlackAdapter.js b/app/slackbridge/server/SlackAdapter.js index c61092f7cde57..1cb12c2bdacca 100644 --- a/app/slackbridge/server/SlackAdapter.js +++ b/app/slackbridge/server/SlackAdapter.js @@ -5,7 +5,7 @@ import https from 'https'; import { RTMClient } from '@slack/client'; import { Meteor } from 'meteor/meteor'; -import { logger } from './logger'; +import { slackLogger } from './logger'; import { SlackAPI } from './SlackAPI'; import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; import { Messages, Rooms, Users } from '../../models'; @@ -24,7 +24,7 @@ import { FileUpload } from '../../file-upload'; export default class SlackAdapter { constructor(slackBridge) { - logger.slack.debug('constructor'); + slackLogger.debug('constructor'); this.slackBridge = slackBridge; this.rtm = {}; // slack-client Real Time Messaging API this.apiToken = {}; // Slack API Token passed in via Connect @@ -56,7 +56,7 @@ export default class SlackAdapter { try { this.populateMembershipChannelMap(); // If run outside of Meteor.startup, HTTP is not defined } catch (err) { - logger.slack.error('Error attempting to connect to Slack', err); + slackLogger.error('Error attempting to connect to Slack', err); this.slackBridge.disconnect(); } }); @@ -74,9 +74,9 @@ export default class SlackAdapter { } registerForEvents() { - logger.slack.debug('Register for events'); + slackLogger.debug('Register for events'); this.rtm.on('authenticated', () => { - logger.slack.info('Connected to Slack'); + slackLogger.info('Connected to Slack'); }); this.rtm.on('unable_to_rtm_start', () => { @@ -84,7 +84,7 @@ export default class SlackAdapter { }); this.rtm.on('disconnected', () => { - logger.slack.info('Disconnected from Slack'); + slackLogger.info('Disconnected from Slack'); this.slackBridge.disconnect(); }); @@ -102,34 +102,34 @@ export default class SlackAdapter { * } **/ this.rtm.on('message', Meteor.bindEnvironment((slackMessage) => { - logger.slack.debug('OnSlackEvent-MESSAGE: ', slackMessage); + slackLogger.debug('OnSlackEvent-MESSAGE: ', slackMessage); if (slackMessage) { try { this.onMessage(slackMessage); } catch (err) { - logger.slack.error('Unhandled error onMessage', err); + slackLogger.error('Unhandled error onMessage', err); } } })); this.rtm.on('reaction_added', Meteor.bindEnvironment((reactionMsg) => { - logger.slack.debug('OnSlackEvent-REACTION_ADDED: ', reactionMsg); + slackLogger.debug('OnSlackEvent-REACTION_ADDED: ', reactionMsg); if (reactionMsg) { try { this.onReactionAdded(reactionMsg); } catch (err) { - logger.slack.error('Unhandled error onReactionAdded', err); + slackLogger.error('Unhandled error onReactionAdded', err); } } })); this.rtm.on('reaction_removed', Meteor.bindEnvironment((reactionMsg) => { - logger.slack.debug('OnSlackEvent-REACTION_REMOVED: ', reactionMsg); + slackLogger.debug('OnSlackEvent-REACTION_REMOVED: ', reactionMsg); if (reactionMsg) { try { this.onReactionRemoved(reactionMsg); } catch (err) { - logger.slack.error('Unhandled error onReactionRemoved', err); + slackLogger.error('Unhandled error onReactionRemoved', err); } } })); @@ -193,12 +193,12 @@ export default class SlackAdapter { * } **/ this.rtm.on('channel_left', Meteor.bindEnvironment((channelLeftMsg) => { - logger.slack.debug('OnSlackEvent-CHANNEL_LEFT: ', channelLeftMsg); + slackLogger.debug('OnSlackEvent-CHANNEL_LEFT: ', channelLeftMsg); if (channelLeftMsg) { try { this.onChannelLeft(channelLeftMsg); } catch (err) { - logger.slack.error('Unhandled error onChannelLeft', err); + slackLogger.error('Unhandled error onChannelLeft', err); } } })); @@ -365,7 +365,7 @@ export default class SlackAdapter { // Stash this away to key off it later so we don't send it back to Slack this.slackBridge.reactionsMap.set(`unset${ rocketMsg._id }${ rocketReaction }`, rocketUser); - logger.slack.debug('Removing reaction from Slack'); + slackLogger.debug('Removing reaction from Slack'); Meteor.runAsUser(rocketUser._id, () => { Meteor.call('setReaction', rocketReaction, rocketMsg._id); }); @@ -411,7 +411,7 @@ export default class SlackAdapter { // Stash this away to key off it later so we don't send it back to Slack this.slackBridge.reactionsMap.set(`set${ rocketMsg._id }${ rocketReaction }`, rocketUser); - logger.slack.debug('Adding reaction from Slack'); + slackLogger.debug('Adding reaction from Slack'); Meteor.runAsUser(rocketUser._id, () => { Meteor.call('setReaction', rocketReaction, rocketMsg._id); }); @@ -455,7 +455,7 @@ export default class SlackAdapter { } postFindChannel(rocketChannelName) { - logger.slack.debug('Searching for Slack channel or group', rocketChannelName); + slackLogger.debug('Searching for Slack channel or group', rocketChannelName); const channels = this.slackAPI.getChannels(); if (channels && channels.length > 0) { for (const channel of channels) { @@ -506,7 +506,7 @@ export default class SlackAdapter { addSlackChannel(rocketChID, slackChID) { const ch = this.getSlackChannel(rocketChID); if (ch == null) { - logger.slack.debug('Added channel', { rocketChID, slackChID }); + slackLogger.debug('Added channel', { rocketChID, slackChID }); this.slackChannelRocketBotMembershipMap.set(rocketChID, { id: slackChID, family: slackChID.charAt(0) === 'C' ? 'channels' : 'groups' }); } } @@ -558,7 +558,7 @@ export default class SlackAdapter { } populateMembershipChannelMap() { - logger.slack.debug('Populating channel map'); + slackLogger.debug('Populating channel map'); this.populateMembershipChannelMapByChannels(); this.populateMembershipChannelMapByGroups(); } @@ -575,10 +575,10 @@ export default class SlackAdapter { timestamp: slackTS, }; - logger.slack.debug('Posting Add Reaction to Slack'); + slackLogger.debug('Posting Add Reaction to Slack'); const postResult = this.slackAPI.react(data); if (postResult) { - logger.slack.debug('Reaction added to Slack'); + slackLogger.debug('Reaction added to Slack'); } } } @@ -595,10 +595,10 @@ export default class SlackAdapter { timestamp: slackTS, }; - logger.slack.debug('Posting Remove Reaction to Slack'); + slackLogger.debug('Posting Remove Reaction to Slack'); const postResult = this.slackAPI.removeReaction(data); if (postResult) { - logger.slack.debug('Reaction removed from Slack'); + slackLogger.debug('Reaction removed from Slack'); } } } @@ -615,10 +615,10 @@ export default class SlackAdapter { as_user: true, }; - logger.slack.debug('Post Delete Message to Slack', data); + slackLogger.debug('Post Delete Message to Slack', data); const postResult = this.slackAPI.removeMessage(data); if (postResult) { - logger.slack.debug('Message deleted on Slack'); + slackLogger.debug('Message deleted on Slack'); } } } @@ -674,7 +674,7 @@ export default class SlackAdapter { data.thread_ts = tmessage.slackTs; } } - logger.slack.debug('Post Message To Slack', data); + slackLogger.debug('Post Message To Slack', data); // If we don't have the bot id yet and we have multiple slack bridges, we need to keep track of the messages that are being sent if (!this.slackBotId && this.rocket.slackAdapters && this.rocket.slackAdapters.length >= 2) { @@ -690,7 +690,7 @@ export default class SlackAdapter { if (postResult.statusCode === 200 && postResult.data && postResult.data.message && postResult.data.message.bot_id && postResult.data.message.ts) { this.slackBotId = postResult.data.message.bot_id; Messages.setSlackBotIdAndSlackTs(rocketMessage._id, postResult.data.message.bot_id, postResult.data.message.ts); - logger.slack.debug(`RocketMsgID=${ rocketMessage._id } SlackMsgID=${ postResult.data.message.ts } SlackBotID=${ postResult.data.message.bot_id }`); + slackLogger.debug(`RocketMsgID=${ rocketMessage._id } SlackMsgID=${ postResult.data.message.ts } SlackBotID=${ postResult.data.message.bot_id }`); } } } @@ -707,16 +707,16 @@ export default class SlackAdapter { text: rocketMessage.msg, as_user: true, }; - logger.slack.debug('Post UpdateMessage To Slack', data); + slackLogger.debug('Post UpdateMessage To Slack', data); const postResult = this.slackAPI.updateMessage(data); if (postResult) { - logger.slack.debug('Message updated on Slack'); + slackLogger.debug('Message updated on Slack'); } } } processChannelJoin(slackMessage) { - logger.slack.debug('Channel join', slackMessage.channel.id); + slackLogger.debug('Channel join', slackMessage.channel.id); const rocketCh = this.rocket.addChannel(slackMessage.channel); if (rocketCh != null) { this.addSlackChannel(rocketCh._id, slackMessage.channel); @@ -775,7 +775,7 @@ export default class SlackAdapter { if (rocketMsgObj) { deleteMessage(rocketMsgObj, rocketUser); - logger.slack.debug('Rocket message deleted by Slack'); + slackLogger.debug('Rocket message deleted by Slack'); } } } @@ -802,7 +802,7 @@ export default class SlackAdapter { }; updateMessage(rocketMsgObj, rocketUser); - logger.slack.debug('Rocket message updated by Slack'); + slackLogger.debug('Rocket message updated by Slack'); } } } @@ -975,7 +975,7 @@ export default class SlackAdapter { return rocketMsgObj; } - logger.slack.error('Pinned item with no attachment'); + slackLogger.error('Pinned item with no attachment'); } processSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting) { @@ -1015,15 +1015,15 @@ export default class SlackAdapter { case 'file_share': return this.processShareMessage(rocketChannel, rocketUser, slackMessage, isImporting); case 'file_comment': - logger.slack.error('File comment not implemented'); + slackLogger.error('File comment not implemented'); return; case 'file_mention': - logger.slack.error('File mentioned not implemented'); + slackLogger.error('File mentioned not implemented'); return; case 'pinned_item': return this.processPinnedItemMessage(rocketChannel, rocketUser, slackMessage, isImporting); case 'unpinned_item': - logger.slack.error('Unpinned item not implemented'); + slackLogger.error('Unpinned item not implemented'); } } @@ -1096,12 +1096,12 @@ export default class SlackAdapter { } importFromHistory(family, options) { - logger.slack.debug('Importing messages history'); + slackLogger.debug('Importing messages history'); const data = this.slackAPI.getHistory(family, options); if (Array.isArray(data.messages) && data.messages.length) { let latest = 0; for (const message of data.messages.reverse()) { - logger.slack.debug('MESSAGE: ', message); + slackLogger.debug('MESSAGE: ', message); if (!latest || message.ts > latest) { latest = message.ts; } @@ -1113,7 +1113,7 @@ export default class SlackAdapter { } copyChannelInfo(rid, channelMap) { - logger.slack.debug('Copying users from Slack channel to Rocket.Chat', channelMap.id, rid); + slackLogger.debug('Copying users from Slack channel to Rocket.Chat', channelMap.id, rid); const channel = this.slackAPI.getRoomInfo(channelMap.id); if (channel) { const members = this.slackAPI.getMembers(channelMap.id); @@ -1121,7 +1121,7 @@ export default class SlackAdapter { for (const member of members) { const user = this.rocket.findUser(member) || this.rocket.addUser(member); if (user) { - logger.slack.debug('Adding user to room', user.username, rid); + slackLogger.debug('Adding user to room', user.username, rid); addUserToRoom(rid, user, null, true); } } @@ -1150,7 +1150,7 @@ export default class SlackAdapter { if (topic) { const creator = this.rocket.findUser(topic_creator) || this.rocket.addUser(topic_creator); - logger.slack.debug('Setting room topic', rid, topic, creator.username); + slackLogger.debug('Setting room topic', rid, topic, creator.username); saveRoomTopic(rid, topic, creator, false); } } @@ -1185,19 +1185,19 @@ export default class SlackAdapter { } importMessages(rid, callback) { - logger.slack.info('importMessages: ', rid); + slackLogger.info('importMessages: ', rid); const rocketchat_room = Rooms.findOneById(rid); if (rocketchat_room) { if (this.getSlackChannel(rid)) { this.copyChannelInfo(rid, this.getSlackChannel(rid)); - logger.slack.debug('Importing messages from Slack to Rocket.Chat', this.getSlackChannel(rid), rid); + slackLogger.debug('Importing messages from Slack to Rocket.Chat', this.getSlackChannel(rid), rid); let results = this.importFromHistory(this.getSlackChannel(rid).family, { channel: this.getSlackChannel(rid).id, oldest: 1 }); while (results && results.has_more) { results = this.importFromHistory(this.getSlackChannel(rid).family, { channel: this.getSlackChannel(rid).id, oldest: results.ts }); } - logger.slack.debug('Pinning Slack channel messages to Rocket.Chat', this.getSlackChannel(rid), rid); + slackLogger.debug('Pinning Slack channel messages to Rocket.Chat', this.getSlackChannel(rid), rid); this.copyPins(rid, this.getSlackChannel(rid)); return callback(); @@ -1207,10 +1207,10 @@ export default class SlackAdapter { this.addSlackChannel(rid, slack_room.id); return this.importMessages(rid, callback); } - logger.slack.error('Could not find Slack room with specified name', rocketchat_room.name); + slackLogger.error('Could not find Slack room with specified name', rocketchat_room.name); return callback(new Meteor.Error('error-slack-room-not-found', 'Could not find Slack room with specified name')); } - logger.slack.error('Could not find Rocket.Chat room with specified id', rid); + slackLogger.error('Could not find Rocket.Chat room with specified id', rid); return callback(new Meteor.Error('error-invalid-room', 'Invalid room')); } } diff --git a/app/slackbridge/server/logger.js b/app/slackbridge/server/logger.js index f8cf660783912..6f25e8015ff30 100644 --- a/app/slackbridge/server/logger.js +++ b/app/slackbridge/server/logger.js @@ -1,11 +1,8 @@ import { Logger } from '../../logger'; -export const logger = new Logger('SlackBridge', { - sections: { - connection: 'Connection', - events: 'Events', - class: 'Class', - slack: 'Slack', - rocket: 'Rocket', - }, -}); +export const logger = new Logger('SlackBridge'); + +export const connLogger = logger.section('Connection'); +export const classLogger = logger.section('Class'); +export const slackLogger = logger.section('Slack'); +export const rocketLogger = logger.section('Rocket'); diff --git a/app/slackbridge/server/removeChannelLinks.js b/app/slackbridge/server/removeChannelLinks.js index 213636d075650..e4552b38505b4 100644 --- a/app/slackbridge/server/removeChannelLinks.js +++ b/app/slackbridge/server/removeChannelLinks.js @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { Rooms } from '../../models/server'; -import { hasRole } from '../../authorization'; -import { settings } from '../../settings'; +import { hasRole } from '../../authorization/server'; +import { settings } from '../../settings/server'; Meteor.methods({ removeSlackBridgeChannelLinks() { diff --git a/app/slackbridge/server/settings.js b/app/slackbridge/server/settings.js deleted file mode 100644 index c22042f72f632..0000000000000 --- a/app/slackbridge/server/settings.js +++ /dev/null @@ -1,106 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(function() { - settings.addGroup('SlackBridge', function() { - this.add('SlackBridge_Enabled', false, { - type: 'boolean', - i18nLabel: 'Enabled', - public: true, - }); - - this.add('SlackBridge_APIToken', '', { - type: 'string', - multiline: true, - enableQuery: { - _id: 'SlackBridge_Enabled', - value: true, - }, - i18nLabel: 'SlackBridge_APIToken', - i18nDescription: 'SlackBridge_APIToken_Description', - secret: true, - }); - - this.add('SlackBridge_FileUpload_Enabled', true, { - type: 'boolean', - enableQuery: { - _id: 'SlackBridge_Enabled', - value: true, - }, - i18nLabel: 'FileUpload', - }); - - this.add('SlackBridge_Out_Enabled', false, { - type: 'boolean', - enableQuery: { - _id: 'SlackBridge_Enabled', - value: true, - }, - }); - - this.add('SlackBridge_Out_All', false, { - type: 'boolean', - enableQuery: [{ - _id: 'SlackBridge_Enabled', - value: true, - }, { - _id: 'SlackBridge_Out_Enabled', - value: true, - }], - }); - - this.add('SlackBridge_Out_Channels', '', { - type: 'roomPick', - enableQuery: [{ - _id: 'SlackBridge_Enabled', - value: true, - }, { - _id: 'SlackBridge_Out_Enabled', - value: true, - }, { - _id: 'SlackBridge_Out_All', - value: false, - }], - }); - - this.add('SlackBridge_AliasFormat', '', { - type: 'string', - enableQuery: { - _id: 'SlackBridge_Enabled', - value: true, - }, - i18nLabel: 'Alias_Format', - i18nDescription: 'Alias_Format_Description', - }); - - this.add('SlackBridge_ExcludeBotnames', '', { - type: 'string', - enableQuery: { - _id: 'SlackBridge_Enabled', - value: true, - }, - i18nLabel: 'Exclude_Botnames', - i18nDescription: 'Exclude_Botnames_Description', - }); - - this.add('SlackBridge_Reactions_Enabled', true, { - type: 'boolean', - enableQuery: { - _id: 'SlackBridge_Enabled', - value: true, - }, - i18nLabel: 'Reactions', - }); - - this.add('SlackBridge_Remove_Channel_Links', 'removeSlackBridgeChannelLinks', { - type: 'action', - actionText: 'Remove_Channel_Links', - i18nDescription: 'SlackBridge_Remove_Channel_Links_Description', - enableQuery: { - _id: 'SlackBridge_Enabled', - value: true, - }, - }); - }); -}); diff --git a/app/slackbridge/server/settings.ts b/app/slackbridge/server/settings.ts new file mode 100644 index 0000000000000..2eb9f969a5cac --- /dev/null +++ b/app/slackbridge/server/settings.ts @@ -0,0 +1,102 @@ +import { settingsRegistry } from '../../settings/server'; + +settingsRegistry.addGroup('SlackBridge', function() { + this.add('SlackBridge_Enabled', false, { + type: 'boolean', + i18nLabel: 'Enabled', + public: true, + }); + + this.add('SlackBridge_APIToken', '', { + type: 'string', + multiline: true, + enableQuery: { + _id: 'SlackBridge_Enabled', + value: true, + }, + i18nLabel: 'SlackBridge_APIToken', + i18nDescription: 'SlackBridge_APIToken_Description', + secret: true, + }); + + this.add('SlackBridge_FileUpload_Enabled', true, { + type: 'boolean', + enableQuery: { + _id: 'SlackBridge_Enabled', + value: true, + }, + i18nLabel: 'FileUpload', + }); + + this.add('SlackBridge_Out_Enabled', false, { + type: 'boolean', + enableQuery: { + _id: 'SlackBridge_Enabled', + value: true, + }, + }); + + this.add('SlackBridge_Out_All', false, { + type: 'boolean', + enableQuery: [{ + _id: 'SlackBridge_Enabled', + value: true, + }, { + _id: 'SlackBridge_Out_Enabled', + value: true, + }], + }); + + this.add('SlackBridge_Out_Channels', '', { + type: 'roomPick', + enableQuery: [{ + _id: 'SlackBridge_Enabled', + value: true, + }, { + _id: 'SlackBridge_Out_Enabled', + value: true, + }, { + _id: 'SlackBridge_Out_All', + value: false, + }], + }); + + this.add('SlackBridge_AliasFormat', '', { + type: 'string', + enableQuery: { + _id: 'SlackBridge_Enabled', + value: true, + }, + i18nLabel: 'Alias_Format', + i18nDescription: 'Alias_Format_Description', + }); + + this.add('SlackBridge_ExcludeBotnames', '', { + type: 'string', + enableQuery: { + _id: 'SlackBridge_Enabled', + value: true, + }, + i18nLabel: 'Exclude_Botnames', + i18nDescription: 'Exclude_Botnames_Description', + }); + + this.add('SlackBridge_Reactions_Enabled', true, { + type: 'boolean', + enableQuery: { + _id: 'SlackBridge_Enabled', + value: true, + }, + i18nLabel: 'Reactions', + }); + + this.add('SlackBridge_Remove_Channel_Links', 'removeSlackBridgeChannelLinks', { + type: 'action', + actionText: 'Remove_Channel_Links', + i18nDescription: 'SlackBridge_Remove_Channel_Links_Description', + enableQuery: { + _id: 'SlackBridge_Enabled', + value: true, + }, + }); +}); diff --git a/app/slackbridge/server/slackbridge.js b/app/slackbridge/server/slackbridge.js index 0c3e7b9e6d423..a40449ad700f8 100644 --- a/app/slackbridge/server/slackbridge.js +++ b/app/slackbridge/server/slackbridge.js @@ -1,7 +1,7 @@ import SlackAdapter from './SlackAdapter.js'; import RocketAdapter from './RocketAdapter.js'; -import { logger } from './logger'; -import { settings } from '../../settings'; +import { classLogger, connLogger } from './logger'; +import { settings } from '../../settings/server'; /** * SlackBridge interfaces between this Rocket installation and a remote Slack installation. @@ -44,7 +44,7 @@ class SlackBridgeClass { } this.connected = true; - logger.connection.info('Enabled'); + connLogger.info('Enabled'); } } @@ -56,13 +56,13 @@ class SlackBridgeClass { }); this.slackAdapters = []; this.connected = false; - logger.connection.info('Disabled'); + connLogger.info('Disabled'); } } processSettings() { // Slack installation API token - settings.get('SlackBridge_APIToken', (key, value) => { + settings.watch('SlackBridge_APIToken', (value) => { if (value !== this.apiTokens) { this.apiTokens = value; if (this.connected) { @@ -71,35 +71,35 @@ class SlackBridgeClass { } } - logger.class.debug(`Setting: ${ key }`, value); + classLogger.debug('Setting: SlackBridge_APIToken', value); }); // Import messages from Slack with an alias; %s is replaced by the username of the user. If empty, no alias will be used. - settings.get('SlackBridge_AliasFormat', (key, value) => { + settings.watch('SlackBridge_AliasFormat', (value) => { this.aliasFormat = value; - logger.class.debug(`Setting: ${ key }`, value); + classLogger.debug('Setting: SlackBridge_AliasFormat', value); }); // Do not propagate messages from bots whose name matches the regular expression above. If left empty, all messages from bots will be propagated. - settings.get('SlackBridge_ExcludeBotnames', (key, value) => { + settings.watch('SlackBridge_ExcludeBotnames', (value) => { this.excludeBotnames = value; - logger.class.debug(`Setting: ${ key }`, value); + classLogger.debug('Setting: SlackBridge_ExcludeBotnames', value); }); // Reactions - settings.get('SlackBridge_Reactions_Enabled', (key, value) => { + settings.watch('SlackBridge_Reactions_Enabled', (value) => { this.isReactionsEnabled = value; - logger.class.debug(`Setting: ${ key }`, value); + classLogger.debug('Setting: SlackBridge_Reactions_Enabled', value); }); // Is this entire SlackBridge enabled - settings.get('SlackBridge_Enabled', (key, value) => { + settings.watch('SlackBridge_Enabled', (value) => { if (value && this.apiTokens) { this.connect(); } else { this.disconnect(); } - logger.class.debug(`Setting: ${ key }`, value); + classLogger.debug('Setting: SlackBridge_Enabled', value); }); } } diff --git a/app/slashcommands-status/lib/status.js b/app/slashcommands-status/lib/status.js index 9f7a46588460f..359068a0cda5b 100644 --- a/app/slashcommands-status/lib/status.js +++ b/app/slashcommands-status/lib/status.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { handleError, slashCommands } from '../../utils'; +import { slashCommands } from '../../utils'; import { api } from '../../../server/sdk/api'; function Status(command, params, item) { @@ -11,6 +11,7 @@ function Status(command, params, item) { Meteor.call('setUserStatus', null, params, (err) => { if (err) { if (Meteor.isClient) { + const { handleError } = require('../../../client/lib/utils/handleError'); return handleError(err); } diff --git a/app/slashcommands-topic/lib/topic.js b/app/slashcommands-topic/lib/topic.js index 3b34239ef164d..dd904c541083b 100644 --- a/app/slashcommands-topic/lib/topic.js +++ b/app/slashcommands-topic/lib/topic.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { handleError, slashCommands } from '../../utils'; +import { slashCommands } from '../../utils'; import { ChatRoom } from '../../models'; import { callbacks } from '../../callbacks'; import { hasPermission } from '../../authorization'; @@ -11,6 +11,7 @@ function Topic(command, params, item) { Meteor.call('saveRoomSettings', item.rid, 'roomTopic', params, (err) => { if (err) { if (Meteor.isClient) { + const { handleError } = require('../../../client/lib/utils/handleError'); return handleError(err); } throw err; diff --git a/app/smarsh-connector/server/functions/generateEml.js b/app/smarsh-connector/server/functions/generateEml.js index 8633fd1ca0ca8..f828ce310387c 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 9b69b05b3ac1e..67fcfde02e675 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/smarsh-connector/server/settings.js b/app/smarsh-connector/server/settings.js index 38ad04755a6f6..04e0bf5de33e1 100644 --- a/app/smarsh-connector/server/settings.js +++ b/app/smarsh-connector/server/settings.js @@ -1,9 +1,9 @@ import moment from 'moment'; -import { settings } from '../../settings'; +import { settingsRegistry } from '../../settings/server'; import 'moment-timezone'; -settings.addGroup('Smarsh', function addSettings() { +settingsRegistry.addGroup('Smarsh', function addSettings() { this.add('Smarsh_Enabled', false, { type: 'boolean', i18nLabel: 'Smarsh_Enabled', diff --git a/app/smarsh-connector/server/startup.js b/app/smarsh-connector/server/startup.js index d0d356322da4e..32842fe8048c7 100644 --- a/app/smarsh-connector/server/startup.js +++ b/app/smarsh-connector/server/startup.js @@ -1,13 +1,12 @@ -import { Meteor } from 'meteor/meteor'; import { SyncedCron } from 'meteor/littledata:synced-cron'; -import _ from 'underscore'; import { smarsh } from './lib/rocketchat'; -import { settings } from '../../settings'; +import { settings } from '../../settings/server'; const smarshJobName = 'Smarsh EML Connector'; -const _addSmarshSyncedCronJob = _.debounce(Meteor.bindEnvironment(function __addSmarshSyncedCronJobDebounced() { + +settings.watchMultiple(['Smarsh_Enabled', 'Smarsh_Email', 'From_Email', 'Smarsh_Interval'], function __addSmarshSyncedCronJobDebounced() { if (SyncedCron.nextScheduledAtDate(smarshJobName)) { SyncedCron.remove(smarshJobName); } @@ -19,15 +18,4 @@ const _addSmarshSyncedCronJob = _.debounce(Meteor.bindEnvironment(function __add job: smarsh.generateEml, }); } -}), 500); - -Meteor.startup(() => { - Meteor.defer(() => { - _addSmarshSyncedCronJob(); - - settings.get('Smarsh_Interval', _addSmarshSyncedCronJob); - settings.get('Smarsh_Enabled', _addSmarshSyncedCronJob); - settings.get('Smarsh_Email', _addSmarshSyncedCronJob); - settings.get('From_Email', _addSmarshSyncedCronJob); - }); }); diff --git a/app/sms/server/SMS.js b/app/sms/server/SMS.js index 9ee7c90b10e77..9f5dcc93574b8 100644 --- a/app/sms/server/SMS.js +++ b/app/sms/server/SMS.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { settings } from '../../settings'; +import { settings } from '../../settings/server'; export const SMS = { enabled: false, @@ -22,10 +22,10 @@ export const SMS = { }, }; -settings.get('SMS_Enabled', function(key, value) { +settings.watch('SMS_Enabled', function(value) { SMS.enabled = value; }); -settings.get('SMS_Default_Omnichannel_Department', function(key, value) { +settings.watch('SMS_Default_Omnichannel_Department', function(value) { SMS.department = value; }); diff --git a/app/sms/server/services/mobex.js b/app/sms/server/services/mobex.js index 1f41dece22103..a85f1650e65da 100644 --- a/app/sms/server/services/mobex.js +++ b/app/sms/server/services/mobex.js @@ -3,6 +3,7 @@ import { Base64 } from 'meteor/base64'; import { settings } from '../../../settings'; import { SMS } from '../SMS'; +import { SystemLogger } from '../../../../server/lib/logger/system'; class Mobex { constructor() { @@ -27,7 +28,7 @@ class Mobex { } if (isNaN(numMedia)) { - console.error(`Error parsing NumMedia ${ data.NumMedia }`); + SystemLogger.error(`Error parsing NumMedia ${ data.NumMedia }`); return returnData; } @@ -84,7 +85,7 @@ class Mobex { } } catch (e) { result.resultMsg = `Error while sending SMS with Mobex. Detail: ${ e }`; - console.error('Error while sending SMS with Mobex', e); + SystemLogger.error('Error while sending SMS with Mobex', e); } return result; @@ -129,7 +130,7 @@ class Mobex { result.response = response; } catch (e) { result.resultMsg = `Error while sending SMS with Mobex. Detail: ${ e }`; - console.error('Error while sending SMS with Mobex', e); + SystemLogger.error('Error while sending SMS with Mobex', e); } return result; diff --git a/app/sms/server/services/twilio.js b/app/sms/server/services/twilio.js index bf52dcd5491f8..eeae457ff694f 100644 --- a/app/sms/server/services/twilio.js +++ b/app/sms/server/services/twilio.js @@ -7,6 +7,7 @@ import { settings } from '../../../settings'; import { SMS } from '../SMS'; import { fileUploadIsValidContentType } from '../../../utils/lib/fileUploadRestrictions'; import { api } from '../../../../server/sdk/api'; +import { SystemLogger } from '../../../../server/lib/logger/system'; const MAX_FILE_SIZE = 5242880; @@ -49,7 +50,7 @@ class Twilio { } if (isNaN(numMedia)) { - console.error(`Error parsing NumMedia ${ data.NumMedia }`); + SystemLogger.error(`Error parsing NumMedia ${ data.NumMedia }`); return returnData; } @@ -98,7 +99,7 @@ class Twilio { if (reason) { rid && userId && notifyAgent(userId, rid, reason); - return console.error(`(Twilio) -> ${ reason }`); + return SystemLogger.error(`(Twilio) -> ${ reason }`); } mediaUrl = [publicFilePath]; diff --git a/app/sms/server/services/voxtelesys.js b/app/sms/server/services/voxtelesys.js index 3b0c7dd0ae929..c2b00e59d2e37 100644 --- a/app/sms/server/services/voxtelesys.js +++ b/app/sms/server/services/voxtelesys.js @@ -8,6 +8,7 @@ import { SMS } from '../SMS'; import { fileUploadIsValidContentType } from '../../../utils/lib/fileUploadRestrictions'; import { mime } from '../../../utils/lib/mimeTypes'; import { api } from '../../../../server/sdk/api'; +import { SystemLogger } from '../../../../server/lib/logger/system'; const MAX_FILE_SIZE = 5242880; @@ -79,7 +80,7 @@ class Voxtelesys { if (reason) { rid && userId && notifyAgent(userId, rid, reason); - return console.error(`(Voxtelesys) -> ${ reason }`); + return SystemLogger.error(`(Voxtelesys) -> ${ reason }`); } media = [publicFilePath]; @@ -100,7 +101,7 @@ class Voxtelesys { try { HTTP.call('POST', this.URL || 'https://smsapi.voxtelesys.net/api/v1/sms', options); } catch (error) { - console.error(`Error connecting to Voxtelesys SMS API: ${ error }`); + SystemLogger.error(`Error connecting to Voxtelesys SMS API: ${ error }`); } } diff --git a/app/sms/server/settings.js b/app/sms/server/settings.js deleted file mode 100644 index 4a5ffae29bf99..0000000000000 --- a/app/sms/server/settings.js +++ /dev/null @@ -1,170 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(function() { - settings.addGroup('SMS', function() { - this.add('SMS_Enabled', false, { - type: 'boolean', - i18nLabel: 'Enabled', - }); - - this.add('SMS_Service', 'twilio', { - type: 'select', - values: [ - { - key: 'twilio', - i18nLabel: 'Twilio', - }, - { - key: 'voxtelesys', - i18nLabel: 'Voxtelesys', - }, - { - key: 'mobex', - i18nLabel: 'Mobex', - }, - ], - i18nLabel: 'Service', - }); - - this.add('SMS_Default_Omnichannel_Department', '', { - type: 'string', - }); - - this.section('Twilio', function() { - this.add('SMS_Twilio_Account_SID', '', { - type: 'string', - enableQuery: { - _id: 'SMS_Service', - value: 'twilio', - }, - i18nLabel: 'Account_SID', - secret: true, - }); - this.add('SMS_Twilio_authToken', '', { - type: 'string', - enableQuery: { - _id: 'SMS_Service', - value: 'twilio', - }, - i18nLabel: 'Auth_Token', - secret: true, - }); - this.add('SMS_Twilio_FileUpload_Enabled', true, { - type: 'boolean', - enableQuery: { - _id: 'SMS_Service', - value: 'twilio', - }, - i18nLabel: 'FileUpload_Enabled', - secret: true, - }); - this.add('SMS_Twilio_FileUpload_MediaTypeWhiteList', 'image/*,audio/*,video/*,text/*,application/pdf', { - type: 'string', - enableQuery: { - _id: 'SMS_Service', - value: 'twilio', - }, - i18nLabel: 'FileUpload_MediaTypeWhiteList', - i18nDescription: 'FileUpload_MediaTypeWhiteListDescription', - secret: true, - }); - }); - - this.section('Voxtelesys', function() { - this.add('SMS_Voxtelesys_authToken', '', { - type: 'string', - enableQuery: { - _id: 'SMS_Service', - value: 'voxtelesys', - }, - i18nLabel: 'Auth_Token', - secret: true, - }); - this.add('SMS_Voxtelesys_URL', 'https://smsapi.voxtelesys.net/api/v1/sms', { - type: 'string', - enableQuery: { - _id: 'SMS_Service', - value: 'voxtelesys', - }, - i18nLabel: 'URL', - secret: true, - }); - this.add('SMS_Voxtelesys_FileUpload_Enabled', true, { - type: 'boolean', - enableQuery: { - _id: 'SMS_Service', - value: 'voxtelesys', - }, - i18nLabel: 'FileUpload_Enabled', - secret: true, - }); - this.add('SMS_Voxtelesys_FileUpload_MediaTypeWhiteList', 'image/*,audio/*,video/*,text/*,application/pdf', { - type: 'string', - enableQuery: { - _id: 'SMS_Service', - value: 'voxtelesys', - }, - i18nLabel: 'FileUpload_MediaTypeWhiteList', - i18nDescription: 'FileUpload_MediaTypeWhiteListDescription', - secret: true, - }); - }); - - this.section('Mobex', function() { - this.add('SMS_Mobex_gateway_address', '', { - type: 'string', - enableQuery: { - _id: 'SMS_Service', - value: 'mobex', - }, - i18nLabel: 'Mobex_sms_gateway_address', - i18nDescription: 'Mobex_sms_gateway_address_desc', - }); - this.add('SMS_Mobex_restful_address', '', { - type: 'string', - enableQuery: { - _id: 'SMS_Service', - value: 'mobex', - }, - i18nLabel: 'Mobex_sms_gateway_restful_address', - i18nDescription: 'Mobex_sms_gateway_restful_address_desc', - }); - this.add('SMS_Mobex_username', '', { - type: 'string', - enableQuery: { - _id: 'SMS_Service', - value: 'mobex', - }, - i18nLabel: 'Mobex_sms_gateway_username', - }); - this.add('SMS_Mobex_password', '', { - type: 'password', - enableQuery: { - _id: 'SMS_Service', - value: 'mobex', - }, - i18nLabel: 'Mobex_sms_gateway_password', - }); - this.add('SMS_Mobex_from_number', '', { - type: 'int', - enableQuery: { - _id: 'SMS_Service', - value: 'mobex', - }, - i18nLabel: 'Mobex_sms_gateway_from_number', - i18nDescription: 'Mobex_sms_gateway_from_number_desc', - }); - this.add('SMS_Mobex_from_numbers_list', '', { - type: 'string', - enableQuery: { - _id: 'SMS_Service', - value: 'mobex', - }, - i18nLabel: 'Mobex_sms_gateway_from_numbers_list', - i18nDescription: 'Mobex_sms_gateway_from_numbers_list_desc', - }); - }); - }); -}); diff --git a/app/sms/server/settings.ts b/app/sms/server/settings.ts new file mode 100644 index 0000000000000..448934362ab2f --- /dev/null +++ b/app/sms/server/settings.ts @@ -0,0 +1,167 @@ +import { settingsRegistry } from '../../settings/server'; + + +settingsRegistry.addGroup('SMS', function() { + this.add('SMS_Enabled', false, { + type: 'boolean', + i18nLabel: 'Enabled', + }); + + this.add('SMS_Service', 'twilio', { + type: 'select', + values: [ + { + key: 'twilio', + i18nLabel: 'Twilio', + }, + { + key: 'voxtelesys', + i18nLabel: 'Voxtelesys', + }, + { + key: 'mobex', + i18nLabel: 'Mobex', + }, + ], + i18nLabel: 'Service', + }); + + this.add('SMS_Default_Omnichannel_Department', '', { + type: 'string', + }); + + this.section('Twilio', function() { + this.add('SMS_Twilio_Account_SID', '', { + type: 'string', + enableQuery: { + _id: 'SMS_Service', + value: 'twilio', + }, + i18nLabel: 'Account_SID', + secret: true, + }); + this.add('SMS_Twilio_authToken', '', { + type: 'string', + enableQuery: { + _id: 'SMS_Service', + value: 'twilio', + }, + i18nLabel: 'Auth_Token', + secret: true, + }); + this.add('SMS_Twilio_FileUpload_Enabled', true, { + type: 'boolean', + enableQuery: { + _id: 'SMS_Service', + value: 'twilio', + }, + i18nLabel: 'FileUpload_Enabled', + secret: true, + }); + this.add('SMS_Twilio_FileUpload_MediaTypeWhiteList', 'image/*,audio/*,video/*,text/*,application/pdf', { + type: 'string', + enableQuery: { + _id: 'SMS_Service', + value: 'twilio', + }, + i18nLabel: 'FileUpload_MediaTypeWhiteList', + i18nDescription: 'FileUpload_MediaTypeWhiteListDescription', + secret: true, + }); + }); + + this.section('Voxtelesys', function() { + this.add('SMS_Voxtelesys_authToken', '', { + type: 'string', + enableQuery: { + _id: 'SMS_Service', + value: 'voxtelesys', + }, + i18nLabel: 'Auth_Token', + secret: true, + }); + this.add('SMS_Voxtelesys_URL', 'https://smsapi.voxtelesys.net/api/v1/sms', { + type: 'string', + enableQuery: { + _id: 'SMS_Service', + value: 'voxtelesys', + }, + i18nLabel: 'URL', + secret: true, + }); + this.add('SMS_Voxtelesys_FileUpload_Enabled', true, { + type: 'boolean', + enableQuery: { + _id: 'SMS_Service', + value: 'voxtelesys', + }, + i18nLabel: 'FileUpload_Enabled', + secret: true, + }); + this.add('SMS_Voxtelesys_FileUpload_MediaTypeWhiteList', 'image/*,audio/*,video/*,text/*,application/pdf', { + type: 'string', + enableQuery: { + _id: 'SMS_Service', + value: 'voxtelesys', + }, + i18nLabel: 'FileUpload_MediaTypeWhiteList', + i18nDescription: 'FileUpload_MediaTypeWhiteListDescription', + secret: true, + }); + }); + + this.section('Mobex', function() { + this.add('SMS_Mobex_gateway_address', '', { + type: 'string', + enableQuery: { + _id: 'SMS_Service', + value: 'mobex', + }, + i18nLabel: 'Mobex_sms_gateway_address', + i18nDescription: 'Mobex_sms_gateway_address_desc', + }); + this.add('SMS_Mobex_restful_address', '', { + type: 'string', + enableQuery: { + _id: 'SMS_Service', + value: 'mobex', + }, + i18nLabel: 'Mobex_sms_gateway_restful_address', + i18nDescription: 'Mobex_sms_gateway_restful_address_desc', + }); + this.add('SMS_Mobex_username', '', { + type: 'string', + enableQuery: { + _id: 'SMS_Service', + value: 'mobex', + }, + i18nLabel: 'Mobex_sms_gateway_username', + }); + this.add('SMS_Mobex_password', '', { + type: 'password', + enableQuery: { + _id: 'SMS_Service', + value: 'mobex', + }, + i18nLabel: 'Mobex_sms_gateway_password', + }); + this.add('SMS_Mobex_from_number', '', { + type: 'int', + enableQuery: { + _id: 'SMS_Service', + value: 'mobex', + }, + i18nLabel: 'Mobex_sms_gateway_from_number', + i18nDescription: 'Mobex_sms_gateway_from_number_desc', + }); + this.add('SMS_Mobex_from_numbers_list', '', { + type: 'string', + enableQuery: { + _id: 'SMS_Service', + value: 'mobex', + }, + i18nLabel: 'Mobex_sms_gateway_from_numbers_list', + i18nDescription: 'Mobex_sms_gateway_from_numbers_list_desc', + }); + }); +}); diff --git a/app/statistics/server/lib/SAUMonitor.js b/app/statistics/server/lib/SAUMonitor.js index 36b7036fb5a04..9ffa5479a9e24 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 0d7b2da16f3d9..18338bc051046 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 464f14b0e5744..5d3c7a6efa843 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 75bc6ca3dc2c8..0eb444940b3ad 100644 --- a/app/statistics/server/lib/getServicesStatistics.ts +++ b/app/statistics/server/lib/getServicesStatistics.ts @@ -6,12 +6,12 @@ function getCustomOAuthServices(): Record { - const customOauth = settings.get(/Accounts_OAuth_Custom-[^-]+$/mi); - return Object.fromEntries(customOauth.map(({ key, value }) => { + const customOauth = settings.getByRegexp(/Accounts_OAuth_Custom-[^-]+$/mi); + return Object.fromEntries(Object.entries(customOauth).map(([key, value]) => { const name = key.replace('Accounts_OAuth_Custom-', ''); return [name, { enabled: Boolean(value), - mergeRoles: Boolean(settings.get(`Accounts_OAuth_Custom-${ name }-merge_roles`)), + mergeRoles: settings.get(`Accounts_OAuth_Custom-${ name }-merge_roles`), users: Users.countActiveUsersByService(name), }]; })); @@ -25,10 +25,10 @@ export function getServicesStatistics(): Record { loginFallback: settings.get('LDAP_Login_Fallback'), encryption: settings.get('LDAP_Encryption'), mergeUsers: settings.get('LDAP_Merge_Existing_Users'), - syncRoles: settings.get('LDAP_Sync_User_Data_Groups'), - syncRolesAutoRemove: settings.get('LDAP_Sync_User_Data_Groups_AutoRemove'), - syncData: settings.get('LDAP_Sync_User_Data'), - syncChannels: settings.get('LDAP_Sync_User_Data_Groups_AutoChannels'), + syncRoles: settings.get('LDAP_Sync_User_Data_Roles'), + syncRolesAutoRemove: settings.get('LDAP_Sync_User_Data_Roles_AutoRemove'), + syncData: settings.get('LDAP_Sync_Custom_Fields'), + syncChannels: settings.get('LDAP_Sync_User_Data_Channels'), syncAvatar: settings.get('LDAP_Sync_User_Avatar'), groupFilter: settings.get('LDAP_Group_Filter_Enable'), backgroundSync: { @@ -40,7 +40,7 @@ export function getServicesStatistics(): Record { ee: { syncActiveState: settings.get('LDAP_Sync_User_Active_State'), syncTeams: settings.get('LDAP_Enable_LDAP_Groups_To_RC_Teams'), - syncRoles: settings.get('LDAP_Enable_LDAP_Roles_To_RC_Roles'), + syncRoles: settings.get('LDAP_Sync_User_Data_Roles'), }, }, saml: { diff --git a/app/statistics/server/lib/statistics.js b/app/statistics/server/lib/statistics.js index cf6390b2b0092..3fa09484ed4d3 100644 --- a/app/statistics/server/lib/statistics.js +++ b/app/statistics/server/lib/statistics.js @@ -3,29 +3,26 @@ 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 { Migrations } from '../../../migrations/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'; import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server'; -import { Team } from '../../../../server/sdk'; +import { Team, Analytics } from '../../../../server/sdk'; const wizardFields = [ 'Organization_Type', @@ -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,13 +172,13 @@ 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; - statistics.migration = Migrations._getControl(); + statistics.migration = getControl(); statistics.instanceCount = InstanceStatus.getCollection().find({ _updatedAt: { $gt: new Date(Date.now() - process.uptime() * 1000 - 2000) } }).count(); const { oplogEnabled, mongoVersion, mongoStorageEngine } = getMongoInfo(); @@ -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, @@ -211,13 +221,14 @@ export const statistics = { statistics.pushQueue = Promise.await(NotificationQueue.col.estimatedDocumentCount()); statistics.enterprise = getEnterpriseStatistics(); + Promise.await(Analytics.resetSeatRequestCount()); 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/statistics/server/startup/monitor.js b/app/statistics/server/startup/monitor.js index 70cd64bf5c6cc..ab85ddbe835e1 100644 --- a/app/statistics/server/startup/monitor.js +++ b/app/statistics/server/startup/monitor.js @@ -8,7 +8,7 @@ const SAUMonitor = new SAUMonitorClass(); Meteor.startup(() => { let TroubleshootDisableSessionsMonitor; - settings.get('Troubleshoot_Disable_Sessions_Monitor', (key, value) => { + settings.watch('Troubleshoot_Disable_Sessions_Monitor', (value) => { if (TroubleshootDisableSessionsMonitor === value) { return; } TroubleshootDisableSessionsMonitor = value; diff --git a/app/theme/client/imports/components/contextual-bar.css b/app/theme/client/imports/components/contextual-bar.css index e27c96fefae2d..6ce4092d57664 100644 --- a/app/theme/client/imports/components/contextual-bar.css +++ b/app/theme/client/imports/components/contextual-bar.css @@ -99,7 +99,6 @@ } &-icon { - flex: 0 0 auto; margin: 0 0.25rem; @@ -124,7 +123,6 @@ } &-title { - overflow: hidden; flex: 1; @@ -138,7 +136,6 @@ } &-description { - display: block; flex: 1; @@ -222,7 +219,6 @@ background: var(--rc-color-alert-message-primary-background); &--selected { - cursor: pointer; color: var(--rc-color-alert-message-secondary); diff --git a/app/theme/client/imports/components/header.css b/app/theme/client/imports/components/header.css index ebf39c94879a8..587797fac846b 100644 --- a/app/theme/client/imports/components/header.css +++ b/app/theme/client/imports/components/header.css @@ -108,7 +108,6 @@ } &__data { - overflow: hidden; flex-direction: column; diff --git a/app/theme/client/imports/components/main-content.css b/app/theme/client/imports/components/main-content.css index 04f7cb05d9706..4c01f982ee363 100644 --- a/app/theme/client/imports/components/main-content.css +++ b/app/theme/client/imports/components/main-content.css @@ -1,5 +1,4 @@ .main-content { - position: relative; z-index: 0; diff --git a/app/theme/client/imports/components/message-box.css b/app/theme/client/imports/components/message-box.css index 676c0fd33813d..649a1e0826e70 100644 --- a/app/theme/client/imports/components/message-box.css +++ b/app/theme/client/imports/components/message-box.css @@ -52,19 +52,19 @@ } } - &__typing { + &__activity { position: absolute; top: 4px; left: 0; margin-left: 24px; - color: var(--message-box-user-typing-color); + color: var(--message-box-user-activity-color); - font-size: var(--message-box-user-typing-text-size); + font-size: var(--message-box-user-activity-text-size); &-user { - color: var(--message-box-user-typing-user-color); + color: var(--message-box-user-activity-user-color); font-weight: bold; } @@ -285,7 +285,7 @@ margin-top: 1rem; padding: 0; - &__typing { + &__activity { top: -1rem; margin-left: 1rem; @@ -377,7 +377,7 @@ } } -.rtl .rc-message-box__typing { +.rtl .rc-message-box__activity { right: 0; margin-right: 24px; diff --git a/app/theme/client/imports/components/modal/create-channel.css b/app/theme/client/imports/components/modal/create-channel.css index 8fd3fb876a66f..f9c5a8be28cd9 100644 --- a/app/theme/client/imports/components/modal/create-channel.css +++ b/app/theme/client/imports/components/modal/create-channel.css @@ -16,7 +16,6 @@ } &__wrapper { - display: flex; flex-direction: column; diff --git a/app/theme/client/imports/components/popout.css b/app/theme/client/imports/components/popout.css index 52164b2c5d5aa..097f241426e47 100644 --- a/app/theme/client/imports/components/popout.css +++ b/app/theme/client/imports/components/popout.css @@ -1,20 +1,17 @@ @keyframes loading { 0% { - transform: scale(0.7); opacity: 0; } 50% { - transform: scale(1); opacity: 1; } 100% { - transform: scale(0.7); opacity: 0; diff --git a/app/theme/client/imports/components/popover.css b/app/theme/client/imports/components/popover.css index 807bdd7c88d21..c8167997c68a6 100644 --- a/app/theme/client/imports/components/popover.css +++ b/app/theme/client/imports/components/popover.css @@ -53,6 +53,10 @@ border-radius: var(--popover-radius); background-color: var(--popover-background); box-shadow: 0 0 2px 0 rgba(47, 52, 61, 0.08), 0 0 12px 0 rgba(47, 52, 61, 0.12); + + &--templateless { + padding: var(--popover-padding) 0; + } } &__column { @@ -82,6 +86,8 @@ margin-bottom: 8px; + padding: 0 var(--popover-padding); + text-transform: uppercase; color: var(--popover-title-color); @@ -96,7 +102,7 @@ width: 100%; - padding: 4px 0; + padding: 4px var(--popover-padding); cursor: pointer; @@ -109,6 +115,10 @@ font-size: var(--popover-item-text-size); align-items: center; + &:hover { + background-color: #f7f8fa; + } + &--alert { color: var(--rc-color-error); @@ -179,9 +189,9 @@ } &__divider { - width: 100%; + width: 88%; height: var(--popover-divider-height); - margin: 1rem 0; + margin: 1rem auto; background: var(--popover-divider-color); diff --git a/app/theme/client/imports/components/sidebar/sidebar-flex.css b/app/theme/client/imports/components/sidebar/sidebar-flex.css index b124ee54bcea3..1022a18268be3 100644 --- a/app/theme/client/imports/components/sidebar/sidebar-flex.css +++ b/app/theme/client/imports/components/sidebar/sidebar-flex.css @@ -1,13 +1,11 @@ .sidebar-flex { &__header { - display: flex; padding: var(--sidebar-default-padding); } &__title { - flex: 1; font-size: 1rem; diff --git a/app/theme/client/imports/components/sidebar/sidebar.css b/app/theme/client/imports/components/sidebar/sidebar.css index 1b8f4e312f032..e22e2752b519e 100644 --- a/app/theme/client/imports/components/sidebar/sidebar.css +++ b/app/theme/client/imports/components/sidebar/sidebar.css @@ -1,5 +1,4 @@ .sidebar { - position: relative; z-index: 2; diff --git a/app/theme/client/imports/components/table.css b/app/theme/client/imports/components/table.css index 16da7f6cf3e5e..9617002312e82 100644 --- a/app/theme/client/imports/components/table.css +++ b/app/theme/client/imports/components/table.css @@ -60,7 +60,6 @@ } & td { - overflow: hidden; padding: 0.25rem 0; diff --git a/app/theme/client/imports/components/userInfo.css b/app/theme/client/imports/components/userInfo.css index 5549cd6c3703b..8e5f3dbbd5739 100644 --- a/app/theme/client/imports/components/userInfo.css +++ b/app/theme/client/imports/components/userInfo.css @@ -39,7 +39,6 @@ } &__banner { - position: absolute; z-index: 1; bottom: 50px; @@ -59,7 +58,6 @@ } &__avatar { - position: relative; width: 120px; @@ -75,7 +73,6 @@ } &__name { - width: 100%; text-align: center; @@ -161,7 +158,6 @@ } &--separator { - margin: 14px 0; border-bottom: 1px solid #d7d7d7; @@ -205,7 +201,6 @@ } &-details { - margin-bottom: calc(var(--default-small-padding) * -1); padding: var(--default-padding); @@ -266,7 +261,6 @@ align-items: flex-end; &-icon { - color: #444444; font-size: 1.25rem; @@ -288,7 +282,6 @@ } &-value { - display: flex; margin: 0 0.25rem; diff --git a/app/theme/client/imports/forms/input.css b/app/theme/client/imports/forms/input.css index 4f945a91e0f6a..21ef11f574fab 100644 --- a/app/theme/client/imports/forms/input.css +++ b/app/theme/client/imports/forms/input.css @@ -84,7 +84,6 @@ textarea.rc-input__element { } &::placeholder { - text-align: start; text-overflow: ellipsis; @@ -200,7 +199,6 @@ textarea.rc-input__element { } &__name { - overflow: hidden; flex: 0 1 auto; @@ -221,7 +219,6 @@ textarea.rc-input__element { } &__description { - color: var(--color-gray); font-size: 0.875rem; @@ -229,7 +226,6 @@ textarea.rc-input__element { } select.rc-input { - width: 100%; padding: 0.782rem; diff --git a/app/theme/client/imports/forms/popup-list.css b/app/theme/client/imports/forms/popup-list.css index 35349a13d96db..e645f9a144303 100644 --- a/app/theme/client/imports/forms/popup-list.css +++ b/app/theme/client/imports/forms/popup-list.css @@ -44,7 +44,6 @@ } &-name { - overflow: hidden; text-overflow: ellipsis; diff --git a/app/theme/client/imports/forms/select.css b/app/theme/client/imports/forms/select.css index 58116f5173920..ebd2665546bfe 100644 --- a/app/theme/client/imports/forms/select.css +++ b/app/theme/client/imports/forms/select.css @@ -1,5 +1,4 @@ .rc-select { - position: relative; display: flex; diff --git a/app/theme/client/imports/forms/tags.css b/app/theme/client/imports/forms/tags.css index 5d1dce60c5a8b..b949f84bf5cc0 100644 --- a/app/theme/client/imports/forms/tags.css +++ b/app/theme/client/imports/forms/tags.css @@ -47,7 +47,6 @@ } &__input { - flex: 1; margin: 0.25rem; diff --git a/app/theme/client/imports/general/apps.css b/app/theme/client/imports/general/apps.css index 8b3cbe7172bcc..1b4495da5ac89 100644 --- a/app/theme/client/imports/general/apps.css +++ b/app/theme/client/imports/general/apps.css @@ -9,7 +9,6 @@ } &-container { - display: flex; width: 100%; @@ -30,7 +29,6 @@ padding: 25px; &__photo { - flex: 0 0 auto; width: 95px; @@ -44,7 +42,6 @@ } &__content { - display: flex; overflow: hidden; flex-direction: column; @@ -65,7 +62,6 @@ } h2 { - padding: 5px 0; font-size: 18px; @@ -144,7 +140,6 @@ @media (width <= 500px) { .rc-apps { &-container { - flex-direction: column; padding: 25px; diff --git a/app/theme/client/imports/general/base.css b/app/theme/client/imports/general/base.css index 1cf8605b6b6a0..eb6eecd06db1e 100644 --- a/app/theme/client/imports/general/base.css +++ b/app/theme/client/imports/general/base.css @@ -83,7 +83,6 @@ button { } #rocket-chat { - position: relative; display: flex; diff --git a/app/theme/client/imports/general/base_old.css b/app/theme/client/imports/general/base_old.css index cd8294c202cfc..339396f3e718d 100644 --- a/app/theme/client/imports/general/base_old.css +++ b/app/theme/client/imports/general/base_old.css @@ -62,14 +62,6 @@ content: ""; } - &:first-child { - margin-top: 4px; - } - - &:last-child { - margin-bottom: 4px; - } - &:first-child::before { border-radius: 2px 2px 0 0; } @@ -296,7 +288,6 @@ &.double-col { & > label { - width: 30%; margin-bottom: 0; padding: 10px 20px 10px 0; @@ -307,7 +298,6 @@ } & > div { - width: 60%; min-height: 2.5rem; @@ -892,7 +882,6 @@ border-radius: var(--border-radius); &__content { - overflow: auto; margin: -1rem; @@ -900,7 +889,6 @@ } & .cms-page-close { - display: flex; margin-bottom: 10px; @@ -1036,14 +1024,12 @@ } & .settings-description { - padding-top: 2px; line-height: 1.2rem; } & .settings-alert { - margin-top: 0.75rem; padding: 1rem; @@ -1542,7 +1528,6 @@ } .rc-old .rc-message-box .reply-preview { - position: relative; display: flex; @@ -1566,7 +1551,6 @@ } .rc-old .rc-message-box .reply-preview:not(:last-child)::before { - position: absolute; right: 15px; @@ -1910,7 +1894,7 @@ font-family: inherit; font-size: 0.875rem; - font-weight: 600; + font-weight: 700; line-height: inherit; } @@ -2697,7 +2681,6 @@ & input, & select { - position: relative; width: 100%; @@ -3061,7 +3044,6 @@ .rc-old .dropzone { & .dropzone-overlay { - position: absolute; z-index: 1000000; top: 0; @@ -3117,154 +3099,6 @@ padding-left: 10px; } -.webrtc-video { - &.webrtc-video-overlay, - & .main-video, - & .state-overlay::before, - & .videos .video-item { - color: var(--color-white); - } - - &.webrtc-video-overlay { - position: fixed; - z-index: 1000; - top: 0; - right: 0; - bottom: 0; - left: 0; - - display: flex; - overflow-y: auto; - flex-direction: column; - - padding: var(--default-small-padding); - - background-color: #ffffff; - - & .main-video { - display: flex; - flex-direction: column; - - min-height: 140px; - margin-bottom: 4px; - align-items: center; - flex-grow: 1; - - .webrtc-video-element { - width: auto; - max-width: 100%; - height: 100%; - min-height: 140px; - } - } - } - - & .main-video { - text-align: center; - - & .webrtc-video-element { - width: 100%; - min-height: 299px; - } - - & > div { - position: relative; - - margin-top: -28px; - margin-bottom: 2px; - padding: 0 8px; - - text-align: center; - - font-weight: bold; - line-height: 25px; - } - } - - & .video-flip { - transform: scaleX(-1); - filter: FlipH; - } - - & .videos { - display: flex; - flex-wrap: wrap; - justify-content: center; - - & .video-item { - position: relative; - - overflow: hidden; - - width: 93px; - margin-right: 3px; - margin-bottom: 3px; - - text-align: center; - - line-height: 0; - - & &.state-overlay::before { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - - display: flex; - - content: attr(data-state-text); - - font-size: 12px; - font-weight: bold; - align-items: center; - justify-content: center; - } - - & .webrtc-video-element { - max-width: 100px; - height: 70px; - } - - & > div { - position: relative; - - overflow: hidden; - - margin-top: -16px; - padding: 0 2px; - - text-align: center; - text-overflow: ellipsis; - - font-size: 12px; - font-weight: bold; - line-height: 16px; - } - - & .video-muted-overlay { - position: absolute; - top: 16px; - right: 0; - bottom: 16px; - left: 0; - - display: flex; - - text-align: center; - - font-size: 24px; - align-items: center; - justify-content: center; - } - } - } -} - -.webrtc-video-element { - background-color: #000000; -} - .rc-old .alert-icon { display: block; diff --git a/app/theme/client/imports/general/forms.css b/app/theme/client/imports/general/forms.css index 9516f91a2072f..6e43b0d4baffa 100644 --- a/app/theme/client/imports/general/forms.css +++ b/app/theme/client/imports/general/forms.css @@ -198,7 +198,6 @@ } &__content { - display: flex; overflow-y: auto; flex-direction: column; diff --git a/app/theme/client/imports/general/variables.css b/app/theme/client/imports/general/variables.css index 635786e6a0ccf..94cdfd27f3d86 100644 --- a/app/theme/client/imports/general/variables.css +++ b/app/theme/client/imports/general/variables.css @@ -335,9 +335,9 @@ --message-box-placeholder-color: var(--color-gray-medium); --message-box-markdown-color: var(--color-gray); --message-box-markdown-hover-color: var(--color-dark); - --message-box-user-typing-color: var(--color-gray); - --message-box-user-typing-text-size: 0.75rem; - --message-box-user-typing-user-color: var(--color-dark); + --message-box-user-activity-color: var(--color-gray); + --message-box-user-activity-text-size: 0.75rem; + --message-box-user-activity-user-color: var(--color-dark); --message-box-container-border-color: var(--color-gray-medium); --message-box-container-border-width: var(--border); --message-box-container-border-radius: var(--border-radius); diff --git a/app/theme/server/server.js b/app/theme/server/server.js index b1a55d40ac20b..b571936817d66 100644 --- a/app/theme/server/server.js +++ b/app/theme/server/server.js @@ -6,17 +6,12 @@ import Autoprefixer from 'less-plugin-autoprefixer'; import { WebApp } from 'meteor/webapp'; import { Meteor } from 'meteor/meteor'; -import { settings } from '../../settings'; +import { settings, settingsRegistry } from '../../settings/server'; import { Logger } from '../../logger'; import { addStyle } from '../../ui-master/server/inject'; +import { Settings } from '../../models/server'; -const logger = new Logger('rocketchat:theme', { - methods: { - stop_rendering: { - type: 'info', - }, - }, -}); +const logger = new Logger('rocketchat:theme'); let currentHash = ''; let currentSize = 0; @@ -26,34 +21,27 @@ export const theme = new class { this.variables = {}; this.packageCallbacks = []; this.customCSS = ''; - settings.add('css', ''); - settings.addGroup('Layout'); - settings.onload('css', Meteor.bindEnvironment((key, value, initialLoad) => { - if (!initialLoad) { - Meteor.startup(function() { - process.emit('message', { - refresh: 'client', - }); - }); - } - })); - this.compileDelayed = _.debounce(Meteor.bindEnvironment(this.compile.bind(this)), 100); - Meteor.startup(() => { - settings.onAfterInitialLoad(() => { - settings.get(/^theme-./, Meteor.bindEnvironment((key, value) => { - if (key === 'theme-custom-css' && value != null) { - this.customCSS = value; - } else { - const name = key.replace(/^theme-[a-z]+-/, ''); - if (this.variables[name] != null) { - this.variables[name].value = value; - } - } - - this.compileDelayed(); - })); + settingsRegistry.add('css', ''); + settingsRegistry.addGroup('Layout'); + settings.change('css', () => { + process.emit('message', { + refresh: 'client', }); }); + + this.compileDelayed = _.debounce(Meteor.bindEnvironment(this.compile.bind(this)), 100); + settings.watchByRegex(/^theme-./, (key, value) => { + if (key === 'theme-custom-css' && value != null) { + this.customCSS = value; + } else { + const name = key.replace(/^theme-[a-z]+-/, ''); + if (this.variables[name] != null) { + this.variables[name].value = value; + } + } + + this.compileDelayed(); + }); } compile() { @@ -69,11 +57,11 @@ export const theme = new class { }; const start = Date.now(); return less.render(content, options, function(err, data) { - logger.stop_rendering(Date.now() - start); + logger.info({ stop_rendering: Date.now() - start }); if (err != null) { - return console.log(err); + return logger.error(err); } - settings.updateById('css', data.css); + Settings.updateValueById('css', data.css); return Meteor.startup(function() { return Meteor.setTimeout(function() { @@ -95,7 +83,7 @@ export const theme = new class { section, }; - return settings.add(`theme-color-${ name }`, value, config); + return settingsRegistry.add(`theme-color-${ name }`, value, config); } addVariable(type, name, value, section, persist = true, editor, allowedTypes, property) { @@ -114,7 +102,7 @@ export const theme = new class { allowedTypes, property, }; - return settings.add(`theme-${ type }-${ name }`, value, config); + return settingsRegistry.add(`theme-${ type }-${ name }`, value, config); } } @@ -136,7 +124,7 @@ export const theme = new class { }(); Meteor.startup(() => { - settings.get('css', (key, value = '') => { + settings.watch('css', (value = '') => { currentHash = crypto.createHash('sha1').update(value).digest('hex'); currentSize = value.length; addStyle('css-theme', value); diff --git a/app/theme/server/variables.js b/app/theme/server/variables.js index c95c243ffeb6b..d609508aaf5fb 100644 --- a/app/theme/server/variables.js +++ b/app/theme/server/variables.js @@ -1,5 +1,5 @@ import { theme } from './server'; -import { settings } from '../../settings'; +import { settingsRegistry } from '../../settings/server'; // TODO: Define registers/getters/setters for packages to work with established // heirarchy of colors instead of making duplicate definitions // TODO: Settings pages to show simple separation of major/minor/addon colors @@ -48,7 +48,7 @@ for (let matches = regionRegex.exec(variablesContent); matches; matches = region }); } -settings.add('theme-custom-css', '', { +settingsRegistry.add('theme-custom-css', '', { group: 'Layout', type: 'code', code: 'text/css', diff --git a/app/threads/client/components/ThreadComponent.tsx b/app/threads/client/components/ThreadComponent.tsx deleted file mode 100644 index 5af90e6c875be..0000000000000 --- a/app/threads/client/components/ThreadComponent.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import React, { useEffect, useRef, useState, useCallback, useMemo, FC } from 'react'; -import { Template } from 'meteor/templating'; -import { Blaze } from 'meteor/blaze'; -import { Tracker } from 'meteor/tracker'; -import { useLocalStorage } from '@rocket.chat/fuselage-hooks'; - -import { ChatMessage } from '../../../models/client'; -import { useRoute } from '../../../../client/contexts/RouterContext'; -import { roomTypes } from '../../../utils/client'; -import { normalizeThreadTitle } from '../lib/normalizeThreadTitle'; -import { useUserId, useUserSubscription } from '../../../../client/contexts/UserContext'; -import { useEndpoint, useMethod } from '../../../../client/contexts/ServerContext'; -import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext'; -import ThreadSkeleton from './ThreadSkeleton'; -import ThreadView from './ThreadView'; -import { IMessage } from '../../../../definition/IMessage'; -import { IRoom } from '../../../../definition/IRoom'; -import { useTabBarOpenUserInfo } from '../../../../client/views/room/providers/ToolboxProvider'; - -const subscriptionFields = {}; - -const useThreadMessage = (tmid: string): IMessage => { - const [message, setMessage] = useState(() => Tracker.nonreactive(() => ChatMessage.findOne({ _id: tmid }))); - const getMessage = useEndpoint('GET', 'chat.getMessage'); - const getMessageParsed = useCallback<(params: Parameters[0]) => Promise>(async (params) => { - const { message } = await getMessage(params); - return { - ...message, - _updatedAt: new Date(message._updatedAt), - }; - }, [getMessage]); - - useEffect(() => { - const computation = Tracker.autorun(async (computation) => { - const msg = ChatMessage.findOne({ _id: tmid }) || await getMessageParsed({ msgId: tmid }); - - if (!msg || computation.stopped) { - return; - } - - setMessage((prevMsg) => { - if (!prevMsg || prevMsg._id !== msg._id || prevMsg._updatedAt?.getTime() !== msg._updatedAt?.getTime()) { - return msg; - } - - return prevMsg; - }); - }); - - return (): void => { - computation.stop(); - }; - }, [getMessageParsed, tmid]); - - return message; -}; - -const ThreadComponent: FC<{ - mid: string; - jump: unknown; - room: IRoom; - onClickBack: (e: unknown) => void; -}> = ({ - mid, - jump, - room, - onClickBack, -}) => { - const subscription = useUserSubscription(room._id, subscriptionFields); - const channelRoute = useRoute(roomTypes.getConfig(room.t).route.name); - const threadMessage = useThreadMessage(mid); - - const openUserInfo = useTabBarOpenUserInfo(); - - const ref = useRef(null); - const uid = useUserId(); - - const headerTitle = useMemo(() => (threadMessage ? normalizeThreadTitle(threadMessage) : null), [threadMessage]); - const [expanded, setExpand] = useLocalStorage('expand-threads', false); - const following = !uid ? false : threadMessage?.replies?.includes(uid) ?? false; - - const dispatchToastMessage = useToastMessageDispatch(); - const followMessage = useMethod('followMessage'); - const unfollowMessage = useMethod('unfollowMessage'); - - const setFollowing = useCallback<(following: boolean) => void>(async (following) => { - try { - if (following) { - await followMessage({ mid }); - return; - } - - await unfollowMessage({ mid }); - } catch (error) { - dispatchToastMessage({ - type: 'error', - message: error, - }); - } - }, [dispatchToastMessage, followMessage, unfollowMessage, mid]); - - const handleClose = useCallback(() => { - channelRoute.push(room.t === 'd' ? { rid: room._id } : { name: room.name || room._id }); - }, [channelRoute, room._id, room.t, room.name]); - - const [viewData, setViewData] = useState(() => ({ - mainMessage: threadMessage, - jump, - following, - subscription, - rid: room._id, - tabBar: { openUserInfo }, - })); - - useEffect(() => { - setViewData((viewData) => { - if (!threadMessage || viewData.mainMessage?._id === threadMessage._id) { - return viewData; - } - - return { - mainMessage: threadMessage, - jump, - following, - subscription, - rid: room._id, - tabBar: { openUserInfo }, - }; - }); - }, [following, jump, openUserInfo, room._id, subscription, threadMessage]); - - useEffect(() => { - if (!ref.current || !viewData.mainMessage) { - return; - } - const view = Blaze.renderWithData(Template.thread, viewData, ref.current); - - return (): void => { - Blaze.remove(view); - }; - }, [viewData]); - - if (!threadMessage) { - return ; - } - - return setExpand(!expanded)} - onToggleFollow={(following): void => setFollowing(!following)} - onClose={handleClose} - onClickBack={onClickBack} - />; -}; - -export default ThreadComponent; diff --git a/app/threads/client/components/ThreadSkeleton.tsx b/app/threads/client/components/ThreadSkeleton.tsx deleted file mode 100644 index 7d0cd9e1d1cfa..0000000000000 --- a/app/threads/client/components/ThreadSkeleton.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { FC, useMemo } from 'react'; -import { Modal, Box } from '@rocket.chat/fuselage'; - -import VerticalBar from '../../../../client/components/VerticalBar'; - -type ThreadSkeletonProps = { - expanded: boolean; - onClose: () => void; -}; - -const ThreadSkeleton: FC = ({ expanded, onClose }) => { - const style = useMemo(() => (document.dir === 'rtl' - ? { - left: 0, - borderTopRightRadius: 4, - } - : { - right: 0, - borderTopLeftRadius: 4, - }), []); - - return <> - {expanded && } - - - - ; -}; - -export default ThreadSkeleton; diff --git a/app/threads/client/components/ThreadView.tsx b/app/threads/client/components/ThreadView.tsx deleted file mode 100644 index 98752fbc3b263..0000000000000 --- a/app/threads/client/components/ThreadView.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React, { useCallback, useMemo, forwardRef } from 'react'; -import { Modal, Box } from '@rocket.chat/fuselage'; - -import { useTranslation } from '../../../../client/contexts/TranslationContext'; -import { useLayoutContextualBarExpanded } from '../../../../client/providers/LayoutProvider'; -import VerticalBar from '../../../../client/components/VerticalBar'; - -type ThreadViewProps = { - title: string; - expanded: boolean; - following: boolean; - onToggleExpand: (expanded: boolean) => void; - onToggleFollow: (following: boolean) => void; - onClose: () => void; - onClickBack: (e: unknown) => void; -}; - -const ThreadView = forwardRef(({ - title, - expanded, - following, - onToggleExpand, - onToggleFollow, - onClose, - onClickBack, -}, ref) => { - const hasExpand = useLayoutContextualBarExpanded(); - - const style = useMemo(() => (document.dir === 'rtl' - ? { - left: 0, - borderTopRightRadius: 4, - } - : { - right: 0, - borderTopLeftRadius: 4, - }), []); - - const t = useTranslation(); - - const expandLabel = expanded ? t('Collapse') : t('Expand'); - const expandIcon = expanded ? 'arrow-collapse' : 'arrow-expand'; - - const handleExpandActionClick = useCallback(() => { - onToggleExpand(expanded); - }, [expanded, onToggleExpand]); - - const followLabel = following ? t('Following') : t('Not_Following'); - const followIcon = following ? 'bell' : 'bell-off'; - - const handleFollowActionClick = useCallback(() => { - onToggleFollow(following); - }, [following, onToggleFollow]); - - return <> - {hasExpand && expanded && } - - - - - {onClickBack && } - - {hasExpand && } - - - - - - - - - ; -}); - -export default ThreadView; diff --git a/app/threads/client/flextab/messageBoxFollow.js b/app/threads/client/flextab/messageBoxFollow.js index 42c485381d9ee..bfd724a3eb8d7 100644 --- a/app/threads/client/flextab/messageBoxFollow.js +++ b/app/threads/client/flextab/messageBoxFollow.js @@ -1,11 +1,11 @@ import { Template } from 'meteor/templating'; +import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; import './messageBoxFollow.html'; -import { call } from '../../../ui-utils/client'; Template.messageBoxFollow.events({ 'click .js-follow'() { const { tmid } = this; - call('followMessage', { mid: tmid }); + callWithErrorHandling('followMessage', { mid: tmid }); }, }); diff --git a/app/threads/client/flextab/thread.js b/app/threads/client/flextab/thread.js index b74f61ce94b17..ecf508efbf9e3 100644 --- a/app/threads/client/flextab/thread.js +++ b/app/threads/client/flextab/thread.js @@ -8,7 +8,7 @@ import { Tracker } from 'meteor/tracker'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { chatMessages, ChatMessages } from '../../../ui'; -import { call, keyCodes } from '../../../ui-utils/client'; +import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; import { messageContext } from '../../../ui-utils/client/lib/messageContext'; import { upsertMessageBulk } from '../../../ui-utils/client/lib/RoomHistoryManager'; import { Messages } from '../../../models'; @@ -20,6 +20,7 @@ import { settings } from '../../../settings/client'; import { callbacks } from '../../../callbacks/client'; import './messageBoxFollow'; import { getCommonRoomEvents } from '../../../ui/client/views/app/lib/getCommonRoomEvents'; +import { keyCodes } from '../../../../client/lib/utils/keyCodes'; const sort = { ts: 1 }; @@ -243,7 +244,7 @@ Template.thread.onCreated(async function() { this.state.set('loading', true); - const messages = await call('getThreadMessages', { tmid }); + const messages = await callWithErrorHandling('getThreadMessages', { tmid }); upsertMessageBulk({ msgs: messages }, this.Threads); diff --git a/app/threads/client/messageAction/follow.js b/app/threads/client/messageAction/follow.js index b2f98475257c5..619fb226b5d7a 100644 --- a/app/threads/client/messageAction/follow.js +++ b/app/threads/client/messageAction/follow.js @@ -1,13 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import toastr from 'toastr'; import { Messages } from '../../../models/client'; import { settings } from '../../../settings/client'; -import { MessageAction, call } from '../../../ui-utils/client'; +import { MessageAction } from '../../../ui-utils/client'; import { messageArgs } from '../../../ui-utils/client/lib/messageArgs'; import { roomTypes } from '../../../utils/client'; +import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; +import { dispatchToastMessage } from '../../../../client/lib/toast'; Meteor.startup(function() { Tracker.autorun(() => { @@ -21,8 +22,8 @@ Meteor.startup(function() { context: ['message', 'message-mobile', 'threads'], async action() { const { msg } = messageArgs(this); - call('followMessage', { mid: msg._id }).then(() => - toastr.success(TAPi18n.__('You_followed_this_message')), + callWithErrorHandling('followMessage', { mid: msg._id }).then(() => + dispatchToastMessage({ type: 'success', message: TAPi18n.__('You_followed_this_message') }), ); }, condition({ msg: { _id, tmid, replies = [] }, u, room }, context) { diff --git a/app/threads/client/messageAction/unfollow.js b/app/threads/client/messageAction/unfollow.js index 44e73434445b3..23a96192ccb86 100644 --- a/app/threads/client/messageAction/unfollow.js +++ b/app/threads/client/messageAction/unfollow.js @@ -1,12 +1,13 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import toastr from 'toastr'; import { Messages } from '../../../models/client'; import { settings } from '../../../settings/client'; -import { MessageAction, call } from '../../../ui-utils/client'; +import { MessageAction } from '../../../ui-utils/client'; +import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; import { messageArgs } from '../../../ui-utils/client/lib/messageArgs'; +import { dispatchToastMessage } from '../../../../client/lib/toast'; Meteor.startup(function() { Tracker.autorun(() => { @@ -20,8 +21,8 @@ Meteor.startup(function() { context: ['message', 'message-mobile', 'threads'], async action() { const { msg } = messageArgs(this); - call('unfollowMessage', { mid: msg._id }).then(() => - toastr.success(TAPi18n.__('You_unfollowed_this_message')), + callWithErrorHandling('unfollowMessage', { mid: msg._id }).then(() => + dispatchToastMessage({ type: 'success', message: TAPi18n.__('You_unfollowed_this_message') }), ); }, condition({ msg: { _id, tmid, replies = [] }, u }, context) { diff --git a/app/threads/client/threads.css b/app/threads/client/threads.css index 8b3537eeaeb3b..73bb37795f67c 100644 --- a/app/threads/client/threads.css +++ b/app/threads/client/threads.css @@ -59,7 +59,6 @@ .message { & .thread-replied { - display: inline-flex; display: flex; @@ -135,7 +134,6 @@ } .thread-quote__message { - display: flex; overflow: hidden; diff --git a/app/threads/server/hooks/aftersavemessage.js b/app/threads/server/hooks/aftersavemessage.js index f08f922b6fec0..db9bbc3364bf9 100644 --- a/app/threads/server/hooks/aftersavemessage.js +++ b/app/threads/server/hooks/aftersavemessage.js @@ -64,7 +64,7 @@ export const processThreads = (message, room) => { }; Meteor.startup(function() { - settings.get('Threads_enabled', function(key, value) { + settings.watch('Threads_enabled', function(value) { if (!value) { callbacks.remove('afterSaveMessage', 'threads-after-save-message'); return; diff --git a/app/threads/server/methods/followMessage.js b/app/threads/server/methods/followMessage.js index 4ab2e59e29458..642c32e3b6337 100644 --- a/app/threads/server/methods/followMessage.js +++ b/app/threads/server/methods/followMessage.js @@ -4,6 +4,7 @@ import { check } from 'meteor/check'; import { Messages } from '../../../models/server'; import { RateLimiter } from '../../../lib/server'; import { settings } from '../../../settings/server'; +import { canAccessRoom } from '../../../authorization/server'; import { follow } from '../functions'; Meteor.methods({ @@ -24,8 +25,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-message', 'Invalid message', { method: 'followMessage' }); } - const room = Meteor.call('canAccessRoom', message.rid, uid); - if (!room) { + if (!canAccessRoom({ _id: message.rid }, { _id: uid })) { throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'followMessage' }); } diff --git a/app/threads/server/methods/unfollowMessage.js b/app/threads/server/methods/unfollowMessage.js index 743d9bd5e7195..a5ed0fa50c6a6 100644 --- a/app/threads/server/methods/unfollowMessage.js +++ b/app/threads/server/methods/unfollowMessage.js @@ -4,6 +4,7 @@ import { check } from 'meteor/check'; import { Messages } from '../../../models/server'; import { RateLimiter } from '../../../lib/server'; import { settings } from '../../../settings/server'; +import { canAccessRoom } from '../../../authorization/server'; import { unfollow } from '../functions'; Meteor.methods({ @@ -21,12 +22,11 @@ Meteor.methods({ const message = Messages.findOneById(mid, { fields: { rid: 1, tmid: 1 } }); if (!message) { - throw new Meteor.Error('error-invalid-message', 'Invalid message', { method: 'followMessage' }); + throw new Meteor.Error('error-invalid-message', 'Invalid message', { method: 'unfollowMessage' }); } - const room = Meteor.call('canAccessRoom', message.rid, uid); - if (!room) { - throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'followMessage' }); + if (!canAccessRoom({ _id: message.rid }, { _id: uid })) { + throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'unfollowMessage' }); } return unfollow({ rid: message.rid, tmid: message.tmid || message._id, uid }); diff --git a/app/threads/server/settings.js b/app/threads/server/settings.js deleted file mode 100644 index edc965aaca800..0000000000000 --- a/app/threads/server/settings.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../settings'; - -Meteor.startup(() => { - settings.addGroup('Threads', function() { - this.add('Threads_enabled', true, { - group: 'Threads', - i18nLabel: 'Enable', - type: 'boolean', - public: true, - }); - }); -}); diff --git a/app/threads/server/settings.ts b/app/threads/server/settings.ts new file mode 100644 index 0000000000000..4762d0170299f --- /dev/null +++ b/app/threads/server/settings.ts @@ -0,0 +1,10 @@ +import { settingsRegistry } from '../../settings/server'; + +settingsRegistry.addGroup('Threads', function() { + this.add('Threads_enabled', true, { + group: 'Threads', + i18nLabel: 'Enable', + type: 'boolean', + public: true, + }); +}); diff --git a/app/tokenpass/client/tokenpassChannelSettings.js b/app/tokenpass/client/tokenpassChannelSettings.js index 63525138d9f1b..f86bcd5330015 100644 --- a/app/tokenpass/client/tokenpassChannelSettings.js +++ b/app/tokenpass/client/tokenpassChannelSettings.js @@ -2,10 +2,11 @@ import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import toastr from 'toastr'; -import { t, handleError } from '../../utils'; +import { t } from '../../utils'; import { ChatRoom } from '../../models'; +import { handleError } from '../../../client/lib/utils/handleError'; +import { dispatchToastMessage } from '../../../client/lib/toast'; Template.channelSettings__tokenpass.helpers({ addDisabled() { @@ -93,7 +94,7 @@ Template.channelSettings__tokenpass.events({ i.balance.set(''); i.initial = tokenpass; [...i.findAll('input')].forEach((el) => { el.value = ''; }); - return toastr.success(TAPi18n.__('Room_tokenpass_config_changed_successfully')); + return dispatchToastMessage({ type: 'success', message: TAPi18n.__('Room_tokenpass_config_changed_successfully') }); }); }, 'click .js-cancel'(e, i) { diff --git a/app/tokenpass/server/startup.js b/app/tokenpass/server/startup.js index 6ff1ae50ce863..974bfc6bbc141 100644 --- a/app/tokenpass/server/startup.js +++ b/app/tokenpass/server/startup.js @@ -2,12 +2,12 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; import { updateUserTokenpassBalances } from './functions/updateUserTokenpassBalances'; -import { settings } from '../../settings'; +import { settingsRegistry } from '../../settings/server'; import { callbacks } from '../../callbacks'; import { validateTokenAccess } from './roomAccessValidator.compatibility'; import './roomAccessValidator.internalService'; -settings.addGroup('OAuth', function() { +settingsRegistry.addGroup('OAuth', function() { this.section('Tokenpass', function() { const enableQuery = { _id: 'Accounts_OAuth_Tokenpass', diff --git a/app/ui-cached-collection/client/models/CachedCollection.js b/app/ui-cached-collection/client/models/CachedCollection.js index eec9930c90d5a..506768f79ea49 100644 --- a/app/ui-cached-collection/client/models/CachedCollection.js +++ b/app/ui-cached-collection/client/models/CachedCollection.js @@ -10,8 +10,8 @@ import { Emitter } from '@rocket.chat/emitter'; import { callbacks } from '../../../callbacks'; import Notifications from '../../../notifications/client/lib/Notifications'; -import { getConfig } from '../../../ui-utils/client/config'; -import { callMethod } from '../../../ui-utils/client/lib/callMethod'; +import { getConfig } from '../../../../client/lib/utils/getConfig'; +import { call } from '../../../../client/lib/utils/call'; const wrap = (fn) => (...args) => new Promise((resolve, reject) => { fn(...args, (err, result) => { @@ -218,7 +218,7 @@ export class CachedCollection extends Emitter { async loadFromServer() { const startTime = new Date(); const lastTime = this.updatedAt; - const data = await callMethod(this.methodName); + const data = await call(this.methodName); this.log(`${ data.length } records loaded from server`); data.forEach((record) => { callbacks.run(`cachedCollection-loadFromServer-${ this.name }`, record, 'changed'); @@ -318,7 +318,7 @@ export class CachedCollection extends Emitter { this.log(`syncing from ${ this.updatedAt }`); - const data = await callMethod(this.syncMethodName, this.updatedAt); + const data = await call(this.syncMethodName, this.updatedAt); let changes = []; if (data.update && data.update.length > 0) { diff --git a/app/ui-login/client/login/form.js b/app/ui-login/client/login/form.js index 7cfa83b116f77..1df320ace8611 100644 --- a/app/ui-login/client/login/form.js +++ b/app/ui-login/client/login/form.js @@ -5,12 +5,12 @@ import { FlowRouter } from 'meteor/kadira:flow-router'; import { Session } from 'meteor/session'; import { Template } from 'meteor/templating'; import _ from 'underscore'; -import s from 'underscore.string'; -import toastr from 'toastr'; import { settings } from '../../../settings'; import { callbacks } from '../../../callbacks'; -import { t, handleError } from '../../../utils'; +import { t } from '../../../utils'; +import { handleError } from '../../../../client/lib/utils/handleError'; +import { dispatchToastMessage } from '../../../../client/lib/toast'; Template.loginForm.helpers({ userName() { @@ -73,7 +73,7 @@ Template.loginForm.helpers({ return settings.get('Accounts_ManuallyApproveNewUsers'); }, typedEmail() { - return s.trim(Template.instance().typedEmail); + return Template.instance().typedEmail?.trim(); }, }); @@ -86,23 +86,23 @@ Template.loginForm.events({ const state = instance.state.get(); if (formData) { if (state === 'email-verification') { - Meteor.call('sendConfirmationEmail', s.trim(formData.email), () => { + Meteor.call('sendConfirmationEmail', formData.email?.trim(), () => { instance.loading.set(false); callbacks.run('userConfirmationEmailRequested'); - toastr.success(t('We_have_sent_registration_email')); + dispatchToastMessage({ type: 'success', message: t('We_have_sent_registration_email') }); return instance.state.set('login'); }); return; } if (state === 'forgot-password') { - Meteor.call('sendForgotPasswordEmail', s.trim(formData.email), (err) => { + Meteor.call('sendForgotPasswordEmail', formData.email?.trim(), (err) => { if (err) { handleError(err); return instance.state.set('login'); } instance.loading.set(false); callbacks.run('userForgotPasswordEmailRequested'); - toastr.success(t('If_this_email_is_registered')); + dispatchToastMessage({ type: 'success', message: t('If_this_email_is_registered') }); return instance.state.set('login'); }); return; @@ -113,14 +113,14 @@ Template.loginForm.events({ instance.loading.set(false); if (error != null) { if (error.reason === 'Email already exists.') { - toastr.error(t('Email_already_exists')); + dispatchToastMessage({ type: 'error', message: t('Email_already_exists') }); } else { handleError(error); } return; } callbacks.run('userRegistered'); - return Meteor.loginWithPassword(s.trim(formData.email), formData.pass, function(error) { + return Meteor.loginWithPassword(formData.email?.trim(), formData.pass, function(error) { if (error && error.error === 'error-invalid-email') { return instance.state.set('wait-email-activation'); } if (error && error.error === 'error-user-is-not-activated') { @@ -137,24 +137,29 @@ Template.loginForm.events({ if (settings.get('CROWD_Enable')) { loginMethod = 'loginWithCrowd'; } - return Meteor[loginMethod](s.trim(formData.emailOrUsername), formData.pass, function(error) { + return Meteor[loginMethod](formData.emailOrUsername?.trim(), formData.pass, function(error) { instance.loading.set(false); if (error != null) { - if (error.error === 'error-user-is-not-activated') { - return toastr.error(t('Wait_activation_warning')); - } if (error.error === 'error-invalid-email') { - instance.typedEmail = formData.emailOrUsername; - return instance.state.set('email-verification'); - } if (error.error === 'error-user-is-not-activated') { - toastr.error(t('Wait_activation_warning')); - } else if (error.error === 'error-app-user-is-not-allowed-to-login') { - toastr.error(t('App_user_not_allowed_to_login')); - } else if (error.error === 'error-login-blocked-for-ip') { - toastr.error(t('Error_login_blocked_for_ip')); - } else if (error.error === 'error-login-blocked-for-user') { - toastr.error(t('Error_login_blocked_for_user')); - } else { - return toastr.error(t('User_not_found_or_incorrect_password')); + switch (error.error) { + case 'error-user-is-not-activated': + return dispatchToastMessage({ type: 'error', message: t('Wait_activation_warning') }); + case 'error-invalid-email': + instance.typedEmail = formData.emailOrUsername; + return instance.state.set('email-verification'); + case 'error-app-user-is-not-allowed-to-login': + dispatchToastMessage({ type: 'error', message: t('App_user_not_allowed_to_login') }); + break; + case 'error-login-blocked-for-ip': + dispatchToastMessage({ type: 'error', message: t('Error_login_blocked_for_ip') }); + break; + case 'error-login-blocked-for-user': + dispatchToastMessage({ type: 'error', message: t('Error_login_blocked_for_user') }); + break; + case 'error-license-user-limit-reached': + dispatchToastMessage({ type: 'error', message: t('error-license-user-limit-reached') }); + break; + default: + return dispatchToastMessage({ type: 'error', message: t('User_not_found_or_incorrect_password') }); } } Session.set('forceLogin', false); diff --git a/app/ui-login/client/login/services.js b/app/ui-login/client/login/services.js index 2b8a04f1cc6c6..93fede8756cc5 100644 --- a/app/ui-login/client/login/services.js +++ b/app/ui-login/client/login/services.js @@ -2,9 +2,9 @@ import { capitalize } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; import { Template } from 'meteor/templating'; import { ServiceConfiguration } from 'meteor/service-configuration'; -import toastr from 'toastr'; import { CustomOAuth } from '../../../custom-oauth'; +import { dispatchToastMessage } from '../../../../client/lib/toast'; Meteor.startup(function() { return ServiceConfiguration.configurations.find({ @@ -84,9 +84,9 @@ Template.loginServices.events({ if (error) { console.log(JSON.stringify(error)); if (error.reason) { - toastr.error(error.reason); + dispatchToastMessage({ type: 'error', message: error.reason }); } else { - toastr.error(error.message); + dispatchToastMessage({ type: 'error', message: error.message }); } } }); diff --git a/app/ui-login/client/username/username.js b/app/ui-login/client/username/username.js index 0bd06fe5dd808..5ab2ec76b9ae3 100644 --- a/app/ui-login/client/username/username.js +++ b/app/ui-login/client/username/username.js @@ -3,12 +3,12 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; import { Tracker } from 'meteor/tracker'; import _ from 'underscore'; -import toastr from 'toastr'; import { settings } from '../../../settings'; import { Button } from '../../../ui'; import { t } from '../../../utils'; import { callbacks } from '../../../callbacks'; +import { dispatchToastMessage } from '../../../../client/lib/toast'; Template.username.onCreated(function() { const self = this; @@ -148,7 +148,7 @@ Template.username.events({ Meteor.call('saveCustomFields', formData, function(err) { if (err != null) { - toastr.error(err.error); + dispatchToastMessage({ type: 'error', message: err.error }); } }); diff --git a/app/ui-master/client/body.js b/app/ui-master/client/body.js index 649e3cadab597..5f246e15365d5 100644 --- a/app/ui-master/client/body.js +++ b/app/ui-master/client/body.js @@ -7,12 +7,14 @@ import { Template } from 'meteor/templating'; import { t } from '../../utils/client'; import { chatMessages } from '../../ui'; -import { Layout, popover, fireGlobalEvent, RoomManager } from '../../ui-utils'; +import { popover, RoomManager } from '../../ui-utils'; import { settings } from '../../settings'; import { ChatSubscription } from '../../models'; import './body.html'; import { imperativeModal } from '../../../client/lib/imperativeModal'; import GenericModal from '../../../client/components/GenericModal'; +import { fireGlobalEvent } from '../../../client/lib/utils/fireGlobalEvent'; +import { isLayoutEmbedded } from '../../../client/lib/utils/isLayoutEmbedded'; Template.body.onRendered(function() { new Clipboard('.clipboard'); @@ -99,7 +101,7 @@ Template.body.onRendered(function() { }; this.autorun(() => { - if (Layout.isEmbedded()) { + if (isLayoutEmbedded()) { $(document.body).on('click', 'a', handleMessageLinkClick); } else { $(document.body).off('click', 'a', handleMessageLinkClick); diff --git a/app/ui-master/client/main.html b/app/ui-master/client/main.html index 201a9ad78b63e..587453a419366 100644 --- a/app/ui-master/client/main.html +++ b/app/ui-master/client/main.html @@ -23,7 +23,6 @@ {{else}} - {{> videoCall overlay=true}}