diff --git a/.docker/Dockerfile.rhel b/.docker/Dockerfile.rhel index d121ecf19614..123349efe1a0 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.1 +ENV RC_VERSION 4.0.3 MAINTAINER buildmaster@rocket.chat diff --git a/.github/history-manual.json b/.github/history-manual.json index 27ecf455e466..de60527e3d41 100644 --- a/.github/history-manual.json +++ b/.github/history-manual.json @@ -115,5 +115,13 @@ "matheusbsilva137", "pierre-lehnen-rc" ] + }], + "3.18.2": [{ + "title": "[FIX] Security Hotfix (https://docs.rocket.chat/guides/security/security-updates)", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "pierre-lehnen-rc" + ] }] } diff --git a/.github/history.json b/.github/history.json index 0aab8474c9e6..8c882ffc2cd8 100644 --- a/.github/history.json +++ b/.github/history.json @@ -64551,6 +64551,1027 @@ ], "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", @@ -64562,6 +65583,358 @@ "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", @@ -64592,6 +65965,259 @@ "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.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": "23378", + "title": "[FIX] Users' `roles` and `type` being reset to default on LDAP DataSync", + "userLogin": "matheusbsilva137", + "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": "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": "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" + ] } ] } diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 375cf6de4f2e..b34315f71a1d 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 @@ -192,7 +185,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 +242,10 @@ jobs: run: | npm install - - name: Test + - name: Unit Test + run: npm run testunit + + - name: E2E Test env: TEST_MODE: "true" MONGO_URL: mongodb://localhost:27017/rocketchat @@ -260,7 +256,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: @@ -273,132 +269,132 @@ jobs: # commit: true # token: ${{ secrets.GITHUB_TOKEN }} - # build-image-pr: - # runs-on: ubuntu-latest - # if: github.event.pull_request.head.repo.full_name == github.repository - - # strategy: - # matrix: - # release: ["official", "preview"] - - # steps: - # - uses: actions/checkout@v2 - - # - name: Login to GitHub Container Registry - # uses: docker/login-action@v1 - # with: - # registry: ghcr.io - # username: ${{ secrets.CR_USER }} - # password: ${{ secrets.CR_PAT }} - - # - name: Free disk space - # run: | - # sudo swapoff -a - # sudo rm -f /swapfile - # sudo apt clean - # docker rmi $(docker image ls -aq) - # df -h - - # # - name: Cache node modules - # # id: cache-nodemodules - # # uses: actions/cache@v2 - # # with: - # # path: | - # # ./node_modules - # # ./ee/server/services/node_modules - # # key: ${{ runner.OS }}-node_modules-4-${{ hashFiles('**/package-lock.json', '.github/workflows/build_and_test.yml') }} - - # - name: Cache meteor local - # uses: actions/cache@v2 - # with: - # path: ./.meteor/local - # key: ${{ runner.OS }}-meteor_cache-${{ hashFiles('.meteor/versions', '.github/workflows/build_and_test.yml') }} - - # - name: Cache meteor - # uses: actions/cache@v2 - # with: - # path: ~/.meteor - # key: ${{ runner.OS }}-meteor-${{ hashFiles('.meteor/release', '.github/workflows/build_and_test.yml') }} - - # - name: Use Node.js 12.22.1 - # uses: actions/setup-node@v2 - # with: - # node-version: "12.22.1" - - # - name: Install Meteor - # run: | - # # Restore bin from cache - # set +e - # METEOR_SYMLINK_TARGET=$(readlink ~/.meteor/meteor) - # METEOR_TOOL_DIRECTORY=$(dirname "$METEOR_SYMLINK_TARGET") - # set -e - # LAUNCHER=$HOME/.meteor/$METEOR_TOOL_DIRECTORY/scripts/admin/launch-meteor - # if [ -e $LAUNCHER ] - # then - # echo "Cached Meteor bin found, restoring it" - # sudo cp "$LAUNCHER" "/usr/local/bin/meteor" - # else - # echo "No cached Meteor bin found." - # fi - - # # only install meteor if bin isn't found - # command -v meteor >/dev/null 2>&1 || curl https://install.meteor.com | sed s/--progress-bar/-sL/g | /bin/sh - - # - name: Versions - # run: | - # npm --versions - # node -v - # meteor --version - # meteor npm --versions - # meteor node -v - # git version - - # - name: npm install - # # if: steps.cache-nodemodules.outputs.cache-hit != 'true' - # run: | - # meteor npm install - - # # To reduce memory need during actual build, build the packages solely first - # # - name: Build a Meteor cache - # # run: | - # # # to do this we can clear the main files and it build the rest - # # echo "" > server/main.js - # # echo "" > client/main.js - # # 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 - - # - name: Build Rocket.Chat - # run: | - # meteor build --server-only --directory /tmp/build-pr - - # - name: Build Docker image for PRs - # run: | - # cd /tmp/build-pr - - # LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") - # IMAGE_NAME="rocket.chat" - # if [[ '${{ matrix.release }}' = 'preview' ]]; then - # IMAGE_NAME="${IMAGE_NAME}.preview" - # fi; - - # IMAGE_NAME="ghcr.io/${LOWERCASE_REPOSITORY}/${IMAGE_NAME}:pr-${{ github.event.number }}" - - # echo "Build official Docker image ${IMAGE_NAME}" - - # DOCKER_PATH="${GITHUB_WORKSPACE}/.docker" - # if [[ '${{ matrix.release }}' = 'preview' ]]; then - # DOCKER_PATH="${DOCKER_PATH}-mongo" - # fi; - - # echo "Build ${{ matrix.release }} Docker image" - # cp ${DOCKER_PATH}/Dockerfile . - # if [ -e ${DOCKER_PATH}/entrypoint.sh ]; then - # cp ${DOCKER_PATH}/entrypoint.sh . - # fi; - - # docker build -t $IMAGE_NAME . - # docker push $IMAGE_NAME +# build-image-pr: +# runs-on: ubuntu-20.04 +# if: github.event.pull_request.head.repo.full_name == github.repository + +# strategy: +# matrix: +# release: ["official", "preview"] + +# steps: +# - uses: actions/checkout@v2 + +# - name: Login to GitHub Container Registry +# uses: docker/login-action@v1 +# with: +# registry: ghcr.io +# username: ${{ secrets.CR_USER }} +# password: ${{ secrets.CR_PAT }} + +# - name: Free disk space +# run: | +# sudo swapoff -a +# sudo rm -f /swapfile +# sudo apt clean +# docker rmi $(docker image ls -aq) +# df -h + +# # - name: Cache node modules +# # id: cache-nodemodules +# # uses: actions/cache@v2 +# # with: +# # path: | +# # ./node_modules +# # ./ee/server/services/node_modules +# # key: ${{ runner.OS }}-node_modules-4-${{ hashFiles('**/package-lock.json', '.github/workflows/build_and_test.yml') }} + +# - name: Cache meteor local +# uses: actions/cache@v2 +# with: +# path: ./.meteor/local +# key: ${{ runner.OS }}-meteor_cache-${{ hashFiles('.meteor/versions', '.github/workflows/build_and_test.yml') }} + +# - name: Cache meteor +# uses: actions/cache@v2 +# with: +# path: ~/.meteor +# key: ${{ runner.OS }}-meteor-${{ hashFiles('.meteor/release', '.github/workflows/build_and_test.yml') }} + +# - name: Use Node.js 12.22.1 +# uses: actions/setup-node@v2 +# with: +# node-version: "12.22.1" + +# - name: Install Meteor +# run: | +# # Restore bin from cache +# set +e +# METEOR_SYMLINK_TARGET=$(readlink ~/.meteor/meteor) +# METEOR_TOOL_DIRECTORY=$(dirname "$METEOR_SYMLINK_TARGET") +# set -e +# LAUNCHER=$HOME/.meteor/$METEOR_TOOL_DIRECTORY/scripts/admin/launch-meteor +# if [ -e $LAUNCHER ] +# then +# echo "Cached Meteor bin found, restoring it" +# sudo cp "$LAUNCHER" "/usr/local/bin/meteor" +# else +# echo "No cached Meteor bin found." +# fi + +# # only install meteor if bin isn't found +# command -v meteor >/dev/null 2>&1 || curl https://install.meteor.com | sed s/--progress-bar/-sL/g | /bin/sh + +# - name: Versions +# run: | +# npm --versions +# node -v +# meteor --version +# meteor npm --versions +# meteor node -v +# git version + +# - name: npm install +# # if: steps.cache-nodemodules.outputs.cache-hit != 'true' +# run: | +# meteor npm install + +# # To reduce memory need during actual build, build the packages solely first +# # - name: Build a Meteor cache +# # run: | +# # # to do this we can clear the main files and it build the rest +# # echo "" > server/main.js +# # echo "" > client/main.js +# # 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 + +# - name: Build Rocket.Chat +# run: | +# meteor build --server-only --directory /tmp/build-pr + +# - name: Build Docker image for PRs +# run: | +# cd /tmp/build-pr + +# LOWERCASE_REPOSITORY=$(echo "${{ github.repository_owner }}" | tr "[:upper:]" "[:lower:]") +# IMAGE_NAME="rocket.chat" +# if [[ '${{ matrix.release }}' = 'preview' ]]; then +# IMAGE_NAME="${IMAGE_NAME}.preview" +# fi; + +# IMAGE_NAME="ghcr.io/${LOWERCASE_REPOSITORY}/${IMAGE_NAME}:pr-${{ github.event.number }}" + +# echo "Build official Docker image ${IMAGE_NAME}" + +# DOCKER_PATH="${GITHUB_WORKSPACE}/.docker" +# if [[ '${{ matrix.release }}' = 'preview' ]]; then +# DOCKER_PATH="${DOCKER_PATH}-mongo" +# fi; + +# echo "Build ${{ matrix.release }} Docker image" +# cp ${DOCKER_PATH}/Dockerfile . +# if [ -e ${DOCKER_PATH}/entrypoint.sh ]; then +# cp ${DOCKER_PATH}/entrypoint.sh . +# fi; + +# docker build -t $IMAGE_NAME . +# docker push $IMAGE_NAME deploy: runs-on: ubuntu-latest diff --git a/.github/workflows/stale.yml.widechatDisabled b/.github/workflows/stale.yml.widechatDisabled index bd7459c7025f..b7970cf1bf5b 100644 --- a/.github/workflows/stale.yml.widechatDisabled +++ b/.github/workflows/stale.yml.widechatDisabled @@ -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 5ccfd361beed..8f8e7a09a9aa 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,2 +1,2 @@ meteor npm run lint && \ -meteor npm run testunit -- --exclude app/models/server/models/Sessions.tests.js +meteor npm run testunit diff --git a/.meteor/packages b/.meteor/packages index 12471574ad2b..7ce41971bcc1 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 70a4d6140cc3..958e6359313a 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 diff --git a/.snapcraft/resources/prepareRocketChat b/.snapcraft/resources/prepareRocketChat index 70647e4e77ea..afe64c8a04f7 100755 --- a/.snapcraft/resources/prepareRocketChat +++ b/.snapcraft/resources/prepareRocketChat @@ -1,6 +1,6 @@ #!/bin/bash -curl -SLf "https://releases.rocket.chat/3.18.1/download/" -o rocket.chat.tgz +curl -SLf "https://releases.rocket.chat/4.0.3/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 7c767953b61e..fe717d823b82 100644 --- a/.snapcraft/snap/snapcraft.yaml +++ b/.snapcraft/snap/snapcraft.yaml @@ -7,7 +7,7 @@ # 5. `snapcraft snap` name: rocketchat-server -version: 3.18.1 +version: 4.0.3 summary: Rocket.Chat server description: Have your own Slack like online chat, built with Meteor. https://rocket.chat/ confinement: strict diff --git a/HISTORY.md b/HISTORY.md index 33c47d77e493..cf3ad29eab91 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,580 @@ +# 4.0.3 +`2021-10-18 · 2 🐛 · 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 + + +- **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)) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@thassiov](https://github.com/thassiov) + +# 4.0.2 +`2021-10-14 · 4 🐛 · 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 + + +- **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 + +### 👩‍💻👨‍💻 Contributors 😍 + +- [@Aman-Maheshwari](https://github.com/Aman-Maheshwari) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@murtaza98](https://github.com/murtaza98) + +# 4.0.1 +`2021-10-06 · 7 🐛 · 1 🔍 · 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)) + +
+🔍 Minor changes + + +- Chore: Update Apps-Engine version ([#23375](https://github.com/RocketChat/Rocket.Chat/pull/23375)) + +
+ +### 👩‍💻👨‍💻 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 🔍 · 4 👩‍💻👨‍💻` + +### Engine versions +- Node: `12.22.1` +- NPM: `6.14.1` +- MongoDB: `3.4, 3.6, 4.0, 4.2` +- Apps-Engine: `1.27.1` + +### 🐛 Bug fixes + + +- Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) + +- Update visitor info on email reception based on current inbox settings ([#23280](https://github.com/RocketChat/Rocket.Chat/pull/23280)) + +
+🔍 Minor changes + + +- 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)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@KevLehman](https://github.com/KevLehman) +- [@murtaza98](https://github.com/murtaza98) +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@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` @@ -22,17 +596,22 @@ 🔍 Minor changes -- Regression: Auth banner for EE ([#23091](https://github.com/RocketChat/Rocket.Chat/pull/23091)) +- Regression: Auth banner for EE ([#23091](https://github.com/RocketChat/Rocket.Chat/pull/23091) by [@g-thome](https://github.com/g-thome)) 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 😍 + +- [@g-thome](https://github.com/g-thome) + ### 👩‍💻👨‍💻 Core Team 🤓 - [@KevLehman](https://github.com/KevLehman) - [@casalsgh](https://github.com/casalsgh) -- [@g-thome](https://github.com/g-thome) - [@sampaiodiego](https://github.com/sampaiodiego) # 3.18.0 @@ -53,7 +632,7 @@ - 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)) +- Banner for the updates regarding authentication services ([#23055](https://github.com/RocketChat/Rocket.Chat/pull/23055) by [@g-thome](https://github.com/g-thome)) Add a banner to inform admins about future authentication changes. This banner targets servers that use some sort of authentication service since they're the ones which this update concerns the most. @@ -163,7 +742,7 @@ - User presence being processes even if presence monitor was disabled ([#22927](https://github.com/RocketChat/Rocket.Chat/pull/22927)) -- users registered via third party apps bypass custom required fields ([#22396](https://github.com/RocketChat/Rocket.Chat/pull/22396)) +- users registered via third party apps bypass custom required fields ([#22396](https://github.com/RocketChat/Rocket.Chat/pull/22396) by [@g-thome](https://github.com/g-thome)) moves the custom fields from the initial registration form to the "pick a username" screen so that everyone is forced to fill the custom required fields @@ -234,6 +813,7 @@ - [@aditya-mitra](https://github.com/aditya-mitra) - [@eltociear](https://github.com/eltociear) - [@epif4nio](https://github.com/epif4nio) +- [@g-thome](https://github.com/g-thome) - [@hrahul2605](https://github.com/hrahul2605) - [@jsm84](https://github.com/jsm84) - [@nmagedman](https://github.com/nmagedman) @@ -245,7 +825,6 @@ - [@MartinSchoeler](https://github.com/MartinSchoeler) - [@d-gubert](https://github.com/d-gubert) - [@dougfabris](https://github.com/dougfabris) -- [@g-thome](https://github.com/g-thome) - [@gabriellsh](https://github.com/gabriellsh) - [@ggazzo](https://github.com/ggazzo) - [@marceloschmidt](https://github.com/marceloschmidt) @@ -367,7 +946,7 @@ ![image](https://user-images.githubusercontent.com/27704687/123525502-8558bd80-d6a7-11eb-8211-12633cb3b5c6.png) -- Federation setup ([#22208](https://github.com/RocketChat/Rocket.Chat/pull/22208)) +- Federation setup ([#22208](https://github.com/RocketChat/Rocket.Chat/pull/22208) by [@g-thome](https://github.com/g-thome)) - Logout other user endpoint ([#22661](https://github.com/RocketChat/Rocket.Chat/pull/22661)) @@ -613,7 +1192,7 @@ - Sort AutocompleteDepartmentsMultiple ([#22419](https://github.com/RocketChat/Rocket.Chat/pull/22419)) -- status message won't show up for other users ([#22110](https://github.com/RocketChat/Rocket.Chat/pull/22110)) +- status message won't show up for other users ([#22110](https://github.com/RocketChat/Rocket.Chat/pull/22110) by [@g-thome](https://github.com/g-thome)) replace the current blaze block that queries the local session store by a react component that fetches memoized user data @@ -706,7 +1285,7 @@ - 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)) +- Regression: Federation warnings on ci ([#22765](https://github.com/RocketChat/Rocket.Chat/pull/22765) by [@g-thome](https://github.com/g-thome)) fix some linting warnings on federation modal @@ -784,6 +1363,7 @@ - [@cprice-kgi](https://github.com/cprice-kgi) - [@dependabot[bot]](https://github.com/dependabot[bot]) - [@ericrosenthal](https://github.com/ericrosenthal) +- [@g-thome](https://github.com/g-thome) - [@lucassartor](https://github.com/lucassartor) - [@nmagedman](https://github.com/nmagedman) - [@rafaelblink](https://github.com/rafaelblink) @@ -797,7 +1377,6 @@ - [@alansikora](https://github.com/alansikora) - [@d-gubert](https://github.com/d-gubert) - [@dougfabris](https://github.com/dougfabris) -- [@g-thome](https://github.com/g-thome) - [@gabriellsh](https://github.com/gabriellsh) - [@ggazzo](https://github.com/ggazzo) - [@matheusbsilva137](https://github.com/matheusbsilva137) @@ -846,9 +1425,12 @@ - Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) -### 👩‍💻👨‍💻 Core Team 🤓 +### 👩‍💻👨‍💻 Contributors 😍 - [@g-thome](https://github.com/g-thome) + +### 👩‍💻👨‍💻 Core Team 🤓 + - [@ggazzo](https://github.com/ggazzo) - [@matheusbsilva137](https://github.com/matheusbsilva137) - [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) @@ -1480,7 +2062,7 @@ - Add support to queries within the `name`, `username` and `status` parameters. -- Add team members to channel when set as auto join ([#22056](https://github.com/RocketChat/Rocket.Chat/pull/22056)) +- Add team members to channel when set as auto join ([#22056](https://github.com/RocketChat/Rocket.Chat/pull/22056) by [@g-thome](https://github.com/g-thome)) Create a channels.autojoin endpoint to set a channel as autojoin. Also make it so that old team members join this channel automatically @@ -1953,7 +2535,7 @@ - Release 3.14.4 ([#22181](https://github.com/RocketChat/Rocket.Chat/pull/22181)) -- Remove memory leak from userData ([#22094](https://github.com/RocketChat/Rocket.Chat/pull/22094)) +- Remove memory leak from userData ([#22094](https://github.com/RocketChat/Rocket.Chat/pull/22094) by [@g-thome](https://github.com/g-thome)) - String helpers ([#21988](https://github.com/RocketChat/Rocket.Chat/pull/21988)) @@ -1975,6 +2557,7 @@ - [@bhavayAnand9](https://github.com/bhavayAnand9) - [@dependabot-preview[bot]](https://github.com/dependabot-preview[bot]) - [@dependabot[bot]](https://github.com/dependabot[bot]) +- [@g-thome](https://github.com/g-thome) - [@lucassartor](https://github.com/lucassartor) - [@rafaelblink](https://github.com/rafaelblink) - [@renancleyson-dev](https://github.com/renancleyson-dev) @@ -1990,7 +2573,6 @@ - [@MartinSchoeler](https://github.com/MartinSchoeler) - [@d-gubert](https://github.com/d-gubert) - [@dougfabris](https://github.com/dougfabris) -- [@g-thome](https://github.com/g-thome) - [@gabriellsh](https://github.com/gabriellsh) - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) @@ -2109,10 +2691,13 @@ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@g-thome](https://github.com/g-thome) + ### 👩‍💻👨‍💻 Core Team 🤓 - [@KevLehman](https://github.com/KevLehman) -- [@g-thome](https://github.com/g-thome) - [@sampaiodiego](https://github.com/sampaiodiego) # 3.14.1 @@ -2238,7 +2823,7 @@ ![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)) +- REST endpoint `teams.update` ([#21134](https://github.com/RocketChat/Rocket.Chat/pull/21134) by [@g-thome](https://github.com/g-thome)) add teams.update endpoint @@ -2263,7 +2848,7 @@ ![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)) +- 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)) add permission check for each room @@ -2405,7 +2990,7 @@ 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)) +- Message link null corrupts message rendering ([#21579](https://github.com/RocketChat/Rocket.Chat/pull/21579) by [@g-thome](https://github.com/g-thome)) Additional checks on message_link field before rendering message contents @@ -2512,7 +3097,7 @@ - Bump Livechat Version ([#21694](https://github.com/RocketChat/Rocket.Chat/pull/21694)) -- Chore: Add tests for teams.update REST endpoint ([#21653](https://github.com/RocketChat/Rocket.Chat/pull/21653)) +- Chore: Add tests for teams.update REST endpoint ([#21653](https://github.com/RocketChat/Rocket.Chat/pull/21653) by [@g-thome](https://github.com/g-thome)) add more tests to this endpoint @@ -2605,6 +3190,7 @@ - [@Jeanstaquet](https://github.com/Jeanstaquet) - [@Kartik18g](https://github.com/Kartik18g) - [@cuonghuunguyen](https://github.com/cuonghuunguyen) +- [@g-thome](https://github.com/g-thome) - [@im-adithya](https://github.com/im-adithya) - [@joshi008](https://github.com/joshi008) - [@lolimay](https://github.com/lolimay) @@ -2621,7 +3207,6 @@ - [@MartinSchoeler](https://github.com/MartinSchoeler) - [@d-gubert](https://github.com/d-gubert) - [@dougfabris](https://github.com/dougfabris) -- [@g-thome](https://github.com/g-thome) - [@gabriellsh](https://github.com/gabriellsh) - [@ggazzo](https://github.com/ggazzo) - [@graywolf336](https://github.com/graywolf336) @@ -2843,7 +3428,7 @@ - Quick action buttons for Omnichannel ([#21123](https://github.com/RocketChat/Rocket.Chat/pull/21123) by [@rafaelblink](https://github.com/rafaelblink)) -- Teams ([#20966](https://github.com/RocketChat/Rocket.Chat/pull/20966)) +- Teams ([#20966](https://github.com/RocketChat/Rocket.Chat/pull/20966) by [@g-thome](https://github.com/g-thome)) ## Teams @@ -3168,7 +3753,7 @@ - Regression: Add isLastOwner property on teams.listRoomsOfUser endpoint ([#21323](https://github.com/RocketChat/Rocket.Chat/pull/21323)) -- Regression: Add number of team members to teams.list and teams.listAll ([#21361](https://github.com/RocketChat/Rocket.Chat/pull/21361)) +- Regression: Add number of team members to teams.list and teams.listAll ([#21361](https://github.com/RocketChat/Rocket.Chat/pull/21361) by [@g-thome](https://github.com/g-thome)) - Regression: Add scope to permission checks in Team's endpoints ([#21369](https://github.com/RocketChat/Rocket.Chat/pull/21369)) @@ -3315,6 +3900,7 @@ - [@cuonghuunguyen](https://github.com/cuonghuunguyen) - [@cyberShaw](https://github.com/cyberShaw) - [@fcecagno](https://github.com/fcecagno) +- [@g-thome](https://github.com/g-thome) - [@im-adithya](https://github.com/im-adithya) - [@lolimay](https://github.com/lolimay) - [@lucassartor](https://github.com/lucassartor) @@ -3333,7 +3919,6 @@ - [@alansikora](https://github.com/alansikora) - [@d-gubert](https://github.com/d-gubert) - [@dougfabris](https://github.com/dougfabris) -- [@g-thome](https://github.com/g-thome) - [@gabriellsh](https://github.com/gabriellsh) - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) @@ -3399,10 +3984,13 @@ - Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) +### 👩‍💻👨‍💻 Contributors 😍 + +- [@g-thome](https://github.com/g-thome) + ### 👩‍💻👨‍💻 Core Team 🤓 - [@KevLehman](https://github.com/KevLehman) -- [@g-thome](https://github.com/g-thome) - [@matheusbsilva137](https://github.com/matheusbsilva137) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -3468,7 +4056,7 @@ ![image](https://user-images.githubusercontent.com/27704687/106945019-1386d400-6706-11eb-90db-c12b50f260d5.png) -- Statistics about language usage ([#20832](https://github.com/RocketChat/Rocket.Chat/pull/20832)) +- Statistics about language usage ([#20832](https://github.com/RocketChat/Rocket.Chat/pull/20832) by [@g-thome](https://github.com/g-thome)) track what languages get picked the most as preferred ui language. @@ -3626,7 +4214,7 @@ 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)) +- CORS config not accepting multiple origins ([#20696](https://github.com/RocketChat/Rocket.Chat/pull/20696) by [@g-thome](https://github.com/g-thome)) always include only one value in access-control-allow-origin @@ -3792,7 +4380,7 @@ Changed the height of the div container. -- Room owner not being able to override global retention policy ([#20727](https://github.com/RocketChat/Rocket.Chat/pull/20727)) +- Room owner not being able to override global retention policy ([#20727](https://github.com/RocketChat/Rocket.Chat/pull/20727) by [@g-thome](https://github.com/g-thome)) use correct permissions to check if room owner can override global retention policy @@ -3954,6 +4542,7 @@ - [@abrom](https://github.com/abrom) - [@aditya-mitra](https://github.com/aditya-mitra) - [@bhavayAnand9](https://github.com/bhavayAnand9) +- [@g-thome](https://github.com/g-thome) - [@im-adithya](https://github.com/im-adithya) - [@lolimay](https://github.com/lolimay) - [@lucassartor](https://github.com/lucassartor) @@ -3968,7 +4557,6 @@ - [@MartinSchoeler](https://github.com/MartinSchoeler) - [@d-gubert](https://github.com/d-gubert) - [@dougfabris](https://github.com/dougfabris) -- [@g-thome](https://github.com/g-thome) - [@gabriellsh](https://github.com/gabriellsh) - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) @@ -4017,13 +4605,16 @@ - Prevent Message Attachment rendering ([#20860](https://github.com/RocketChat/Rocket.Chat/pull/20860)) -- Room owner not being able to override global retention policy ([#20727](https://github.com/RocketChat/Rocket.Chat/pull/20727)) +- Room owner not being able to override global retention policy ([#20727](https://github.com/RocketChat/Rocket.Chat/pull/20727) by [@g-thome](https://github.com/g-thome)) use correct permissions to check if room owner can override global retention policy -### 👩‍💻👨‍💻 Core Team 🤓 +### 👩‍💻👨‍💻 Contributors 😍 - [@g-thome](https://github.com/g-thome) + +### 👩‍💻👨‍💻 Core Team 🤓 + - [@ggazzo](https://github.com/ggazzo) - [@renatobecker](https://github.com/renatobecker) @@ -4717,11 +5308,11 @@ - Omnichannel Contact Center (Directory) ([#19931](https://github.com/RocketChat/Rocket.Chat/pull/19931) by [@rafaelblink](https://github.com/rafaelblink)) -- REST Endpoint `instances.get` ([#19926](https://github.com/RocketChat/Rocket.Chat/pull/19926)) +- REST Endpoint `instances.get` ([#19926](https://github.com/RocketChat/Rocket.Chat/pull/19926) by [@g-thome](https://github.com/g-thome)) Returns an array of instances on the cluster. -- REST endpoints to add and retrieve Enterprise licenses ([#19925](https://github.com/RocketChat/Rocket.Chat/pull/19925)) +- REST endpoints to add and retrieve Enterprise licenses ([#19925](https://github.com/RocketChat/Rocket.Chat/pull/19925) by [@g-thome](https://github.com/g-thome)) - Update Checker Description ([#19892](https://github.com/RocketChat/Rocket.Chat/pull/19892)) @@ -4771,9 +5362,9 @@ - File Tab Order ([#19729](https://github.com/RocketChat/Rocket.Chat/pull/19729)) -- Forgot password endpoint return status ([#19842](https://github.com/RocketChat/Rocket.Chat/pull/19842)) +- Forgot password endpoint return status ([#19842](https://github.com/RocketChat/Rocket.Chat/pull/19842) by [@g-thome](https://github.com/g-thome)) -- Group DMs title when user changes his/her name ([#19834](https://github.com/RocketChat/Rocket.Chat/pull/19834)) +- Group DMs title when user changes his/her name ([#19834](https://github.com/RocketChat/Rocket.Chat/pull/19834) by [@g-thome](https://github.com/g-thome)) - Hightlights validation on Account Preferences page ([#19902](https://github.com/RocketChat/Rocket.Chat/pull/19902) by [@aKn1ghtOut](https://github.com/aKn1ghtOut)) @@ -4781,7 +5372,7 @@ 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)) +- Image preview for image URLs on messages ([#19734](https://github.com/RocketChat/Rocket.Chat/pull/19734) by [@g-thome](https://github.com/g-thome)) - Issue with oembed ([#19923](https://github.com/RocketChat/Rocket.Chat/pull/19923)) @@ -4859,9 +5450,9 @@ - Message parsing and rendering - Phase 1 ([#19654](https://github.com/RocketChat/Rocket.Chat/pull/19654)) -- Regression: "My Account" page doesn't load ([#19753](https://github.com/RocketChat/Rocket.Chat/pull/19753)) +- Regression: "My Account" page doesn't load ([#19753](https://github.com/RocketChat/Rocket.Chat/pull/19753) by [@g-thome](https://github.com/g-thome)) -- Regression: Add currently running instance to instances.get endpoint ([#19955](https://github.com/RocketChat/Rocket.Chat/pull/19955)) +- Regression: Add currently running instance to instances.get endpoint ([#19955](https://github.com/RocketChat/Rocket.Chat/pull/19955) by [@g-thome](https://github.com/g-thome)) - Regression: Add Members showing the wrong template ([#19748](https://github.com/RocketChat/Rocket.Chat/pull/19748)) @@ -4901,7 +5492,7 @@ 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)) +- Regression: polishing licenses endpoints ([#19981](https://github.com/RocketChat/Rocket.Chat/pull/19981) by [@g-thome](https://github.com/g-thome)) - Regression: roomInfo folder structure ([#19787](https://github.com/RocketChat/Rocket.Chat/pull/19787)) @@ -4926,6 +5517,7 @@ - [@aKn1ghtOut](https://github.com/aKn1ghtOut) - [@andykrohg](https://github.com/andykrohg) - [@dependabot[bot]](https://github.com/dependabot[bot]) +- [@g-thome](https://github.com/g-thome) - [@rafaelblink](https://github.com/rafaelblink) - [@youssef-md](https://github.com/youssef-md) @@ -4934,7 +5526,6 @@ - [@MartinSchoeler](https://github.com/MartinSchoeler) - [@alansikora](https://github.com/alansikora) - [@dougfabris](https://github.com/dougfabris) -- [@g-thome](https://github.com/g-thome) - [@gabriellsh](https://github.com/gabriellsh) - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) @@ -5018,18 +5609,18 @@ - Download my data with file uploads ([#19862](https://github.com/RocketChat/Rocket.Chat/pull/19862)) -- Forgot password endpoint return status ([#19842](https://github.com/RocketChat/Rocket.Chat/pull/19842)) +- Forgot password endpoint return status ([#19842](https://github.com/RocketChat/Rocket.Chat/pull/19842) by [@g-thome](https://github.com/g-thome)) - Some apps were not correctly enabled during startup in HA environments ([#19763](https://github.com/RocketChat/Rocket.Chat/pull/19763)) ### 👩‍💻👨‍💻 Contributors 😍 +- [@g-thome](https://github.com/g-thome) - [@rafaelblink](https://github.com/rafaelblink) ### 👩‍💻👨‍💻 Core Team 🤓 - [@MartinSchoeler](https://github.com/MartinSchoeler) -- [@g-thome](https://github.com/g-thome) - [@renatobecker](https://github.com/renatobecker) - [@sampaiodiego](https://github.com/sampaiodiego) - [@thassiov](https://github.com/thassiov) @@ -5048,7 +5639,7 @@ - Exception on certain login cases including SAML -- Image preview for image URLs on messages ([#19734](https://github.com/RocketChat/Rocket.Chat/pull/19734)) +- Image preview for image URLs on messages ([#19734](https://github.com/RocketChat/Rocket.Chat/pull/19734) by [@g-thome](https://github.com/g-thome)) - Sidebar presence will now correctly update for Omnichannel rooms ([#19746](https://github.com/RocketChat/Rocket.Chat/pull/19746)) @@ -5056,10 +5647,13 @@ - Startup error when using MongoDB with a password containing special characters ([#19749](https://github.com/RocketChat/Rocket.Chat/pull/19749)) +### 👩‍💻👨‍💻 Contributors 😍 + +- [@g-thome](https://github.com/g-thome) + ### 👩‍💻👨‍💻 Core Team 🤓 - [@alansikora](https://github.com/alansikora) -- [@g-thome](https://github.com/g-thome) - [@gabriellsh](https://github.com/gabriellsh) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -5145,7 +5739,7 @@ - Omnichannel Analytics page doesn't have field labels ([#19400](https://github.com/RocketChat/Rocket.Chat/pull/19400) by [@rafaelblink](https://github.com/rafaelblink)) -- Outgoing integrations without trigger words or with multiple commas ([#19488](https://github.com/RocketChat/Rocket.Chat/pull/19488)) +- Outgoing integrations without trigger words or with multiple commas ([#19488](https://github.com/RocketChat/Rocket.Chat/pull/19488) by [@g-thome](https://github.com/g-thome)) - Prevent headerRoom's click to open room/direct info ([#19596](https://github.com/RocketChat/Rocket.Chat/pull/19596)) @@ -5159,7 +5753,7 @@ - Save button enabled by default in Omnichannel Business Hours Form ([#19493](https://github.com/RocketChat/Rocket.Chat/pull/19493) by [@rafaelblink](https://github.com/rafaelblink)) -- Settings may not update internal cache immediately ([#19628](https://github.com/RocketChat/Rocket.Chat/pull/19628)) +- Settings may not update internal cache immediately ([#19628](https://github.com/RocketChat/Rocket.Chat/pull/19628) by [@g-thome](https://github.com/g-thome)) - Setup Wizard User Creation Locking up ([#19509](https://github.com/RocketChat/Rocket.Chat/pull/19509)) @@ -5199,7 +5793,7 @@ - Fix Docker preview image build ([#19627](https://github.com/RocketChat/Rocket.Chat/pull/19627)) -- Fix permission duplicated error on startup causing CI to halt ([#19653](https://github.com/RocketChat/Rocket.Chat/pull/19653)) +- Fix permission duplicated error on startup causing CI to halt ([#19653](https://github.com/RocketChat/Rocket.Chat/pull/19653) by [@g-thome](https://github.com/g-thome)) - Improve performance of migration 211 (adding mostImportantRole to sessions) ([#19700](https://github.com/RocketChat/Rocket.Chat/pull/19700)) @@ -5241,7 +5835,7 @@ - Regression: Verticalbar size ([#19670](https://github.com/RocketChat/Rocket.Chat/pull/19670)) -- Release 3.8.2 ([#19705](https://github.com/RocketChat/Rocket.Chat/pull/19705)) +- Release 3.8.2 ([#19705](https://github.com/RocketChat/Rocket.Chat/pull/19705) by [@g-thome](https://github.com/g-thome)) - Report DAU and MAU by role ([#19657](https://github.com/RocketChat/Rocket.Chat/pull/19657)) @@ -5257,6 +5851,7 @@ - [@MarcosSpessatto](https://github.com/MarcosSpessatto) - [@antkaz](https://github.com/antkaz) - [@dependabot[bot]](https://github.com/dependabot[bot]) +- [@g-thome](https://github.com/g-thome) - [@mrfigueiredo](https://github.com/mrfigueiredo) - [@rafaelblink](https://github.com/rafaelblink) - [@robertfromont](https://github.com/robertfromont) @@ -5268,7 +5863,6 @@ - [@MartinSchoeler](https://github.com/MartinSchoeler) - [@d-gubert](https://github.com/d-gubert) - [@dougfabris](https://github.com/dougfabris) -- [@g-thome](https://github.com/g-thome) - [@gabriellsh](https://github.com/gabriellsh) - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) @@ -5349,21 +5943,24 @@ ### 🐛 Bug fixes -- Room avatar update event doesn't properly broadcast room id ([#19684](https://github.com/RocketChat/Rocket.Chat/pull/19684)) +- Room avatar update event doesn't properly broadcast room id ([#19684](https://github.com/RocketChat/Rocket.Chat/pull/19684) by [@g-thome](https://github.com/g-thome)) -- Server crash while reading settings for allowed and blocked email domain lists ([#19683](https://github.com/RocketChat/Rocket.Chat/pull/19683)) +- Server crash while reading settings for allowed and blocked email domain lists ([#19683](https://github.com/RocketChat/Rocket.Chat/pull/19683) by [@g-thome](https://github.com/g-thome))
🔍 Minor changes -- Release 3.8.2 ([#19705](https://github.com/RocketChat/Rocket.Chat/pull/19705)) +- Release 3.8.2 ([#19705](https://github.com/RocketChat/Rocket.Chat/pull/19705) by [@g-thome](https://github.com/g-thome))
-### 👩‍💻👨‍💻 Core Team 🤓 +### 👩‍💻👨‍💻 Contributors 😍 - [@g-thome](https://github.com/g-thome) + +### 👩‍💻👨‍💻 Core Team 🤓 + - [@sampaiodiego](https://github.com/sampaiodiego) # 3.8.1 @@ -8241,7 +8838,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)) @@ -8447,7 +9044,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) @@ -8475,6 +9071,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) diff --git a/app/2fa/client/TOTPLDAP.js b/app/2fa/client/TOTPLDAP.js index 0d74719f7cf8..1983584b1e52 100644 --- a/app/2fa/client/TOTPLDAP.js +++ b/app/2fa/client/TOTPLDAP.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; import { Utils2fa } from './lib/2fa'; -import '../../ldap/client/loginHelper'; +import '../../../client/startup/ldap'; Meteor.loginWithLDAPAndTOTP = function(...args) { // Pull username and password diff --git a/app/2fa/server/functions/resetTOTP.ts b/app/2fa/server/functions/resetTOTP.ts index c41e33102fce..d7ac07291845 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 originalAction.apply(this)); - } catch (e) { - logger.debug(`${ method } ${ route } threw an error:`, e.stack); + result = DDP._CurrentInvocation.withValue(invocation, () => 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, }); diff --git a/app/api/server/default/info.js b/app/api/server/default/info.js index 861d9dfb36ac..62ef49023f00 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 fdcc98f4b1d2..000000000000 --- 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 000000000000..edb347cd33b3 --- /dev/null +++ b/app/api/server/helpers/deprecationWarning.ts @@ -0,0 +1,15 @@ +import { API } from '../api'; +import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; + +(API as any).helperMethods.set('deprecationWarning', function _deprecationWarning({ endpoint, versionWillBeRemoved, response }: { endpoint: string; versionWillBeRemoved: string; response: any }) { + 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; +}); diff --git a/app/api/server/helpers/isWidget.ts b/app/api/server/helpers/isWidget.ts new file mode 100644 index 000000000000..203ab081925d --- /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 b5a5c0d6f1dc..48d0caa9d6fc 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 000000000000..9d9d46cffea8 --- /dev/null +++ b/app/api/server/lib/getServerInfo.ts @@ -0,0 +1,22 @@ + +import { Info } from '../../../utils/server'; +import { hasRoleAsync } 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 (await hasRoleAsync(userId, 'admin')) { + return { + info: Info, + }; + } + return { + version: removePatchInfo(Info.version), + }; +} diff --git a/app/api/server/lib/rooms.js b/app/api/server/lib/rooms.js index 0b908620f18a..ea184b1d2fa2 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 { 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,35 @@ export async function findChannelAndPrivateAutocomplete({ uid, selector }) { }; } +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/v1/assets.js b/app/api/server/v1/assets.js index 108f9649ffe6..09739ad66074 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/banners.ts b/app/api/server/v1/banners.ts index 07a8077d7246..515527273ee4 100644 --- a/app/api/server/v1/banners.ts +++ b/app/api/server/v1/banners.ts @@ -1,12 +1,13 @@ import { Promise } from 'meteor/promise'; import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; +import { TextObjectType, BlockType } from '@rocket.chat/apps-engine/definition/uikit'; import { API } from '../api'; import { Banner } from '../../../../server/sdk'; -import { BannerPlatform } from '../../../../definition/IBanner'; +import { BannerPlatform, IBanner } from '../../../../definition/IBanner'; -API.v1.addRoute('banners.getNew', { authRequired: true }, { +API.v1.addRoute('banners.getNew', { authRequired: true }, { // deprecated get() { check(this.queryParams, Match.ObjectIncluding({ platform: String, @@ -22,12 +23,129 @@ API.v1.addRoute('banners.getNew', { authRequired: true }, { throw new Meteor.Error('error-unknown-platform', 'Platform is unknown.'); } - const banners = Promise.await(Banner.getNewBannersForUser(this.userId, platform, bannerId)); + const banners = Promise.await(Banner.getBannersForUser(this.userId, platform, bannerId)); return API.v1.success({ banners }); }, }); + +API.v1.addRoute('banners/:id', { authRequired: true }, { + + get() { + check(this.urlParams, Match.ObjectIncluding({ + id: String, + })); + + const { platform } = this.queryParams; + if (!platform) { + throw new Meteor.Error('error-missing-param', 'The required "platform" param is missing.'); + } + + const { id } = this.urlParams; + if (!id) { + throw new Meteor.Error('error-missing-param', 'The required "id" param is missing.'); + } + + const banners = Promise.await(Banner.getBannersForUser(this.userId, platform, id)); + + return API.v1.success({ banners }); + }, +}); +API.v1.addRoute('banners', { authRequired: true }, { + + get() { + check(this.queryParams, Match.ObjectIncluding({ + platform: String, + })); + + const { platform } = this.queryParams; + if (!platform) { + throw new Meteor.Error('error-missing-param', 'The required "platform" param is missing.'); + } + + if (!Object.values(BannerPlatform).includes(platform)) { + throw new Meteor.Error('error-unknown-platform', 'Platform is unknown.'); + } + + const banners = Promise.await(Banner.getBannersForUser(this.userId, platform)); + + return API.v1.success({ banners }); + }, + ...process.env.NODE_ENV !== 'production' && { + post(): {} { + check(this.bodyParams, Match.ObjectIncluding({ + platform: Match.Maybe(String), + bid: String, + })); + + const { platform = 'web', bid: bannerId } = this.bodyParams; + + 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 b: IBanner = { + _id: bannerId, + platform: [platform], + expireAt: new Date(new Date().getTime() + (1000 * 60 * 60 * 24 * 7)), + startAt: new Date(), + roles: ['admin'], + createdBy: { + _id: this.userId, + username: this.userId, + }, + createdAt: new Date(), + _updatedAt: new Date(), + view: { + viewId: '', + appId: '', + blocks: [{ + type: BlockType.SECTION, + blockId: 'attention', + text: { + type: TextObjectType.PLAINTEXT, + text: 'Test', + emoji: false, + }, + }], + }, + }; + + const banners = Promise.await(Banner.create(b)); + + return API.v1.success({ banners }); + }, + delete(): {} { + check(this.bodyParams, Match.ObjectIncluding({ + bid: String, + })); + + const { bid } = this.bodyParams; + + Promise.await(Banner.disable(bid)); + + return API.v1.success(); + }, + + patch(): {} { + check(this.bodyParams, Match.ObjectIncluding({ + bid: String, + })); + + const { bid } = this.bodyParams; + + Promise.await(Banner.enable(bid)); + + return API.v1.success(); + }, + }, +}); + + API.v1.addRoute('banners.dismiss', { authRequired: true }, { post() { check(this.bodyParams, Match.ObjectIncluding({ diff --git a/app/api/server/v1/emoji-custom.js b/app/api/server/v1/emoji-custom.js index d5f489b9798e..403cca1d189c 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'; 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(); @@ -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 = 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/ldap.ts b/app/api/server/v1/ldap.ts new file mode 100644 index 000000000000..3b47a64b28a3 --- /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 }, { + 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 { + Promise.await(LDAP.testConnection()); + } catch (error) { + SystemLogger.error(error); + throw new Error('Connection_failed'); + } + + return API.v1.success({ + message: 'Connection_success', + }); + }, +}); + +API.v1.addRoute('ldap.testSearch', { authRequired: true }, { + 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'); + } + + Promise.await(LDAP.testSearch(this.bodyParams.username)); + + return API.v1.success({ + message: 'LDAP_User_Found', + }); + }, +}); diff --git a/app/api/server/v1/misc.js b/app/api/server/v1/misc.js index 936df8c9ed89..03e639b34999 100644 --- a/app/api/server/v1/misc.js +++ b/app/api/server/v1/misc.js @@ -7,46 +7,14 @@ 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'; API.v1.addRoute('me', { authRequired: true }, { get() { @@ -242,7 +210,7 @@ API.v1.addRoute('stdout.queue', { authRequired: true }, { if (!hasPermission(this.userId, 'view-logs')) { return API.v1.unauthorized(); } - return API.v1.success({ queue: StdOut.queue }); + return API.v1.success({ queue: getLogs() }); }, }); @@ -289,7 +257,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 index e74c95662517..4ac1661f0786 100644 --- a/app/api/server/v1/permissions.js +++ b/app/api/server/v1/permissions.js @@ -5,40 +5,6 @@ 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; diff --git a/app/api/server/v1/roles.js b/app/api/server/v1/roles.js index 92895cb9f6a8..39d89164e4ab 100644 --- a/app/api/server/v1/roles.js +++ b/app/api/server/v1/roles.js @@ -84,9 +84,14 @@ API.v1.addRoute('roles.addUserToRole', { authRequired: true }, { }); const user = this.getUserFromParams(); + const { roleName, roomId } = this.bodyParams; + + if (hasRole(user._id, roleName, roomId)) { + throw new Meteor.Error('error-user-already-in-role', 'User already in role'); + } Meteor.runAsUser(this.userId, () => { - Meteor.call('authorization:addUserToRole', this.bodyParams.roleName, user.username, this.bodyParams.roomId); + Meteor.call('authorization:addUserToRole', roleName, user.username, roomId); }); return API.v1.success({ diff --git a/app/api/server/v1/rooms.js b/app/api/server/v1/rooms.js index bbc23d165e3f..eee7230a0bbf 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, findRoomsAvailableForTeams, findChannelAndPrivateAutocompleteWithPagination } from '../lib/rooms'; import { sendFile, sendViaEmail } from '../../../../server/lib/channelExport'; import { canAccessRoom, hasPermission } from '../../../authorization/server'; import { Media } from '../../../../server/sdk'; @@ -314,6 +314,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/users.js b/app/api/server/v1/users.js index 50f7e4a8ff7d..74344aa94ceb 100644 --- a/app/api/server/v1/users.js +++ b/app/api/server/v1/users.js @@ -2,7 +2,6 @@ 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'; @@ -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'; @@ -349,58 +349,38 @@ API.v1.addRoute('users.setAvatar', { authRequired: true }, { 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-info')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } + } + + setUserAvatar(user, image.fileBuffer, image.mimetype, 'rest'); return API.v1.success(); }, @@ -945,7 +925,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'); } diff --git a/app/apps/client/RealAppsEngineUIHost.js b/app/apps/client/RealAppsEngineUIHost.js index 29802587f9b7..d27fdf36ac05 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 8ffee03cd9c0..a23f34837d9e 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 15579db899c2..93f1d9c73099 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/server/bridges/livechat.ts b/app/apps/server/bridges/livechat.ts index 0ae171c39128..564cbf220e71 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,7 +88,13 @@ 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, }); @@ -166,10 +180,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 +248,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 a16dbc38101d..49c4808c6883 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); } diff --git a/app/apps/server/bridges/uploads.ts b/app/apps/server/bridges/uploads.ts index 27dcbbff3dd4..ab7d6bb2da14 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/uikit.js b/app/apps/server/communication/uikit.js index d86aa6b8fb4c..4abc6b992ab3 100644 --- a/app/apps/server/communication/uikit.js +++ b/app/apps/server/communication/uikit.js @@ -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 fd9360dce473..8be3edc38e0e 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,9 @@ 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().update(appPackage); this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_UPDATED, appId); } diff --git a/app/apps/server/converters/users.js b/app/apps/server/converters/users.js index 79d4016e7c20..17895ff00cd8 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/orchestrator.js b/app/apps/server/orchestrator.js index ea3f3c07671c..8b164ab0d7dd 100644 --- a/app/apps/server/orchestrator.js +++ b/app/apps/server/orchestrator.js @@ -12,28 +12,40 @@ import { AppMessagesConverter, AppRoomsConverter, AppSettingsConverter, AppUsers 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 +58,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)); @@ -93,6 +110,10 @@ export class AppServerOrchestrator { return this._manager.getExternalComponentManager().getProvidedComponents(); } + getAppSourceStorage() { + return this._appSourceStorage; + } + isInitialized() { return this._isInitialized; } @@ -217,9 +238,48 @@ 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.get('Apps_Framework_Source_Package_Storage_Type', (_, value) => { + if (!Apps.isInitialized()) { + appsSourceStorageType = value; + } else { + Apps.getAppSourceStorage().setStorage(value); + } +}); + +settings.get('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) => { // In case this gets called before `Meteor.startup` diff --git a/app/apps/server/storage/AppFileSystemSourceStorage.ts b/app/apps/server/storage/AppFileSystemSourceStorage.ts new file mode 100644 index 000000000000..f14e73c67d02 --- /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 000000000000..30d5980f2153 --- /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/storage.js b/app/apps/server/storage/AppRealStorage.ts similarity index 65% rename from app/apps/server/storage/storage.js rename to app/apps/server/storage/AppRealStorage.ts index 8fc9c79b705f..b90e5890edfa 100644 --- a/app/apps/server/storage/storage.js +++ b/app/apps/server/storage/AppRealStorage.ts @@ -1,12 +1,13 @@ -import { AppStorage } from '@rocket.chat/apps-engine/server/storage'; +import { AppMetadataStorage, IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; -export class AppRealStorage extends AppStorage { - constructor(data) { +import { AppsModel } from '../../../models/server/models/apps-model'; + +export class AppRealStorage extends AppMetadataStorage { + constructor(private db: AppsModel) { super('mongodb'); - this.db = data; } - create(item) { + public create(item: IAppStorageItem): Promise { return new Promise((resolve, reject) => { item.createdAt = new Date(); item.updatedAt = new Date(); @@ -34,7 +35,7 @@ export class AppRealStorage extends AppStorage { }); } - retrieveOne(id) { + public retrieveOne(id: string): Promise { return new Promise((resolve, reject) => { let doc; @@ -48,9 +49,9 @@ export class AppRealStorage extends AppStorage { }); } - retrieveAll() { + public retrieveAll(): Promise> { return new Promise((resolve, reject) => { - let docs; + let docs: Array; try { docs = this.db.find({}).fetch(); @@ -66,8 +67,8 @@ export class AppRealStorage extends AppStorage { }); } - update(item) { - return new Promise((resolve, reject) => { + public update(item: IAppStorageItem): Promise { + return new Promise((resolve, reject) => { try { this.db.update({ id: item.id }, item); resolve(item.id); @@ -77,7 +78,7 @@ export class AppRealStorage extends AppStorage { }).then(this.retrieveOne.bind(this)); } - remove(id) { + public remove(id: string): Promise<{ success: boolean }> { return new Promise((resolve, reject) => { try { this.db.remove({ id }); diff --git a/app/apps/server/storage/ConfigurableAppSourceStorage.ts b/app/apps/server/storage/ConfigurableAppSourceStorage.ts new file mode 100644 index 000000000000..2f0118b18cf3 --- /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 fd1680c1c9c3..289c56376365 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/authentication/server/lib/logLoginAttempts.ts b/app/authentication/server/lib/logLoginAttempts.ts index 699c7c00ddd0..ed73cf395e6d 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 a746d6e7938e..d62f090b1364 100644 --- a/app/authentication/server/lib/restrictLoginAttempts.ts +++ b/app/authentication/server/lib/restrictLoginAttempts.ts @@ -16,7 +16,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 +23,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; } diff --git a/app/authentication/server/startup/index.js b/app/authentication/server/startup/index.js index 680041fd2d53..3bf9e82da3cd 100644 --- a/app/authentication/server/startup/index.js +++ b/app/authentication/server/startup/index.js @@ -206,6 +206,7 @@ Accounts.onCreateUser(function(options, user = {}) { Mailer.send(email); } + callbacks.run('onCreateUser', options, user); return user; }); diff --git a/app/authorization/client/hasPermission.js b/app/authorization/client/hasPermission.js deleted file mode 100644 index a48549979c7a..000000000000 --- 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 000000000000..744903bc5062 --- /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']): scope is keyof typeof Models => + typeof scope === 'string' && scope in Models; + +const createPermissionValidator = (quantifier: (predicate: (permissionId: IPermission['_id']) => boolean) => boolean) => + (permissionIds: IPermission['_id'][], scope: IRole['scope'], userId: IUser['_id']): boolean => { + 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]; + 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: IRole['scope'], + predicate: (permissionIds: IPermission['_id'][], scope: IRole['scope'], 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?: IRole['scope'], +): boolean => validatePermissions(permissions, scope, all); + +export const hasAtLeastOnePermission = ( + permissions: IPermission['_id'] | IPermission['_id'][], + scope?: IRole['scope'], +): boolean => validatePermissions(permissions, scope, atLeastOne); + +export const userHasAllPermission = ( + permissions: IPermission['_id'] | IPermission['_id'][], + scope?: IRole['scope'], + 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 202f0089d3d1..4bad4a825cdc 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/autotranslate/client/lib/autotranslate.js b/app/autotranslate/client/lib/autotranslate.js index f711bc383a4a..1b742b21010b 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/deeplTranslate.js b/app/autotranslate/server/deeplTranslate.js index 91145470cf55..b45500602a3b 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'; /** diff --git a/app/autotranslate/server/googleTranslate.js b/app/autotranslate/server/googleTranslate.js index b2463718e6f5..88c5403e550b 100644 --- a/app/autotranslate/server/googleTranslate.js +++ b/app/autotranslate/server/googleTranslate.js @@ -7,7 +7,7 @@ import { HTTP } from 'meteor/http'; import _ from 'underscore'; import { AutoTranslate, TranslationProviderRegistry } from './autotranslate'; -import { SystemLogger } from '../../logger/server'; +import { SystemLogger } from '../../../server/lib/logger/system'; import { settings } from '../../settings'; /** diff --git a/app/autotranslate/server/logger.js b/app/autotranslate/server/logger.js index 8f104e75e88c..70ca0cbf97b6 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 a71d450fa72d..1c3da27e6c29 100644 --- a/app/autotranslate/server/msTranslate.js +++ b/app/autotranslate/server/msTranslate.js @@ -7,7 +7,7 @@ import { HTTP } from 'meteor/http'; import _ from 'underscore'; import { TranslationProviderRegistry, AutoTranslate } from './autotranslate'; -import { logger } from './logger'; +import { msLogger } from './logger'; import { settings } from '../../settings'; /** @@ -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/bigbluebutton/server/bigbluebutton-api.js b/app/bigbluebutton/server/bigbluebutton-api.js index b90424914736..8cb3f4d447c4 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 de00f0d1ccf4..53756efe8891 100644 --- a/app/blockstack/server/loginHandler.js +++ b/app/blockstack/server/loginHandler.js @@ -44,7 +44,7 @@ Accounts.registerLoginHandler('blockstack', (loginRequest) => { }); } } catch (e) { - console.error(e); + logger.error(e); } } diff --git a/app/callbacks/lib/callbacks.js b/app/callbacks/lib/callbacks.js index 525d869e191b..e91c1af38831 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 47dd4b405e25..6e4fea97c122 100644 --- a/app/cas/server/cas_rocketchat.js +++ b/app/cas/server/cas_rocketchat.js @@ -4,7 +4,7 @@ import { ServiceConfiguration } from 'meteor/service-configuration'; import { Logger } from '../../logger'; import { settings } from '../../settings'; -export const logger = new Logger('CAS', {}); +export const logger = new Logger('CAS'); Meteor.startup(function() { settings.addGroup('CAS', function() { diff --git a/app/channel-settings/server/functions/saveReactWhenReadOnly.js b/app/channel-settings/server/functions/saveReactWhenReadOnly.js index 97d20782b235..115d98b80523 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/saveRoomReadOnly.js b/app/channel-settings/server/functions/saveRoomReadOnly.js index baf8dff166bf..86e62061bf8c 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/chatpal-search/client/style.css b/app/chatpal-search/client/style.css index 890e3c6891fb..cc704ea30b0d 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/result.js b/app/chatpal-search/client/template/result.js index 2468ecc30c9f..1ed9a1bcafd1 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 4195805d53b7..9a3ad7c56f75 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 ff8e6fb3bc1c..e60d9c507aff 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 c1d75b806a2c..bfc73d4ecbc7 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/connectWorkspace.js b/app/cloud/server/functions/connectWorkspace.js index 6e428803dc0c..974eb47d0668 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 5bf24cb9992b..60691ed20dce 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 3be0f726b121..f6a48a0ad63e 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 f7ad77aa38d1..6acdbe8be039 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 00916c9b93db..1483e04d3d16 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 84837dcdaf61..eba17c2649b1 100644 --- a/app/cloud/server/functions/startRegisterWorkspace.js +++ b/app/cloud/server/functions/startRegisterWorkspace.js @@ -5,6 +5,7 @@ import { syncWorkspace } from './syncWorkspace'; import { settings } from '../../../settings'; import { Settings } from '../../../models'; import { buildWorkspaceRegistrationData } from './buildRegistrationData'; +import { SystemLogger } from '../../../../server/lib/logger/system'; export function startRegisterWorkspace(resend = false) { @@ -28,9 +29,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 360309c7cd5c..03f67acf4a4b 100644 --- a/app/cloud/server/functions/syncWorkspace.js +++ b/app/cloud/server/functions/syncWorkspace.js @@ -8,6 +8,7 @@ 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) { const { workspaceRegistered, connectToCloud } = retrieveRegistrationStatus(); @@ -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; diff --git a/app/cloud/server/functions/userLogout.js b/app/cloud/server/functions/userLogout.js index 5dea9eb07815..405ab5bd0d73 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/cors/server/cors.js b/app/cors/server/cors.js index b14d6b5588bb..f09ea4d7808d 100644 --- a/app/cors/server/cors.js +++ b/app/cors/server/cors.js @@ -8,13 +8,8 @@ import { settings } from '../../settings'; 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) => { WebAppInternals.setInlineScriptsAllowed(!enabled); @@ -52,20 +47,6 @@ WebApp.rawConnectHandlers.use(function(req, res, next) { ); } - // 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/crowd/server/crowd.js b/app/crowd/server/crowd.js index a4cc46ca45ba..c21b4550bd97 100644 --- a/app/crowd/server/crowd.js +++ b/app/crowd/server/crowd.js @@ -10,8 +10,9 @@ import { Users } from '../../models'; import { settings } from '../../settings'; import { hasRole } from '../../authorization'; 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 +155,6 @@ export class CROWD { address: crowdUser.email, verified: settings.get('Accounts_Verify_Email_For_External_Accounts'), }], - active: crowdUser.active, crowd: true, }; @@ -173,6 +173,8 @@ export class CROWD { Meteor.users.update(id, { $set: user, }); + + setUserActiveStatus(id, crowdUser.active); } sync() { diff --git a/app/custom-oauth/server/custom_oauth_server.js b/app/custom-oauth/server/custom_oauth_server.js index 1bc181ed5e4e..ed9af5a4ea6c 100644 --- a/app/custom-oauth/server/custom_oauth_server.js +++ b/app/custom-oauth/server/custom_oauth_server.js @@ -6,13 +6,12 @@ import { HTTP } from 'meteor/http'; import { ServiceConfiguration } from 'meteor/service-configuration'; import _ from 'underscore'; -import { callbacks } from '../../callbacks'; 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 crypto = require('crypto'); @@ -82,23 +81,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; } @@ -193,7 +179,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) { @@ -353,14 +339,6 @@ export class CustomOAuth { return; } - if (this.mergeRoles) { - updateRolesFromSSO(user, serviceData, this.rolesClaim); - } - - if (this.mapChannels) { - mapSSOGroupsToChannels(user, serviceData, this.groupsClaim, this.channelsMap, this.channelsAdmin); - } - // User already created or merged and has identical name as before if (user.services && user.services[serviceName] && user.services[serviceName].id === serviceData.id && user.name === serviceData.name) { return; @@ -370,6 +348,8 @@ export class CustomOAuth { throw new Meteor.Error('CustomOAuth', `User with username ${ user.username } already exists`); } + callbacks.run('afterProcessOAuthUser', { serviceName, serviceData, user }); + const serviceIdKey = `services.${ serviceName }.id`; const update = { $set: { @@ -399,13 +379,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 f00eea03b6b7..000000000000 --- 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-sounds/server/startup/custom-sounds.js b/app/custom-sounds/server/startup/custom-sounds.js index 77a0d2c52b38..00349dfd706d 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/dolphin/lib/common.js b/app/dolphin/lib/common.js index 0e74e6d1fca2..051a1a339a40 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/e2e/client/logger.ts b/app/e2e/client/logger.ts index 1efd15d83475..50e43b634304 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 2c0541350ec5..387e379045cf 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 { 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 bfc10c8c8619..85f492083e3b 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/emoji-custom/client/lib/emojiCustom.js b/app/emoji-custom/client/lib/emojiCustom.js index 8b7ce1f53028..e4530e80a984 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/startup/emoji-custom.js b/app/emoji-custom/server/startup/emoji-custom.js index 8decb068ea1e..c7a83a0cf301 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/client/emojiParser.js b/app/emoji/client/emojiParser.js index 71422578b13e..b6d6d6950cf1 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 bf02d2633523..c1678c647f11 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 d4b2ba2f4993..000000000000 --- 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/federation/server/endpoints/dispatch.js b/app/federation/server/endpoints/dispatch.js index 7a1971ff82a0..ae392ac8aac8 100644 --- a/app/federation/server/endpoints/dispatch.js +++ b/app/federation/server/endpoints/dispatch.js @@ -1,8 +1,7 @@ -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, @@ -135,7 +134,7 @@ 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 @@ -240,7 +239,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 +267,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 }`); } } } @@ -464,7 +463,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) { @@ -479,14 +478,14 @@ API.v1.addRoute('federation.events.dispatch', { authRequired: false, rateLimiter // 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); // 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 87917880229e..cac0168c8c12 100644 --- a/app/federation/server/endpoints/requestFromLatest.js +++ b/app/federation/server/endpoints/requestFromLatest.js @@ -1,7 +1,7 @@ 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'; @@ -25,7 +25,7 @@ API.v1.addRoute('federation.events.requestFromLatest', { authRequired: false }, 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; diff --git a/app/federation/server/endpoints/uploads.js b/app/federation/server/endpoints/uploads.js index ca8b81a92089..7735a630f15e 100644 --- a/app/federation/server/endpoints/uploads.js +++ b/app/federation/server/endpoints/uploads.js @@ -1,5 +1,3 @@ -import { Meteor } from 'meteor/meteor'; - import { API } from '../../../api/server'; import { Uploads } from '../../../models/server'; import { FileUpload } from '../../../file-upload/server'; @@ -19,9 +17,7 @@ API.v1.addRoute('federation.uploads', { authRequired: false }, { 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 d74cdae83248..7b92b0b5e509 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/handler/index.js b/app/federation/server/handler/index.js index 8ccb510194a5..7827ddf063a7 100644 --- a/app/federation/server/handler/index.js +++ b/app/federation/server/handler/index.js @@ -1,7 +1,7 @@ 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'; @@ -10,7 +10,7 @@ export function federationSearchUsers(query) { throw disabled('client.searchUsers'); } - logger.client.debug(() => `searchUsers => query=${ query }`); + clientLogger.debug({ msg: 'searchUsers', query }); const [username, peerDomain] = query.split('@'); @@ -26,7 +26,7 @@ export function getUserByUsername(query) { throw disabled('client.searchUsers'); } - logger.client.debug(() => `getUserByUsername => query=${ query }`); + clientLogger.debug({ msg: 'getUserByUsername', query }); const [username, peerDomain] = query.split('@'); @@ -42,7 +42,7 @@ export function requestEventsFromLatest(domain, fromDomain, contextType, context 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'; @@ -57,7 +57,7 @@ export function dispatchEvents(domains, events) { 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'; diff --git a/app/federation/server/hooks/afterAddedToRoom.js b/app/federation/server/hooks/afterAddedToRoom.js index 1fd359504c5e..02f76297fcfd 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 a0048fcb24a6..ac05794e1c2e 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; } @@ -45,7 +45,7 @@ async function afterCreateDirectRoom(room, extras) { } 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 a58d40446c17..75dfeeac6575 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'; @@ -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 714de63a6e4d..6d070eafbac0 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 5f1227df0a69..524d2078ae8a 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 4dba95ee4df7..6a53d204d0a8 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 c816f251982b..4c7509ec961e 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 5f72868a2323..7fdc128977b5 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 fec108dd91da..ce3d3d726394 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 3578e08c97d0..e33ff80ac956 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 72c9259822c0..42d34091c80a 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 0131e4692806..7e55887263df 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 ce92cfd84059..7a231a13fb91 100644 --- a/app/federation/server/lib/crypt.js +++ b/app/federation/server/lib/crypt.js @@ -1,7 +1,7 @@ import { FederationKeys } from '../../../models/server'; import { getFederationDomain } from './getFederationDomain'; import { search } from './dns'; -import { logger } from './logger'; +import { cryptLogger } from './logger'; export function decrypt(data, peerKey) { // @@ -15,7 +15,7 @@ export function decrypt(data, peerKey) { // Decrypt with the local private key data = FederationKeys.getPrivateKey().decrypt(data); } catch (err) { - logger.crypt.error(err); + cryptLogger.error(err); throw new Error('Could not decrypt'); } @@ -60,7 +60,7 @@ export function encrypt(data, peerKey) { // Encrypt with the local private key return 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 189b8fc18692..0c4e2f348e1b 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'; @@ -26,7 +26,7 @@ export function registerWithHub(peerDomain, url, publicKey) { return true; } catch (err) { - logger.dns.error(err); + dnsLogger.error(err); throw federationErrors.peerCouldNotBeRegisteredWithHub('dns.registerWithHub'); } @@ -34,19 +34,19 @@ export function registerWithHub(peerDomain, url, publicKey) { export 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 }`); 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 52cb31480163..542a2d32ef9e 100644 --- a/app/federation/server/lib/http.js +++ b/app/federation/server/lib/http.js @@ -1,7 +1,7 @@ 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'; @@ -17,7 +17,7 @@ export function federationRequest(method, url, body, headers, peerKey = null) { } } - logger.http.debug(`[${ method }] ${ url }`); + httpLogger.debug(`[${ method }] ${ url }`); return MeteorHTTP.call(method, url, { data, timeout: 2000, headers: { ...headers, 'x-federation-domain': getFederationDomain() } }); } @@ -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); } 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 f3e791a14bf8..9e66d33808b0 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/startup/settings.js b/app/federation/server/startup/settings.js index 48213be78807..a94caaaeff25 100644 --- a/app/federation/server/startup/settings.js +++ b/app/federation/server/startup/settings.js @@ -7,7 +7,7 @@ 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 { setupLogger } from '../lib/logger'; import { FederationKeys } from '../../../models/server'; import { STATUS_ENABLED, STATUS_REGISTERING, STATUS_ERROR_REGISTERING, STATUS_DISABLED } from '../constants'; @@ -89,7 +89,7 @@ const updateSettings = debounce(Meteor.bindEnvironment(function() { }), 150); function enableOrDisable(key, value) { - logger.setup.info(`Federation is ${ value ? 'enabled' : 'disabled' }`); + setupLogger.info(`Federation is ${ value ? 'enabled' : 'disabled' }`); if (value) { updateSettings(); diff --git a/app/file-upload/server/config/AmazonS3.js b/app/file-upload/server/config/AmazonS3.js index fbbb3724ce1a..efbef8d05651 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) { diff --git a/app/file-upload/server/config/GoogleStorage.js b/app/file-upload/server/config/GoogleStorage.js index c8e11bfe1fb5..1717f38c4240 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) { diff --git a/app/file-upload/server/config/Webdav.js b/app/file-upload/server/config/Webdav.js index 8c5382b6b77e..2386b40ae141 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(); }) diff --git a/app/file-upload/server/config/_configUploadStorage.js b/app/file-upload/server/config/_configUploadStorage.js index 3fb54925db71..b0b656b5c468 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,7 +13,7 @@ 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`); diff --git a/app/file-upload/server/lib/FileUpload.js b/app/file-upload/server/lib/FileUpload.js index 23f13684b844..22e1cfc210fb 100644 --- a/app/file-upload/server/lib/FileUpload.js +++ b/app/file-upload/server/lib/FileUpload.js @@ -28,6 +28,7 @@ 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; @@ -235,7 +236,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 }, { @@ -317,7 +318,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 +345,7 @@ export const FileUpload = { })); })); })).catch((err) => { - console.error(err); + SystemLogger.error(err); fut.return(); }); }; @@ -431,7 +432,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 +462,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); diff --git a/app/file-upload/server/lib/proxy.js b/app/file-upload/server/lib/proxy.js index 0921956d111a..2435df950b6d 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/streamToBuffer.ts b/app/file-upload/server/lib/streamToBuffer.ts index 34dc1c434a32..7e0fa8b3cc1e 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/sendFileMessage.ts b/app/file-upload/server/methods/sendFileMessage.ts index 494b4b9658ae..70e6ca9aac72 100644 --- a/app/file-upload/server/methods/sendFileMessage.ts +++ b/app/file-upload/server/methods/sendFileMessage.ts @@ -12,6 +12,7 @@ import { canAccessRoom } from '../../../authorization/server/functions/canAccess 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 = {}) { @@ -84,7 +85,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/ufs/AmazonS3/server.js b/app/file-upload/ufs/AmazonS3/server.js index fc6ab49ebd27..e5fb5f87709f 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 47b384466549..f0fe78265ef7 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 59567fdb4645..3d5326e1f194 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/google-oauth/server/index.js b/app/google-oauth/server/index.js index 81be9e680b0c..c785c3da47a5 100644 --- a/app/google-oauth/server/index.js +++ b/app/google-oauth/server/index.js @@ -22,7 +22,6 @@ Meteor.startup(() => { credentialSecret: escape(options.credentialSecret), storagePrefix: escape(OAuth._storageTokenPrefix), redirectUrl: escape(options.redirectUrl), - isCordova: !! options.isCordova, }; let template; @@ -65,14 +64,12 @@ Meteor.startup(() => { } } - const isCordova = OAuth._isCordovaFromQuery(details.query); if (details.error) { res.end(renderEndOfLoginResponse({ loginStyle: details.loginStyle, setCredentialToken: false, redirectUrl, - isCordova, }), 'utf-8'); return; } @@ -86,7 +83,6 @@ Meteor.startup(() => { credentialToken: details.credentials.token, credentialSecret: details.credentials.secret, redirectUrl, - isCordova, }), 'utf-8'); }; }); diff --git a/app/google-vision/README.md b/app/google-vision/README.md deleted file mode 100644 index 75ada0a6d01b..000000000000 --- 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 a9f977eba4ff..000000000000 --- 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 da4b03a0d29c..000000000000 --- 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 69729c6c1ddb..000000000000 --- 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 95ffc9e6e508..000000000000 --- 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 efa0f446bc72..000000000000 --- 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/iframe-login/server/iframe_server.js b/app/iframe-login/server/iframe_server.js index 586bc8ee8558..95fd1d4cd50f 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 66cc086c1c1a..285f747e6193 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 3f59b669e632..b5b2fc2e43b7 100644 --- a/app/importer-hipchat-enterprise/server/importer.js +++ b/app/importer-hipchat-enterprise/server/importer.js @@ -43,7 +43,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; } } diff --git a/app/importer-pending-avatars/server/importer.js b/app/importer-pending-avatars/server/importer.js index 7a8767ba582a..aa12266decf2 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 7853b6d54e59..8c490192ac5d 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 b92fab40d1e0..9f09a44d49bd 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,107 @@ 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; + + // #ToDo: #TODO: Move this to the model class + const updateData: Record = { + $set: { + roles: userData.roles || ['user'], + type: userData.type || 'user', + ...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 || []); + 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 +262,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 +304,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 +327,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 +355,7 @@ export class ImportDataConverter { }); } - skipRecord(_id: string): void { + protected skipRecord(_id: string): void { ImportData.update({ _id, }, { @@ -424,9 +478,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 +556,7 @@ export class ImportDataConverter { } if (afterImportFn) { - afterImportFn(m, 'message'); + afterImportFn(m, 'message', true); } } catch (e) { this.saveError(_id, e); @@ -528,7 +586,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 +767,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 +826,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 +863,7 @@ export class ImportDataConverter { } if (afterImportFn) { - afterImportFn(c, 'channel'); + afterImportFn(c, 'channel', !existingRoom); } } catch (e) { this.saveError(_id, e); @@ -824,11 +886,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 decfd093543f..29767fdafead 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 000000000000..5baf334e394b --- /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 000000000000..80df62fc6f9e --- /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/index.js b/app/importer/server/index.js index dd9e89ba0209..0fed91e5b660 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/startup/setImportsToInvalid.js b/app/importer/server/startup/setImportsToInvalid.js index a7ec86373222..431d4040ab79 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 63d32c9be338..17a04b6c3582 100644 --- a/app/integrations/server/api/api.js +++ b/app/integrations/server/api/api.js @@ -10,10 +10,10 @@ 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 { settings } from '../../../settings/server'; const compiledScripts = {}; @@ -63,8 +63,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 +77,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,8 +126,8 @@ 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, @@ -142,9 +139,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 +161,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 +210,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 +222,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 +239,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 +263,7 @@ function removeIntegrationRest() { } function integrationSampleRest() { - logger.incoming.info('Sample Integration'); + incomingLogger.info('Sample Integration'); return { statusCode: 200, body: [ @@ -308,7 +300,7 @@ function integrationSampleRest() { } function integrationInfoRest() { - logger.incoming.info('Info integration'); + incomingLogger.info('Info integration'); return { statusCode: 200, body: { @@ -387,7 +379,7 @@ const Api = new WebHookAPI({ }); 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 a7cd0a682c4a..33cc33ddb24d 100644 --- a/app/integrations/server/lib/triggerHandler.js +++ b/app/integrations/server/lib/triggerHandler.js @@ -9,10 +9,10 @@ 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 { settings } from '../../../settings/server'; +import { getRoomByNameOrIdWithOptionToJoin, processWebhookMessage } from '../../../lib/server'; +import { outgoingLogger } from '../logger'; import { integrations } from '../../lib/rocketchat'; export class RocketChatIntegrationHandler { @@ -26,17 +26,17 @@ export class RocketChatIntegrationHandler { } 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); } @@ -172,11 +172,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 +192,7 @@ export class RocketChatIntegrationHandler { message.channel = `#${ tmpRoom._id }`; } - message = processWebhookMessage(message, user, defaultValues, trigger); + message = processWebhookMessage(message, user, defaultValues); return message; } @@ -240,8 +240,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 +256,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 +292,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 +320,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 +375,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 +540,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 +552,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 +563,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 +581,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 +603,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 +628,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 +670,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 +706,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).`); + outgoingLogger.error(`Disabling the Integration "${ trigger.name }" because the status code was 401 (Gone).`); Models.Integrations.update({ _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 +751,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 4a293574b984..dd0076279883 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/irc/server/irc-bridge/index.js b/app/irc/server/irc-bridge/index.js index c796720c471d..8f296f847e95 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 a5ebf1ddfb82..aed761ff127b 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/servers/RFC2813/index.js b/app/irc/server/servers/RFC2813/index.js index 6d6335c4017b..77750b9d1059 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/ldap/client/index.js b/app/ldap/client/index.js deleted file mode 100644 index fecf898e1ae4..000000000000 --- 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 b2d6cf5d71ff..000000000000 --- 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 acdd21c00c48..000000000000 --- 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 eafdd3161796..000000000000 --- 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 79425c9dfe74..000000000000 --- 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 10e8d90bfe34..000000000000 --- 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 f4f0c65f993b..000000000000 --- 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 312d4dbe96d2..000000000000 --- 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 1511a944e238..000000000000 --- 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/client/UserDeleted.js b/app/lib/client/UserDeleted.js deleted file mode 100644 index 8be7d5ed5d46..000000000000 --- 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 64ed16e27b14..5619d5776428 100644 --- a/app/lib/client/index.js +++ b/app/lib/client/index.js @@ -1,11 +1,6 @@ import '../lib/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 43251237c9a5..000000000000 --- 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 4cc975c68ce2..5cade0413ef3 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/startup/commands.js b/app/lib/client/lib/startup/commands.js deleted file mode 100644 index 0d9a7179ae9d..000000000000 --- 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 c0414276b408..000000000000 --- 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/lib/MessageTypes.js b/app/lib/lib/MessageTypes.js index be706e8d3a09..0ac925f68c00 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/server/functions/addOAuthService.js b/app/lib/server/functions/addOAuthService.js index cb5013129e19..2308a0bf477b 100644 --- a/app/lib/server/functions/addOAuthService.js +++ b/app/lib/server/functions/addOAuthService.js @@ -27,12 +27,37 @@ export function addOAuthService(name, values = {}) { 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 }-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'] }); + 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', + enterprise: true, + invalidValue: 'groups', + modules: ['oauth-enterprise'] }); + 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 }-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'] }); + 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', + enterprise: true, + invalidValue: false, + modules: ['oauth-enterprise'] }); + 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/getFullUserData.js b/app/lib/server/functions/getFullUserData.js index 9113c777e81e..1a33ac474316 100644 --- a/app/lib/server/functions/getFullUserData.js +++ b/app/lib/server/functions/getFullUserData.js @@ -1,6 +1,3 @@ -import s from 'underscore.string'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; - import { Logger } from '../../../logger'; import { settings } from '../../../settings'; import { Users } from '../../../models/server'; @@ -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 fb3c89861341..5286201e0100 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 12ce5fb8d5fc..000000000000 --- 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/mobile.js b/app/lib/server/functions/notifications/mobile.js index 7dd6cf28e2e2..c6e3e2cde110 100644 --- a/app/lib/server/functions/notifications/mobile.js +++ b/app/lib/server/functions/notifications/mobile.js @@ -191,10 +191,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 d96256df548f..bca7c4087330 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 }); if (messageObj.pushm && messageObj.pushm === 'true') { message.pushm = true; diff --git a/app/lib/server/functions/saveUser.js b/app/lib/server/functions/saveUser.js index a5989de04a1e..83ea5273d870 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; @@ -141,13 +142,23 @@ function validateUserData(userId, userData) { } } -function validateUserEditing(userId, userData) { +/** + * Validate permissions to edit user fields + * + * @param {string} userId + * @param {{ _id: string, roles: string[], username: string, name: string, statusText: string, email: string, password: string}} userData + */ +export function validateUserEditing(userId, userData) { const editingMyself = userData._id && userId === userData._id; 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', @@ -161,28 +172,28 @@ 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', @@ -227,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); @@ -356,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 bac0d2c08103..a75093fafb10 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 (!settings.get('Livechat_kill_switch') || room.lastMessage.msg !== settings.get('Livechat_kill_switch_message')) { if (message) { diff --git a/app/lib/server/functions/setUserActiveStatus.js b/app/lib/server/functions/setUserActiveStatus.js index 28d3111d0d2f..63dd06be185d 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'; @@ -55,8 +56,20 @@ export function setUserActiveStatus(userId, active, confirmRelinquish = false) { 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 ecc6e88e895f..8fa18240cf18 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 832db3defcf8..8795fb6b01fc 100644 --- a/app/lib/server/functions/setUsername.js +++ b/app/lib/server/functions/setUsername.js @@ -9,6 +9,7 @@ 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 +45,7 @@ export const _setUsername = function(userId, u, fullUser) { }); } } catch (e) { - console.error(e); + SystemLogger.error(e); } // Set new username* Users.setUsername(user._id, username); diff --git a/app/lib/server/functions/updateMessage.js b/app/lib/server/functions/updateMessage.js index eaa60b0318b5..bf68daf712d6 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 9e2e7a965e57..981d03eb41ff 100644 --- a/app/lib/server/index.js +++ b/app/lib/server/index.js @@ -10,7 +10,6 @@ import '../lib/MessageTypes'; import '../startup'; import '../startup/defaultRoomTypes'; import './lib/bugsnag'; -import './lib/configLogger'; import './lib/debug'; import './lib/loginErrorMessageOverride'; import './oauth/oauth'; @@ -38,10 +37,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 c707d7e2ca76..000000000000 --- 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 000000000000..8686d581d7b1 --- /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.get('Bugsnag_api_key', (_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 3039cc628598..000000000000 --- 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 0fb22935c534..2d37b333468e 100644 --- a/app/lib/server/lib/debug.js +++ b/app/lib/server/lib/debug.js @@ -3,27 +3,12 @@ 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; @@ -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 000000000000..f3cc3149661b --- /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/interceptDirectReplyEmails.js b/app/lib/server/lib/interceptDirectReplyEmails.js index ed7f022b5313..5447f80f679a 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/processDirectEmail.js b/app/lib/server/lib/processDirectEmail.js index b00a042d476d..3b97938d4694 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 { hasPermission } from '../../../authorization/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { sendMessage as _sendMessage } from '../functions'; export const processDirectEmail = function(email) { @@ -126,6 +127,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 28a156282b1a..d85c82867c40 100644 --- a/app/lib/server/lib/sendNotificationsOnMessage.js +++ b/app/lib/server/lib/sendNotificationsOnMessage.js @@ -10,7 +10,6 @@ import { callJoinRoom, messageContainsHighlight, parseMessageTextPerUser, replac import { getEmailData, shouldNotifyEmail } from '../functions/notifications/email'; import { sendWebPush, getPushData, shouldNotifyMobile, getNotificationPayload } 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, @@ -205,7 +186,6 @@ export const sendNotification = async ({ const project = { $project: { - audioNotifications: 1, desktopNotifications: 1, emailNotifications: 1, mobilePushNotifications: 1, diff --git a/app/lib/server/lib/validateEmailDomain.js b/app/lib/server/lib/validateEmailDomain.js index 1bdac7ecf78c..d9cdd297ad04 100644 --- a/app/lib/server/lib/validateEmailDomain.js +++ b/app/lib/server/lib/validateEmailDomain.js @@ -5,6 +5,7 @@ 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 = []; @@ -51,7 +52,7 @@ export const validateEmailDomain = function(email) { if (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/getFullUserData.js b/app/lib/server/methods/getFullUserData.js deleted file mode 100644 index 3c551dacd1ee..000000000000 --- 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/getServerInfo.js b/app/lib/server/methods/getServerInfo.js deleted file mode 100644 index 4445eaf36f35..000000000000 --- 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/sendMessage.js b/app/lib/server/methods/sendMessage.js index 3d91d72022d0..871bd69a431f 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 132af472a03d..c3284ad24908 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/oAuthServicesUpdate.js b/app/lib/server/startup/oAuthServicesUpdate.js index 2e35236f05c5..b414abdab315 100644 --- a/app/lib/server/startup/oAuthServicesUpdate.js +++ b/app/lib/server/startup/oAuthServicesUpdate.js @@ -7,18 +7,12 @@ import { Logger } from '../../../logger'; import { settings } from '../../../settings'; 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); + logger.info({ oauth_updated: service.key }); let serviceName = service.key.replace('Accounts_OAuth_', ''); if (serviceName === 'Meteor') { serviceName = 'meteor-developer'; diff --git a/app/lib/server/startup/rateLimiter.js b/app/lib/server/startup/rateLimiter.js index f12f23caa8c8..f7339c19958d 100644 --- a/app/lib/server/startup/rateLimiter.js +++ b/app/lib/server/startup/rateLimiter.js @@ -7,7 +7,7 @@ import { settings } from '../../../settings'; 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()) @@ -111,7 +111,7 @@ const ruleIds = {}; const callback = (message, name) => (reply, input) => { if (reply.allowed === false) { logger.info('DDP RATE LIMIT:', message); - logger.info(JSON.stringify({ ...reply, ...input }, null, 2)); + logger.info({ ...reply, ...input }); metrics.ddpRateLimitExceeded.inc({ limit_name: name, user_id: input.userId, diff --git a/app/lib/server/startup/settings.js b/app/lib/server/startup/settings.js index 0b4be8bb722c..c6963fb4ce64 100644 --- a/app/lib/server/startup/settings.js +++ b/app/lib/server/startup/settings.js @@ -259,24 +259,6 @@ settings.addGroup('Accounts', function() { 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: [ @@ -295,7 +277,7 @@ settings.addGroup('Accounts', function() { ], public: true, }); - this.add('Accounts_Default_User_Preferences_mobileNotifications', 'all', { + this.add('Accounts_Default_User_Preferences_pushNotifications', 'all', { type: 'select', values: [ { @@ -955,12 +937,6 @@ settings.addGroup('General', function() { 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, @@ -1338,6 +1314,11 @@ settings.addGroup('Message', function() { }, ], }); + + this.add('Message_Code_highlight', 'javascript,css,markdown,dockerfile,json,go,rust,clean,bash,plaintext,powershell,scss,shell,yaml,vim', { + type: 'string', + public: true, + }); }); settings.addGroup('Meta', function() { @@ -1707,14 +1688,6 @@ settings.addGroup('Logs', function() { ], 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', }); diff --git a/app/lib/server/startup/settingsOnLoadDirectReply.js b/app/lib/server/startup/settingsOnLoadDirectReply.js index 73675065cb5b..f38565b79981 100644 --- a/app/lib/server/startup/settingsOnLoadDirectReply.js +++ b/app/lib/server/startup/settingsOnLoadDirectReply.js @@ -1,37 +1,40 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; +import { Logger } from '../../../logger/server'; import { settings } from '../../../settings'; 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...'); + 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 +42,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; diff --git a/app/lib/server/startup/settingsOnLoadSMTP.js b/app/lib/server/startup/settingsOnLoadSMTP.js index 4ad9b806933d..cb31e03e7541 100644 --- a/app/lib/server/startup/settingsOnLoadSMTP.js +++ b/app/lib/server/startup/settingsOnLoadSMTP.js @@ -1,10 +1,11 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; const buildMailURL = _.debounce(function() { - console.log('Updating process.env.MAIL_URL'); + SystemLogger.info('Updating process.env.MAIL_URL'); if (settings.get('SMTP_Host')) { process.env.MAIL_URL = `${ settings.get('SMTP_Protocol') }://`; diff --git a/app/livechat/client/lib/dateHandler.js b/app/livechat/client/lib/dateHandler.js index ac7d4167aa53..8639280b2ee6 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 526c5158cf4f..97e9bcf0fb68 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 2dac3bc7956a..42a9f1f51284 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 765088722c26..c751c701e9ea 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' || !await callWithErrorHandling('canAccessRoom', inquiry.rid, Meteor.userId())) { // 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 e46c66e37608..53a11f7ba3ba 100644 --- a/app/livechat/client/views/app/tabbar/agentEdit.js +++ b/app/livechat/client/views/app/tabbar/agentEdit.js @@ -6,7 +6,8 @@ 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'; Template.agentEdit.helpers({ canEditDepartment() { diff --git a/app/livechat/client/views/app/tabbar/agentInfo.js b/app/livechat/client/views/app/tabbar/agentInfo.js index 6894586f32da..c90de337a12e 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/visitorInfo.js b/app/livechat/client/views/app/tabbar/visitorInfo.js index 56de7d9275c0..a58246dbd164 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 ae06c775aff2..ec26bdf0130b 100644 --- a/app/livechat/client/views/app/tabbar/visitorTranscript.js +++ b/app/livechat/client/views/app/tabbar/visitorTranscript.js @@ -3,7 +3,8 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; import toastr from 'toastr'; -import { t, isEmail, handleError, roomTypes } from '../../../../../utils'; +import { handleError } from '../../../../../../client/lib/utils/handleError'; +import { t, isEmail, roomTypes } from '../../../../../utils'; import { APIClient } from '../../../../../utils/client'; import './visitorTranscript.html'; diff --git a/app/livechat/imports/server/rest/facebook.js b/app/livechat/imports/server/rest/facebook.js index b4b8efa55034..cb9f19afc86c 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/sms.js b/app/livechat/imports/server/rest/sms.js index fe627ba6d817..4f29e7e997e0 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 } }); @@ -83,6 +84,10 @@ API.v1.addRoute('livechat/sms-incoming/:service', { sms: { from: sms.to, }, + source: { + type: OmnichannelSourceType.SMS, + alias: this.urlParams.service, + }, }, }; @@ -132,7 +137,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 b51e9318103c..cbda364a2ee4 100644 --- a/app/livechat/imports/server/rest/upload.js +++ b/app/livechat/imports/server/rest/upload.js @@ -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/server/api/v1/message.js b/app/livechat/server/api/v1/message.js index fc980bdc84ae..47a983a95600 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() { @@ -58,6 +59,11 @@ API.v1.addRoute('livechat/message', { token, }, agent, + roomInfo: { + source: { + type: this.isWidget() ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, + }, + }, }; const result = Promise.await(Livechat.sendMessage(sendMessage)); @@ -307,6 +313,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 ed5c28f13ab0..5dcbf8cf1dd0 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); } diff --git a/app/livechat/server/api/v1/videoCall.js b/app/livechat/server/api/v1/videoCall.js index 4f7b5cfd524f..38b9c2d66491 100644 --- a/app/livechat/server/api/v1/videoCall.js +++ b/app/livechat/server/api/v1/videoCall.js @@ -6,6 +6,7 @@ import { Messages } from '../../../../models'; import { settings as rcSettings } from '../../../../settings'; import { API } from '../../../../api/server'; import { findGuest, getRoom, settings } from '../lib/livechat'; +import { OmnichannelSourceType } from '../../../../../definition/IRoom'; API.v1.addRoute('livechat/video.call/:token', { get() { @@ -26,7 +27,13 @@ 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) { @@ -50,7 +57,7 @@ API.v1.addRoute('livechat/video.call/:token', { timeout: new Date(Date.now() + 3600 * 1000), }; - return API.v1.success({ videoCall }); + return API.v1.success(this.deprecationWarning({ videoCall })); } catch (e) { return API.v1.failure(e); } diff --git a/app/livechat/server/business-hour/AbstractBusinessHour.ts b/app/livechat/server/business-hour/AbstractBusinessHour.ts index b9da24988ef3..3b564b2c4499 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.setLivechatStatus(agentId, status); + } } export abstract class AbstractBusinessHourType { diff --git a/app/livechat/server/business-hour/BusinessHourManager.ts b/app/livechat/server/business-hour/BusinessHourManager.ts index cc849aa62e5d..e1b5558d0acd 100644 --- a/app/livechat/server/business-hour/BusinessHourManager.ts +++ b/app/livechat/server/business-hour/BusinessHourManager.ts @@ -5,6 +5,7 @@ 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'; 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 6d6b96b6da7a..fa25f40bf503 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/hooks/RDStation.js b/app/livechat/server/hooks/RDStation.js index bc60b1c1c9ef..b15453995eda 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/index.js b/app/livechat/server/index.js index 79e0bf81ef03..06d11207d777 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'; @@ -71,7 +71,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/Helper.js b/app/livechat/server/lib/Helper.js index d27556933abc..fb42f3949724 100644 --- a/app/livechat/server/lib/Helper.js +++ b/app/livechat/server/lib/Helper.js @@ -15,6 +15,7 @@ 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])?)*$/; @@ -60,6 +61,12 @@ 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', + }, }, extraRoomInfo); const roomId = Rooms.insert(room); @@ -196,7 +203,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 []; } }; @@ -250,8 +257,12 @@ export const dispatchInquiryQueued = (inquiry, agent) => { // Alert only the online agents of the queued request const onlineAgents = Livechat.getOnlineAgents(department, agent); - logger.debug(`Notifying ${ onlineAgents.count() } agents of new inquiry`); + if (!onlineAgents) { + logger.debug('Cannot notify agents of queued inquiry. No online agents found'); + return; + } + logger.debug(`Notifying ${ onlineAgents.count() } agents of new inquiry`); const notificationUserName = v && (v.name || v.username); onlineAgents.forEach((agent) => { @@ -426,7 +437,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); @@ -437,6 +450,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 7584f9cd9b25..e5da9cddeb6a 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -44,15 +44,16 @@ import { Notifications } from '../../../notifications'; const rooms = {}; +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, { @@ -98,7 +99,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) { @@ -756,7 +759,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' }); } @@ -775,9 +778,9 @@ 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 ...'); + Livechat.webhookLogger.warn('Will try again in 10 seconds ...'); setTimeout(Meteor.bindEnvironment(function() { Livechat.sendRequest(postData, callback, attempts--); }), 10000); @@ -855,7 +858,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; } @@ -1141,6 +1144,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(); + }, + getTranscript({ token, rid, user }) { check(rid, String); @@ -1245,7 +1262,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' }); } diff --git a/app/livechat/server/lib/QueueManager.js b/app/livechat/server/lib/QueueManager.js index f8d1540e84bd..c3a10ebb5bc4 100644 --- a/app/livechat/server/lib/QueueManager.js +++ b/app/livechat/server/lib/QueueManager.js @@ -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 52f9a0c858e0..8e1fa4892772 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() { + // 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/routing/External.js b/app/livechat/server/lib/routing/External.js index 351c5571cc7d..59fbb2bbfbda 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() { @@ -48,7 +49,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/methods/facebook.js b/app/livechat/server/methods/facebook.js index 7d17161fe938..a6ef3b9aac94 100644 --- a/app/livechat/server/methods/facebook.js +++ b/app/livechat/server/methods/facebook.js @@ -1,6 +1,7 @@ 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'; @@ -59,7 +60,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 f6cc509f1441..d60d36957530 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 42b92ac31a4b..caa86c650899 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 741e26ccee53..30fdec7835d8 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/saveOfficeHours.js b/app/livechat/server/methods/saveOfficeHours.js deleted file mode 100644 index d0e16a59843b..000000000000 --- 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 b294d00f22c0..a5af6877af95 100644 --- a/app/livechat/server/methods/sendMessageLivechat.js +++ b/app/livechat/server/methods/sendMessageLivechat.js @@ -3,6 +3,7 @@ import { Match, check } from 'meteor/check'; import { LivechatVisitors } from '../../../models'; import { Livechat } from '../lib/Livechat'; +import { OmnichannelSourceType } from '../../../../definition/IRoom'; Meteor.methods({ sendMessageLivechat({ token, _id, rid, msg, file, attachments }, agent) { @@ -40,6 +41,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 2779af23d2bb..ccea332e18a3 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 58e51ec30087..f64c8d260830 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 45a642a66176..bc5b32fd75a0 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/startup.js b/app/livechat/server/startup.js index 719a545de2b1..edbbb6fecf36 100644 --- a/app/livechat/server/startup.js +++ b/app/livechat/server/startup.js @@ -1,4 +1,5 @@ import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { roomTypes } from '../../utils'; @@ -9,6 +10,8 @@ import { LivechatAgentActivityMonitor } from './statistics/LivechatAgentActivity 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'; @@ -54,4 +57,10 @@ Meteor.startup(async () => { } return businessHourManager.stopManager(); }); + + settings.get('Livechat_Routing_Method', function(key, value) { + RoutingManager.setMethodNameAndStartQueue(value); + }); + + Accounts.onLogout(({ user }) => user?.roles?.includes('livechat-agent') && !user?.roles?.includes('bot') && Livechat.setUserStatusLivechat(user._id, 'not-available')); }); diff --git a/app/livestream/client/views/broadcastView.js b/app/livestream/client/views/broadcastView.js index 4fbe6fc554fc..afa8f26142d3 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 19f64cde9299..d73a31336fae 100644 --- a/app/livestream/client/views/liveStreamTab.js +++ b/app/livestream/client/views/liveStreamTab.js @@ -9,11 +9,12 @@ 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'; export const call = (...args) => new Promise(function(resolve, reject) { Meteor.call(...args, function(err, result) { diff --git a/app/livestream/server/routes.js b/app/livestream/server/routes.js index 3a52aec6031c..36a093d2c25a 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/logger/client/logger.js b/app/logger/client/logger.js index 61eb0bf9c11d..4e05f18ba3b3 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 06fa09708dbe..5630b3a0aa2a 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 d9b6d0a8e84f..000000000000 --- a/app/logger/server/server.js +++ /dev/null @@ -1,322 +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) { - // require('log-timestamp'); - 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 3bff9032aafb..000000000000 --- 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 65dc7687331b..6b33a1dc2b0f 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 9d892acb0c50..c06c568ff822 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/markdown/lib/hljs.js b/app/markdown/lib/hljs.js index a8a57dd89cde..6a5232ba2f1c 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 4d349a4bb142..d7ca58fa1c84 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 5b84b0d45c66..87532beb0d3d 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/message-mark-as-unread/client/actionButton.js b/app/message-mark-as-unread/client/actionButton.js index cc0ef68a6957..6b79e608d316 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 1327ecda6e8c..4752c07a23dc 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 da31e72a35af..c10a3fc1ae5c 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 9650db05f311..39966fbbfb35 100644 --- a/app/message-pin/client/actionButton.js +++ b/app/message-pin/client/actionButton.js @@ -6,11 +6,11 @@ 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'; Meteor.startup(function() { MessageAction.addButton({ diff --git a/app/message-snippet/client/page/snippetPage.js b/app/message-snippet/client/page/snippetPage.js index c4bd5d0b4a69..a65025e547c4 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-star/client/actionButton.js b/app/message-star/client/actionButton.js index 5c194b2103f6..940f1d073c54 100644 --- a/app/message-star/client/actionButton.js +++ b/app/message-star/client/actionButton.js @@ -4,12 +4,12 @@ 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'; Meteor.startup(function() { MessageAction.addButton({ diff --git a/app/meteor-accounts-saml/server/definition/IAttributeMapping.ts b/app/meteor-accounts-saml/server/definition/IAttributeMapping.ts index eda13d450d5e..573ca7afe4b0 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 126a300eea07..a7fe88d19109 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 7a643463f729..7b4e41019ef8 100644 --- a/app/meteor-accounts-saml/server/definition/ISAMLUser.ts +++ b/app/meteor-accounts-saml/server/definition/ISAMLUser.ts @@ -1,5 +1,4 @@ export interface ISAMLUser { - customFields: Map; emailList: Array; fullName: string | null; roles: Array; diff --git a/app/meteor-accounts-saml/server/definition/IServiceProviderOptions.ts b/app/meteor-accounts-saml/server/definition/IServiceProviderOptions.ts index db045b2e6c67..acc67681fcdc 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 ca876193ac37..c2141d74a4a7 100644 --- a/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/app/meteor-accounts-saml/server/lib/SAML.ts @@ -17,6 +17,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, { @@ -71,7 +72,7 @@ export class SAML { } public static insertOrUpdateSAMLUser(userObject: ISAMLUser): {userId: string; token: string} { - const { roleAttributeSync, generateUsername, immutableProperty, nameOverwrite, mailOverwrite, channelsAttributeUpdate } = SAMLUtils.globalSettings; + const { generateUsername, immutableProperty, nameOverwrite, mailOverwrite, channelsAttributeUpdate } = SAMLUtils.globalSettings; let customIdentifierMatch = false; let customIdentifierAttributeName: string | null = null; @@ -102,8 +103,8 @@ export class SAML { address: email, verified: settings.get('Accounts_Verify_Email_For_External_Accounts'), })); - const globalRoles = userObject.roles; + const { roles } = userObject; let { username } = userObject; const active = !settings.get('Accounts_ManuallyApproveNewUsers'); @@ -112,7 +113,7 @@ export class SAML { const newUser: Record = { name: userObject.fullName, active, - globalRoles, + globalRoles: roles, emails, services: { saml: { @@ -169,10 +170,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 +180,8 @@ export class SAML { updateData.name = userObject.fullName; } - if (roleAttributeSync) { - updateData.roles = globalRoles; + if (roles) { + updateData.roles = roles; } if (userObject.channels && channelsAttributeUpdate === true) { @@ -215,7 +212,7 @@ export class SAML { res.writeHead(200); res.write(serviceProvider.generateServiceProviderMetadata()); res.end(); - } catch (err) { + } catch (err: any) { showErrorMessage(res, err); } } @@ -240,7 +237,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 +290,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(); @@ -471,8 +468,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 5ab421c7903d..e582c1b5acef 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 20671c07a415..13d1fc3291b8 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,7 @@ export class SAMLUtils { public static mapProfileToUserObject(profile: Record): ISAMLUser { const userDataMap = this.getUserDataMapping(); SAMLUtils.log('parsed userDataMap', userDataMap); - const { defaultUserRole = 'user', roleAttributeName } = this.globalSettings; + const { defaultUserRole = 'user' } = this.globalSettings; if (userDataMap.identifier.type === 'custom') { if (!userDataMap.identifier.attribute) { @@ -440,7 +429,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 +439,6 @@ export class SAMLUtils { } const userObject: ISAMLUser = { - customFields: new Map(), samlLogin: { provider: this.relayState, idp: profile.issuer, @@ -470,15 +457,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 +469,11 @@ 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 7f6d85181ef0..67da8570c493 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 1a813b3f0a75..89bbc0758093 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); } @@ -178,7 +178,7 @@ export class ResponseParser { const encData = encAssertion.getElementsByTagNameNS('*', 'EncryptedData')[0]; xmlenc.decrypt(encData, options, function(err: Error, result: string) { if (err) { - console.error(err); + SAMLUtils.error(err); } const document = new xmldom.DOMParser().parseFromString(result, 'text/xml'); @@ -320,7 +320,7 @@ export class ResponseParser { const options = { key: this.serviceProviderOptions.privateKey }; xmlenc.decrypt(encSubject.getElementsByTagNameNS('*', 'EncryptedData')[0], options, function(err: Error, result: string) { 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 b18b509c772d..38b2c8e9aa9b 100644 --- a/app/meteor-accounts-saml/server/lib/settings.ts +++ b/app/meteor-accounts-saml/server/lib/settings.ts @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { ServiceConfiguration } from 'meteor/service-configuration'; import { settings } from '../../../settings/server'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { SettingComposedValue } from '../../../settings/lib/settings'; import { IServiceProviderOptions } from '../definition/IServiceProviderOptions'; import { SAMLUtils } from './Utils'; @@ -18,7 +19,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 +36,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 +46,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 +88,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, @@ -131,294 +131,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', - }); + settings.addGroup('SAML', function() { + this.set({ + 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.set({ + 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 4edae1cb301a..d4352315459b 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 84beec3091aa..6b73c7f386ec 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', @@ -16,7 +18,7 @@ Accounts.registerLoginHandler('saml', function(loginRequest) { } const loginResult = SAML.retrieveCredential(loginRequest.credentialToken); - SAMLUtils.log(`RESULT :${ JSON.stringify(loginResult) }`); + 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 ceedc2de0cb1..9ee693123a9a 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 d76cf007d418..b915ad7b60f2 100644 --- a/app/meteor-accounts-saml/server/startup.ts +++ b/app/meteor-accounts-saml/server/startup.ts @@ -6,16 +6,14 @@ 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.get(/^SAML_.+/, updateServices); +}); diff --git a/app/meteor-accounts-saml/tests/data.ts b/app/meteor-accounts-saml/tests/data.ts index b74f59f0ed2c..cf366a7564c5 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 b806e2dd2477..7cee56b74885 100644 --- a/app/meteor-accounts-saml/tests/server.tests.ts +++ b/app/meteor-accounts-saml/tests/server.tests.ts @@ -632,13 +632,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 +649,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.have.property('roles').that.is.an('array').with.members(['user']); 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 +727,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 +840,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 +858,6 @@ describe('SAML', () => { 'otherRoles', 'language', 'channels', - 'customField1', ], }, }; @@ -925,7 +881,6 @@ describe('SAML', () => { 'otherRoles', 'language', 'channels', - 'customField1', ]); // Workaround because chai doesn't handle Maps very well @@ -1001,11 +956,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 +977,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 66a24da4df91..022d9d476eb1 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'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { metrics } from './metrics'; import { getAppsStatistics } from '../../../statistics/server/lib/getAppsStatistics'; @@ -47,7 +48,7 @@ const setPrometheusData = async () => { } 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,7 +175,7 @@ const updatePrometheusConfig = async () => { gcStats()(); } } catch (error) { - console.error(error); + SystemLogger.error(error); } Object.assign(was, is); diff --git a/app/migrations/index.js b/app/migrations/index.js deleted file mode 100644 index ca39cd0df4b1..000000000000 --- 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 bd8a290dd2b3..000000000000 --- 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 25e4b5fd58e8..000000000000 --- 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 c7d25a94dad6..000000000000 --- 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/ChatMessage.js b/app/models/client/models/ChatMessage.js index f891f25f198b..0c6999e31361 100644 --- a/app/models/client/models/ChatMessage.js +++ b/app/models/client/models/ChatMessage.js @@ -5,9 +5,9 @@ import { Tracker } from 'meteor/tracker'; import { CachedCollection } from '../../../ui-cached-collection'; import { CachedChatSubscription } from './CachedChatSubscription'; import { ChatSubscription } from './ChatSubscription'; -import { getConfig } from '../../../ui-utils/client/config'; +import { getConfig } from '../../../../client/lib/utils/getConfig'; import { cleanMessagesAtStartup, triggerOfflineMsgs } from '../../../utils'; -import { renderMessageBody } from '../../../../client/lib/renderMessageBody'; +import { renderMessageBody } from '../../../../client/lib/utils/renderMessageBody'; import { promises } from '../../../promises/client'; import { callbacks } from '../../../callbacks'; import { settings } from '../../../settings'; diff --git a/app/models/client/models/Roles.js b/app/models/client/models/Roles.js index 11fc35c9cc0f..fa439cdd2a55 100644 --- a/app/models/client/models/Roles.js +++ b/app/models/client/models/Roles.js @@ -5,9 +5,7 @@ import * as Models from '..'; const CachedRoles = new CachedCollection({ name: 'roles' }); -const Roles = CachedRoles.collection; - -Object.assign(Roles, { +const Roles = Object.assign(CachedRoles.collection, { 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 a74764b0ae40..d550eaa0e8d6 100644 --- a/app/models/server/index.js +++ b/app/models/server/index.js @@ -41,6 +41,7 @@ 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'; import PushNotificationSubscriptions from './models/PushNotificationSubscriptions'; export { AppsLogsModel } from './models/apps-logs-model'; @@ -96,4 +97,5 @@ export { Analytics, OmnichannelQueue, EmailInbox, + ImportData, }; diff --git a/app/models/server/models/Avatars.js b/app/models/server/models/Avatars.js index eddc203fedde..b0e7e8cb5da1 100644 --- a/app/models/server/models/Avatars.js +++ b/app/models/server/models/Avatars.js @@ -81,32 +81,6 @@ export class Avatars extends Base { 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 }); diff --git a/app/importer/server/models/ImportData.ts b/app/models/server/models/ImportData.ts similarity index 88% rename from app/importer/server/models/ImportData.ts rename to app/models/server/models/ImportData.ts index a6afb291e19c..785086d7ba70 100644 --- a/app/importer/server/models/ImportData.ts +++ b/app/models/server/models/ImportData.ts @@ -1,5 +1,5 @@ -import { Base } from '../../../models/server'; -import { IImportUserRecord, IImportChannelRecord } from '../definitions/IImportRecord'; +import { Base } from './_Base'; +import { IImportUserRecord, IImportChannelRecord } from '../../../../definition/IImportRecord'; class ImportDataModel extends Base { constructor() { @@ -85,4 +85,4 @@ class ImportDataModel extends Base { } } -export const ImportData = new ImportDataModel(); +export default new ImportDataModel(); diff --git a/app/models/server/models/LivechatDepartmentAgents.js b/app/models/server/models/LivechatDepartmentAgents.js index cccfc9f83e8c..871c2ebf37c2 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/LivechatRooms.js b/app/models/server/models/LivechatRooms.js index 70edf612cba9..1d71be35aad0 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'; @@ -194,6 +193,17 @@ export class LivechatRooms extends Base { return this.findOne(query, options); } + findOneByVisitorTokenAndEmailThreadAndDepartment(visitorToken, emailThread, departmentId, options) { + const query = { + t: 'l', + 'v.token': visitorToken, + 'email.thread': emailThread, + ...departmentId && { departmentId }, + }; + + return this.findOne(query, options); + } + findOneOpenByVisitorTokenAndEmailThread(visitorToken, emailThread, options) { const query = { t: 'l', @@ -234,9 +244,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', }; @@ -247,7 +254,7 @@ export class LivechatRooms extends Base { }, }; - const livechatCount = findAndModify(query, null, update); + const livechatCount = Settings.findAndModify(query, null, update); return livechatCount.value.value; } diff --git a/app/models/server/models/LivechatVisitors.js b/app/models/server/models/LivechatVisitors.js index 5c58a8514176..9e7f6c64b3c2 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 2770c197ee7b..1a20414715d7 100644 --- a/app/models/server/models/Messages.js +++ b/app/models/server/models/Messages.js @@ -80,6 +80,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 } }); } @@ -95,17 +111,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); } diff --git a/app/models/server/models/Roles.js b/app/models/server/models/Roles.js index d305c071d100..e7576191d070 100644 --- a/app/models/server/models/Roles.js +++ b/app/models/server/models/Roles.js @@ -29,18 +29,14 @@ export class Roles extends Base { } updateById(_id, name, scope, description, mandatory2fa) { - const query = { _id }; - - const update = { - $set: { - ...name && { name }, - ...scope && { scope }, - ...description && { description }, - ...mandatory2fa && { mandatory2fa }, - }, + const queryData = { + name, + scope, + description, + mandatory2fa, }; - return this.update(query, update); + this.upsert({ _id }, { $set: queryData }); } createWithRandomId(name, scope = 'Users', description = '', protectedRole = true, mandatory2fa = false) { diff --git a/app/models/server/models/Rooms.js b/app/models/server/models/Rooms.js index 05751f59cae5..d884208317fc 100644 --- a/app/models/server/models/Rooms.js +++ b/app/models/server/models/Rooms.js @@ -334,6 +334,7 @@ export class Rooms extends Base { let channelName = s.trim(name); try { + // TODO evaluate if this function call should be here channelName = getValidRoomName(channelName, null, { allowDuplicates: true }); } catch (e) { console.error(e); diff --git a/app/models/server/models/Sessions.js b/app/models/server/models/Sessions.js index df764ccc3351..ffb43d6566a5 100644 --- a/app/models/server/models/Sessions.js +++ b/app/models/server/models/Sessions.js @@ -20,10 +20,6 @@ export const aggregates = { day: { $lte: day }, }], }, - }, { - $sort: { - _id: 1, - }, }, { $project: { userId: 1, @@ -51,6 +47,10 @@ export const aggregates = { time: { $sum: '$time' }, sessions: { $sum: 1 }, }, + }, { + $sort: { + time: -1, + }, }, { $group: { _id: { @@ -70,6 +70,10 @@ export const aggregates = { }, }, }, + }, { + $sort: { + _id: 1, + }, }, { $project: { _id: 0, @@ -183,6 +187,10 @@ export const aggregates = { $sum: '$time', }, }, + }, { + $sort: { + time: -1, + }, }, { $group: { _id: 1, @@ -312,6 +320,10 @@ export const aggregates = { $sum: '$devices.time', }, }, + }, { + $sort: { + time: -1, + }, }, { $project: { _id: 0, @@ -348,6 +360,10 @@ export const aggregates = { $sum: '$devices.time', }, }, + }, { + $sort: { + time: -1, + }, }, { $project: { _id: 0, @@ -384,6 +400,10 @@ export const aggregates = { $sum: '$devices.time', }, }, + }, { + $sort: { + time: -1, + }, }, { $project: { _id: 0, @@ -421,6 +441,10 @@ export const aggregates = { $sum: '$devices.time', }, }, + }, { + $sort: { + time: -1, + }, }, { $project: { _id: 0, diff --git a/app/models/server/models/Sessions.tests.js b/app/models/server/models/Sessions.tests.js index 2b8c7703531f..ba0b94c60892 100644 --- a/app/models/server/models/Sessions.tests.js +++ b/app/models/server/models/Sessions.tests.js @@ -2,9 +2,10 @@ import assert from 'assert'; +import { MongoMemoryServer } from 'mongodb-memory-server'; + import './Sessions.mocks.js'; -const mongoUnit = require('mongo-unit'); const { MongoClient } = require('mongodb'); const { aggregates } = require('./Sessions'); @@ -238,25 +239,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 +272,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 +282,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) => assert.strictEqual(docs.length, 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, { + assert.deepStrictEqual($match, { $and: [{ $or: [ { year: { $gt: 2018 } }, @@ -303,8 +309,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 31); - assert.deepEqual(docs, [ + assert.strictEqual(docs.length, 31); + assert.deepStrictEqual(docs, [ { _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 +350,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 2, day: 10 }); - assert.deepEqual($match, { + assert.deepStrictEqual($match, { year: 2019, $and: [{ $or: [ @@ -363,8 +369,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 31); - assert.deepEqual(docs, [ + assert.strictEqual(docs.length, 31); + assert.deepStrictEqual(docs, [ { _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 +410,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 5, day: 31 }); - assert.deepEqual($match, { + assert.deepStrictEqual($match, { year: 2019, month: 5, day: { $gte: 1, $lte: 31 }, @@ -414,8 +420,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 31); - assert.deepEqual(docs, [ + assert.strictEqual(docs.length, 31); + assert.deepStrictEqual(docs, [ { _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 +461,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 4, day: 30 }); - assert.deepEqual($match, { + assert.deepStrictEqual($match, { year: 2019, month: 4, day: { $gte: 1, $lte: 30 }, @@ -465,8 +471,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 30); - assert.deepEqual(docs, [ + assert.strictEqual(docs.length, 30); + assert.deepStrictEqual(docs, [ { _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 +511,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 2, day: 28 }); - assert.deepEqual($match, { + assert.deepStrictEqual($match, { year: 2019, month: 2, day: { $gte: 1, $lte: 28 }, @@ -515,8 +521,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 28); - assert.deepEqual(docs, [ + assert.strictEqual(docs.length, 28); + assert.deepStrictEqual(docs, [ { _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 +559,7 @@ describe('Sessions Aggregates', () => { const collection = db.collection('sessions_dates'); const $match = aggregates.getMatchOfLastMonthOrWeek({ year: 2019, month: 2, day: 27 }); - assert.deepEqual($match, { + assert.deepStrictEqual($match, { year: 2019, $and: [{ $or: [ @@ -572,8 +578,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 31); - assert.deepEqual(docs, [ + assert.strictEqual(docs.length, 31); + assert.deepStrictEqual(docs, [ { _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 +618,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) => assert.strictEqual(docs.length, 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); + + assert.strictEqual(docs.length, 3); + assert.deepStrictEqual(docs, [{ _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 +649,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 +721,6 @@ describe('Sessions Aggregates', () => { userId: 'xPZXw9xqM3kKshsse2', mostImportantRole: 'admin', }]); - - return collection.insertMany(docs); }); }); @@ -722,8 +728,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, [{ + assert.strictEqual(docs.length, 1); + assert.deepStrictEqual(docs, [{ count: 2, roles: [{ count: 1, @@ -746,8 +752,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, [{ + assert.strictEqual(docs.length, 1); + assert.deepStrictEqual(docs, [{ count: 1, roles: [{ count: 1, @@ -765,8 +771,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, [{ + assert.strictEqual(docs.length, 1); + assert.deepStrictEqual(docs, [{ count: 1, roles: [{ count: 1, @@ -784,8 +790,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, [{ + assert.strictEqual(docs.length, 2); + assert.deepStrictEqual(docs, [{ count: 3, time: 9695, type: 'browser', @@ -805,8 +811,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, [{ + assert.strictEqual(docs.length, 2); + assert.deepStrictEqual(docs, [{ count: 2, time: 5528, type: 'browser', @@ -826,8 +832,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, [{ + assert.strictEqual(docs.length, 2); + assert.deepStrictEqual(docs, [{ count: 3, time: 9695, name: 'Mac OS', @@ -845,8 +851,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, [{ + assert.strictEqual(docs.length, 2); + assert.deepStrictEqual(docs, [{ count: 2, time: 5528, name: 'Mac OS', @@ -864,7 +870,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, { + assert.deepStrictEqual($match, { $and: [{ $or: [ { year: { $gt: 2018 } }, @@ -884,8 +890,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 7); - assert.deepEqual(docs, [ + assert.strictEqual(docs.length, 7); + assert.deepStrictEqual(docs, [ { _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 +907,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, { + assert.deepStrictEqual($match, { year: 2019, $and: [{ $or: [ @@ -920,8 +926,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 7); - assert.deepEqual(docs, [ + assert.strictEqual(docs.length, 7); + assert.deepStrictEqual(docs, [ { _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 +943,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, { + assert.deepStrictEqual($match, { year: 2019, month: 5, day: { $gte: 1, $lte: 7 }, @@ -947,8 +953,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 7); - assert.deepEqual(docs, [ + assert.strictEqual(docs.length, 7); + assert.deepStrictEqual(docs, [ { _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 +970,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, { + assert.deepStrictEqual($match, { year: 2019, month: 5, day: { $gte: 8, $lte: 14 }, @@ -974,8 +980,8 @@ describe('Sessions Aggregates', () => { $match, }]).toArray() .then((docs) => { - assert.equal(docs.length, 7); - assert.deepEqual(docs, [ + assert.strictEqual(docs.length, 7); + assert.deepStrictEqual(docs, [ { _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 +997,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); + assert.strictEqual(docs.length, 0); }); }); @@ -999,8 +1005,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, [{ + assert.strictEqual(docs.length, 1); + assert.deepStrictEqual(docs, [{ count: 2, roles: [{ count: 1, @@ -1023,8 +1029,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, [{ + assert.strictEqual(docs.length, 2); + assert.deepStrictEqual(docs, [{ count: 3, time: 9695, type: 'browser', @@ -1044,8 +1050,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, [{ + assert.strictEqual(docs.length, 2); + assert.deepStrictEqual(docs, [{ count: 3, time: 9695, name: 'Mac OS', diff --git a/app/models/server/models/Settings.js b/app/models/server/models/Settings.js index 83169ac126f9..53d5dba3cf37 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 diff --git a/app/models/server/models/Subscriptions.js b/app/models/server/models/Subscriptions.js index 5aa6ec0caabe..7dac1e85acb8 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/Users.js b/app/models/server/models/Users.js index 4c82d30cb989..5cde675a5de8 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, @@ -357,7 +361,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; } @@ -898,12 +901,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 }; @@ -1483,16 +1480,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: { @@ -1635,6 +1622,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/_BaseDb.js b/app/models/server/models/_BaseDb.js index 46a285b53560..6c9473c81fb8 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 = { diff --git a/app/models/server/raw/Banners.ts b/app/models/server/raw/Banners.ts index 3a6a596d272c..301ea579bc00 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'; @@ -14,6 +14,30 @@ export class BannersRaw extends BaseRaw { 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/ImportData.ts b/app/models/server/raw/ImportData.ts new file mode 100644 index 000000000000..175e64380f56 --- /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/LivechatBusinessHours.ts b/app/models/server/raw/LivechatBusinessHours.ts index 7e4cb0218002..706fff8b19e8 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/Users.js b/app/models/server/raw/Users.js index 89b491ab2f9e..a021a91f25e6 100644 --- a/app/models/server/raw/Users.js +++ b/app/models/server/raw/Users.js @@ -138,6 +138,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 +259,20 @@ export class UsersRaw extends BaseRaw { return result.value; } + setLivechatStatus(userId, status) { // TODO: Create class Agent + const query = { + _id: userId, + }; + + const update = { + $set: { + statusLivechat: status, + }, + }; + + 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 +631,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 +686,12 @@ export class UsersRaw extends BaseRaw { }); } - removeResumeService(userId) { + unsetLoginTokens(userId) { return this.col.updateOne({ _id: userId, }, { - $unset: { - 'services.resume': 1, + $set: { + 'services.resume.loginTokens': [], }, }); } diff --git a/app/models/server/raw/index.ts b/app/models/server/raw/index.ts index 17661b6cddce..4651bf8a441d 100644 --- a/app/models/server/raw/index.ts +++ b/app/models/server/raw/index.ts @@ -71,6 +71,8 @@ import EmailMessageHistoryModel from '../models/EmailMessageHistory'; import { EmailMessageHistoryRaw } from './EmailMessageHistory'; import { api } from '../../../../server/sdk/api'; import { initWatchers } from '../../../../server/modules/watchers/watchers.module'; +import ImportDataModel from '../models/ImportData'; +import { ImportDataRaw } from './ImportData'; const trashCollection = trash.rawCollection(); @@ -109,6 +111,7 @@ export const Sessions = new SessionsRaw(SessionsModel.model.rawCollection(), tra 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 map = { [Messages.col.collectionName]: MessagesModel, diff --git a/app/nextcloud/server/addWebdavServer.js b/app/nextcloud/server/addWebdavServer.js index fa331312705f..89622e54fd79 100644 --- a/app/nextcloud/server/addWebdavServer.js +++ b/app/nextcloud/server/addWebdavServer.js @@ -1,7 +1,8 @@ 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) => { @@ -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/notification-queue/server/NotificationQueue.ts b/app/notification-queue/server/NotificationQueue.ts index 42f05827b919..d16da1cc71ee 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/nrr/client/nrr.js b/app/nrr/client/nrr.js index d783880c2770..5ec665da590d 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/oauth/oauth2-server.js b/app/oauth2-server-config/server/oauth/oauth2-server.js index f1c51982c760..438aaaa2e0e6 100644 --- a/app/oauth2-server-config/server/oauth/oauth2-server.js +++ b/app/oauth2-server-config/server/oauth/oauth2-server.js @@ -13,6 +13,13 @@ const oauth2server = new OAuth2Server({ 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 +30,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 +66,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 f6ba54a3d3ba..09415153a64b 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 34aa1a8eee1a..b859b4ed80de 100644 --- a/app/oembed/server/server.js +++ b/app/oembed/server/server.js @@ -14,6 +14,7 @@ import { OEmbedCache, Messages } from '../../models'; 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 = {}; @@ -224,7 +225,7 @@ OEmbed.getUrlMetaWithCache = function(url, withFragment) { try { OEmbedCache.createWithIdAndData(url, data); } catch (_error) { - console.error('OEmbed duplicated record', url); + SystemLogger.error('OEmbed duplicated record', url); } return data; } diff --git a/app/otr/client/rocketchat.otr.room.js b/app/otr/client/rocketchat.otr.room.js index 6dac9aaa0858..fa62a4627847 100644 --- a/app/otr/client/rocketchat.otr.room.js +++ b/app/otr/client/rocketchat.otr.room.js @@ -10,9 +10,9 @@ 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'; diff --git a/app/push-notifications/server/methods/saveNotificationSettings.js b/app/push-notifications/server/methods/saveNotificationSettings.js index 56537abd7683..5ec16221e141 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 693f8dcc0529..d84846fd592e 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 a677e630b196..dc3890825593 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 553e253eee47..f770aef378f6 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 e95ca8f88716..2d576fe3ea34 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 b8d6e3897130..7bb7a232b0fc 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/search/client/provider/result.js b/app/search/client/provider/result.js index 7df29b89906f..6d0d582d8e99 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/style/style.css b/app/search/client/style/style.css index 98b6bac81669..420d8295614c 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 86464e1c6acd..bd0ddbf10f4d 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/service/providerService.js b/app/search/server/service/providerService.js index ac913f8cfb46..2288a06e390b 100644 --- a/app/search/server/service/providerService.js +++ b/app/search/server/service/providerService.js @@ -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/settings/lib/settings.ts b/app/settings/lib/settings.ts index 588250fb16a6..109ed5fbb651 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/functions/settings.mocks.ts b/app/settings/server/functions/settings.mocks.ts index d28383b14252..8db74e3285d1 100644 --- a/app/settings/server/functions/settings.mocks.ts +++ b/app/settings/server/functions/settings.mocks.ts @@ -10,8 +10,18 @@ class SettingsClass { public upsertCalls = 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))); } upsert(query: any, update: any): void { diff --git a/app/settings/server/functions/settings.ts b/app/settings/server/functions/settings.ts index 72ad2a970f73..ffd25230cc55 100644 --- a/app/settings/server/functions/settings.ts +++ b/app/settings/server/functions/settings.ts @@ -7,6 +7,7 @@ import { SettingsBase } from '../../lib/settings'; import SettingsModel from '../../../models/server/models/Settings'; import { updateValue } from '../raw'; import { ISetting, SettingValue } from '../../../../definition/ISetting'; +import { SystemLogger } from '../../../../server/lib/logger/system'; const blockedSettings = new Set(); const hiddenSettings = new Set(); @@ -67,6 +68,8 @@ const overrideSetting = (_id: string, value: SettingValue, options: ISettingAddO export interface ISettingAddOptions extends Partial { force?: boolean; + actionText?: string; + code?: 'application/json'; } export interface ISettingAddGroupOptions { @@ -85,6 +88,7 @@ interface IUpdateOperator { }; $unset?: { section?: 1; + tab?: 1; }; } @@ -98,11 +102,13 @@ type Query = { type addSectionCallback = (this: { add(id: string, value: SettingValue, options: ISettingAddOptions): void; + set(options: ISettingAddOptions, cb: addSectionCallback): void; }) => void; type addGroupCallback = (this: { add(id: string, value: SettingValue, options: ISettingAddOptions): void; section(section: string, cb: addSectionCallback): void; + set(options: ISettingAddOptions, cb: addGroupCallback): void; }) => void; class Settings extends SettingsBase { @@ -112,37 +118,36 @@ class Settings extends SettingsBase { 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; + private validateOptions(_id: string, value: SettingValue, options: ISettingAddOptions): void { + const sorterKey = options.group && options.section ? `${ options.group }_${ options.section }` : options.group; + if (sorterKey && this._sorter[sorterKey] == null) { + if (options.group && options.section) { + const currentGroupValue = this._sorter[options.group] || 0; + this._sorter[sorterKey] = currentGroupValue * 1000; + } else { + this._sorter[sorterKey] = 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`); + SystemLogger.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 (sorterKey && options.sorter == null) { + options.sorter = this._sorter[sorterKey]++; } if (options.enableQuery != null) { options.enableQuery = JSON.stringify(options.enableQuery); } - if (options.i18nLabel == null) { - options.i18nLabel = _id; + if (options.displayQuery != null) { + options.displayQuery = JSON.stringify(options.displayQuery); } if (options.i18nDescription == null) { options.i18nDescription = `${ _id }_Description`; @@ -159,6 +164,21 @@ class Settings extends SettingsBase { if (options.autocomplete == null) { options.autocomplete = true; } + } + + /* + * Add a setting + */ + add(_id: string, value: SettingValue, { editor, ...options }: ISettingAddOptions = {}): boolean { + if (!_id || value == null) { + return false; + } + + this.validateOptions(_id, value, options); + options.blocked = options.blocked || false; + if (options.i18nLabel == null) { + options.i18nLabel = _id; + } value = overrideSetting(_id, value, options); @@ -195,6 +215,15 @@ class Settings extends SettingsBase { }; } + if (!options.tab) { + updateOperations.$unset = { + tab: 1, + }; + query.tab = { + $exists: false, + }; + } + const existentSetting = SettingsModel.findOne(query); if (existentSetting) { if (existentSetting.editor || !updateOperations.$setOnInsert.editor) { @@ -278,19 +307,36 @@ class Settings extends SettingsBase { } 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); - }, - }), - }); + const addWith = (preset: ISettingAddOptions) => (id: string, value: SettingValue, options: ISettingAddOptions = {}): void => { + const mergedOptions = Object.assign({}, preset, options); + this.add(id, value, mergedOptions); + }; + const sectionSetWith = (preset: ISettingAddOptions) => (options: ISettingAddOptions, cb: addSectionCallback): void => { + const mergedOptions = Object.assign({}, preset, options); + cb.call({ + add: addWith(mergedOptions), + set: sectionSetWith(mergedOptions), + }); + }; + const sectionWith = (preset: ISettingAddOptions) => (section: string, cb: addSectionCallback): void => { + const mergedOptions = Object.assign({}, preset, { section }); + cb.call({ + add: addWith(mergedOptions), + set: sectionSetWith(mergedOptions), + }); + }; + + const groupSetWith = (preset: ISettingAddOptions) => (options: ISettingAddOptions, cb: addGroupCallback): void => { + const mergedOptions = Object.assign({}, preset, options); + + cb.call({ + add: addWith(mergedOptions), + section: sectionWith(mergedOptions), + set: groupSetWith(mergedOptions), + }); + }; + + groupSetWith({ group: _id })({}, cb); } return true; } diff --git a/app/slackbridge/server/RocketAdapter.js b/app/slackbridge/server/RocketAdapter.js index e84efba8d201..db674910be44 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 c61092f7cde5..1cb12c2bdacc 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 f8cf66078391..6f25e8015ff3 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/slackbridge.js b/app/slackbridge/server/slackbridge.js index 0c3e7b9e6d42..addb0c1a2e9a 100644 --- a/app/slackbridge/server/slackbridge.js +++ b/app/slackbridge/server/slackbridge.js @@ -1,6 +1,6 @@ import SlackAdapter from './SlackAdapter.js'; import RocketAdapter from './RocketAdapter.js'; -import { logger } from './logger'; +import { classLogger, connLogger } from './logger'; import { settings } from '../../settings'; /** @@ -44,7 +44,7 @@ class SlackBridgeClass { } this.connected = true; - logger.connection.info('Enabled'); + connLogger.info('Enabled'); } } @@ -56,7 +56,7 @@ class SlackBridgeClass { }); this.slackAdapters = []; this.connected = false; - logger.connection.info('Disabled'); + connLogger.info('Disabled'); } } @@ -71,25 +71,25 @@ class SlackBridgeClass { } } - logger.class.debug(`Setting: ${ key }`, value); + classLogger.debug(`Setting: ${ key }`, 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) => { this.aliasFormat = value; - logger.class.debug(`Setting: ${ key }`, value); + classLogger.debug(`Setting: ${ key }`, 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) => { this.excludeBotnames = value; - logger.class.debug(`Setting: ${ key }`, value); + classLogger.debug(`Setting: ${ key }`, value); }); // Reactions settings.get('SlackBridge_Reactions_Enabled', (key, value) => { this.isReactionsEnabled = value; - logger.class.debug(`Setting: ${ key }`, value); + classLogger.debug(`Setting: ${ key }`, value); }); // Is this entire SlackBridge enabled @@ -99,7 +99,7 @@ class SlackBridgeClass { } else { this.disconnect(); } - logger.class.debug(`Setting: ${ key }`, value); + classLogger.debug(`Setting: ${ key }`, value); }); } } diff --git a/app/slashcommands-status/lib/status.js b/app/slashcommands-status/lib/status.js index 9f7a46588460..359068a0cda5 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 3b34239ef164..dd904c541083 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/sms/server/services/mobex.js b/app/sms/server/services/mobex.js index 1f41dece2210..a85f1650e65d 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 420909b71d3a..26cf98105858 100644 --- a/app/sms/server/services/twilio.js +++ b/app/sms/server/services/twilio.js @@ -8,6 +8,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 logger = new Logger('SMS: Twilio', {}); @@ -77,7 +78,7 @@ class Twilio { } if (isNaN(numMedia)) { - console.error(`Error parsing NumMedia ${ data.NumMedia }`); + SystemLogger.error(`Error parsing NumMedia ${ data.NumMedia }`); return returnData; } @@ -126,7 +127,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 3b0c7dd0ae92..c2b00e59d2e3 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/statistics/server/lib/getServicesStatistics.ts b/app/statistics/server/lib/getServicesStatistics.ts index 75bc6ca3dc2c..2bdb07b698f0 100644 --- a/app/statistics/server/lib/getServicesStatistics.ts +++ b/app/statistics/server/lib/getServicesStatistics.ts @@ -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 cf6390b2b009..b4ffb9f7dd25 100644 --- a/app/statistics/server/lib/statistics.js +++ b/app/statistics/server/lib/statistics.js @@ -18,14 +18,14 @@ import { } 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 { 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', @@ -168,7 +168,7 @@ export const statistics = { }], { 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(); @@ -211,6 +211,7 @@ export const statistics = { statistics.pushQueue = Promise.await(NotificationQueue.col.estimatedDocumentCount()); statistics.enterprise = getEnterpriseStatistics(); + Promise.await(Analytics.resetSeatRequestCount()); return statistics; }, diff --git a/app/theme/client/imports/components/contextual-bar.css b/app/theme/client/imports/components/contextual-bar.css index eb1d31edbb71..611d4015f4fb 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; @@ -224,7 +221,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 00f42098ede8..c173d0286dd9 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 4e9238db7f48..bd0aa7be3636 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: 2; diff --git a/app/theme/client/imports/components/message-box.css b/app/theme/client/imports/components/message-box.css index 676c0fd33813..79a435d072ba 100644 --- a/app/theme/client/imports/components/message-box.css +++ b/app/theme/client/imports/components/message-box.css @@ -52,19 +52,18 @@ } } - &__typing { - position: absolute; + &__action { 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 +284,7 @@ margin-top: 1rem; padding: 0; - &__typing { + &__activity { top: -1rem; margin-left: 1rem; @@ -377,7 +376,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 8fd3fb876a66..f9c5a8be28cd 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 52164b2c5d5a..097f241426e4 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/sidebar/sidebar-flex.css b/app/theme/client/imports/components/sidebar/sidebar-flex.css index b124ee54bcea..1022a18268be 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 1ff3e4b2ef7c..9c529886f777 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: 0; diff --git a/app/theme/client/imports/components/table.css b/app/theme/client/imports/components/table.css index c817bccc2150..88ea125bdeb2 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 5549cd6c3703..8e5f3dbbd573 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 4f945a91e0f6..21ef11f574fa 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 35349a13d96d..e645f9a14430 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 58116f517392..ebd2665546bf 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 5d1dce60c5a8..b949f84bf5cc 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 8b3cbe7172bc..1b4495da5ac8 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 318501c0840a..8875ba5b99f5 100644 --- a/app/theme/client/imports/general/base.css +++ b/app/theme/client/imports/general/base.css @@ -85,7 +85,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 7a8068481a6f..f14408e8cc80 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: 55%; 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; @@ -2698,7 +2682,6 @@ & input, & select { - position: relative; width: 100%; @@ -3062,7 +3045,6 @@ .rc-old .dropzone { & .dropzone-overlay { - position: absolute; z-index: 1000000; top: 0; diff --git a/app/theme/client/imports/general/variables.css b/app/theme/client/imports/general/variables.css index b497845d3e49..5f606bdff7f9 100644 --- a/app/theme/client/imports/general/variables.css +++ b/app/theme/client/imports/general/variables.css @@ -340,9 +340,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 b1a55d40ac20..fbdc4fcee936 100644 --- a/app/theme/server/server.js +++ b/app/theme/server/server.js @@ -10,13 +10,7 @@ import { settings } from '../../settings'; import { Logger } from '../../logger'; import { addStyle } from '../../ui-master/server/inject'; -const logger = new Logger('rocketchat:theme', { - methods: { - stop_rendering: { - type: 'info', - }, - }, -}); +const logger = new Logger('rocketchat:theme'); let currentHash = ''; let currentSize = 0; @@ -69,9 +63,9 @@ 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); diff --git a/app/threads/client/components/ThreadComponent.tsx b/app/threads/client/components/ThreadComponent.tsx index 5af90e6c875b..c7d86130da1f 100644 --- a/app/threads/client/components/ThreadComponent.tsx +++ b/app/threads/client/components/ThreadComponent.tsx @@ -16,18 +16,16 @@ import ThreadView from './ThreadView'; import { IMessage } from '../../../../definition/IMessage'; import { IRoom } from '../../../../definition/IRoom'; import { useTabBarOpenUserInfo } from '../../../../client/views/room/providers/ToolboxProvider'; +import { mapMessageFromApi } from '../../../../client/lib/utils/mapMessageFromApi'; 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 getMessageParsed = useCallback<(params: { msgId: IMessage['_id'] }) => Promise>(async (params) => { const { message } = await getMessage(params); - return { - ...message, - _updatedAt: new Date(message._updatedAt), - }; + return mapMessageFromApi(message); }, [getMessage]); useEffect(() => { @@ -72,7 +70,7 @@ const ThreadComponent: FC<{ const openUserInfo = useTabBarOpenUserInfo(); - const ref = useRef(null); + const ref = useRef(null); const uid = useUserId(); const headerTitle = useMemo(() => (threadMessage ? normalizeThreadTitle(threadMessage) : null), [threadMessage]); diff --git a/app/threads/client/components/ThreadView.tsx b/app/threads/client/components/ThreadView.tsx index 98752fbc3b26..e0735257d1c4 100644 --- a/app/threads/client/components/ThreadView.tsx +++ b/app/threads/client/components/ThreadView.tsx @@ -1,11 +1,11 @@ -import React, { useCallback, useMemo, forwardRef } from 'react'; +import React, { ComponentProps, 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 = { +type ThreadViewProps = ComponentProps & { title: string; expanded: boolean; following: boolean; @@ -15,7 +15,7 @@ type ThreadViewProps = { onClickBack: (e: unknown) => void; }; -const ThreadView = forwardRef(({ +const ThreadView = forwardRef(({ title, expanded, following, diff --git a/app/threads/client/flextab/messageBoxFollow.js b/app/threads/client/flextab/messageBoxFollow.js index 42c485381d9e..bfd724a3eb8d 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 b74f61ce94b1..ecf508efbf9e 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 b2f98475257c..821c66d4e6a9 100644 --- a/app/threads/client/messageAction/follow.js +++ b/app/threads/client/messageAction/follow.js @@ -5,9 +5,10 @@ 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'; Meteor.startup(function() { Tracker.autorun(() => { @@ -21,7 +22,7 @@ Meteor.startup(function() { context: ['message', 'message-mobile', 'threads'], async action() { const { msg } = messageArgs(this); - call('followMessage', { mid: msg._id }).then(() => + callWithErrorHandling('followMessage', { mid: msg._id }).then(() => toastr.success(TAPi18n.__('You_followed_this_message')), ); }, diff --git a/app/threads/client/messageAction/unfollow.js b/app/threads/client/messageAction/unfollow.js index 44e73434445b..a77ea9ffdc55 100644 --- a/app/threads/client/messageAction/unfollow.js +++ b/app/threads/client/messageAction/unfollow.js @@ -5,7 +5,8 @@ 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'; Meteor.startup(function() { @@ -20,7 +21,7 @@ Meteor.startup(function() { context: ['message', 'message-mobile', 'threads'], async action() { const { msg } = messageArgs(this); - call('unfollowMessage', { mid: msg._id }).then(() => + callWithErrorHandling('unfollowMessage', { mid: msg._id }).then(() => toastr.success(TAPi18n.__('You_unfollowed_this_message')), ); }, diff --git a/app/threads/client/threads.css b/app/threads/client/threads.css index 8b3537eeaeb3..73bb37795f67 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/tokenpass/client/tokenpassChannelSettings.js b/app/tokenpass/client/tokenpassChannelSettings.js index 63525138d9f1..91044cb68ef5 100644 --- a/app/tokenpass/client/tokenpassChannelSettings.js +++ b/app/tokenpass/client/tokenpassChannelSettings.js @@ -4,8 +4,9 @@ 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'; Template.channelSettings__tokenpass.helpers({ addDisabled() { diff --git a/app/ui-cached-collection/client/models/CachedCollection.js b/app/ui-cached-collection/client/models/CachedCollection.js index eec9930c90d5..506768f79ea4 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 5746a84a3675..638e5cb797cb 100644 --- a/app/ui-login/client/login/form.js +++ b/app/ui-login/client/login/form.js @@ -10,7 +10,8 @@ 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'; Template.loginForm.helpers({ userName() { @@ -141,21 +142,26 @@ Template.loginForm.events({ return Meteor[loginMethod](s.trim(formData.emailOrUsername), 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 toastr.error(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': + toastr.error(t('App_user_not_allowed_to_login')); + break; + case 'error-login-blocked-for-ip': + toastr.error(t('Error_login_blocked_for_ip')); + break; + case 'error-login-blocked-for-user': + toastr.error(t('Error_login_blocked_for_user')); + break; + case 'error-license-user-limit-reached': + toastr.error(t('error-license-user-limit-reached')); + break; + default: + return toastr.error(t('User_not_found_or_incorrect_password')); } } callbacks.run('onUserLogin'); diff --git a/app/ui-master/client/body.js b/app/ui-master/client/body.js index 649e3cadab59..5f246e15365d 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.js b/app/ui-master/client/main.js index 2cf6317b5e33..e1bbb5dead5b 100644 --- a/app/ui-master/client/main.js +++ b/app/ui-master/client/main.js @@ -5,22 +5,23 @@ import { Session } from 'meteor/session'; import { Template } from 'meteor/templating'; import { getUserPreference } from '../../utils/client'; -import { mainReady, Layout, iframeLogin } from '../../ui-utils'; +import { mainReady, iframeLogin } from '../../ui-utils'; import { settings } from '../../settings'; import { CachedChatSubscription, Roles, Users } from '../../models'; import { CachedCollectionManager } from '../../ui-cached-collection'; import { tooltip } from '../../ui/client/components/tooltip'; import { callbacks } from '../../callbacks/client'; // import { isSyncReady } from '../../../client/lib/userData'; -import { fireGlobalEvent } from '../../ui-utils/client'; - +import { fireGlobalEvent } from '../../../client/lib/utils/fireGlobalEvent'; import './main.html'; +import { isLayoutEmbedded } from '../../../client/lib/utils/isLayoutEmbedded'; +import { isIOsDevice } from '../../../client/lib/utils/isIOsDevice'; callbacks.add('afterLogoutCleanUp', () => fireGlobalEvent('Custom_Script_On_Logout'), callbacks.priority.LOW, 'custom-script-on-logout'); Template.main.helpers({ - removeSidenav: () => Layout.isEmbedded() && !/^\/admin/.test(FlowRouter.current().route.path), + removeSidenav: () => isLayoutEmbedded() && !/^\/admin/.test(FlowRouter.current().route.path), logged: () => { if (!!Meteor.userId() || (settings.get('Accounts_AllowAnonymousRead') === true && Session.get('forceLogin') !== true)) { document.documentElement.classList.add('noscroll'); @@ -81,7 +82,7 @@ Template.main.helpers({ fireGlobalEvent('Custom_Script_Logged_In'); }, embeddedVersion: () => { - if (Layout.isEmbedded()) { + if (isLayoutEmbedded()) { return 'embedded-view'; } }, @@ -97,6 +98,15 @@ Template.main.onCreated(function() { }); Template.main.onRendered(function() { + // iOS prevent click if elements matches hover + isIOsDevice && window.matchMedia('(hover: none)').matches && $(document.body).on('touchend', 'a', (e) => { + if (!e.target.matches(':hover')) { + return; + } + + e.target.click(); + }); + Tracker.autorun(function() { const userId = Meteor.userId(); diff --git a/app/ui-message/client/message.html b/app/ui-message/client/message.html index 9bb8a6aec82b..42fb558f00d1 100644 --- a/app/ui-message/client/message.html +++ b/app/ui-message/client/message.html @@ -26,7 +26,7 @@ {{#if msg.emoji}} {{else}} @@ -162,7 +162,7 @@
    {{#each reaction in reactions}}
  • - {{> renderEmoji reaction.emoji}} + {{{renderEmoji reaction.emoji}}} {{reaction.count}} {{reaction.usernames}} {{reaction.reaction}} diff --git a/app/ui-message/client/message.js b/app/ui-message/client/message.js index 719635828699..86b4aee82c23 100644 --- a/app/ui-message/client/message.js +++ b/app/ui-message/client/message.js @@ -5,18 +5,20 @@ import { Template } from 'meteor/templating'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { escapeHTML } from '@rocket.chat/string-helpers'; -import { timeAgo, formatDateAndTime } from '../../lib/client/lib/formatDate'; -import { DateFormat } from '../../lib/client'; +import { timeAgo } from '../../../client/lib/utils/timeAgo'; +import { formatDateAndTime } from '../../../client/lib/utils/formatDateAndTime'; import { normalizeThreadTitle } from '../../threads/client/lib/normalizeThreadTitle'; import { MessageTypes, MessageAction } from '../../ui-utils/client'; import { RoomRoles, UserRoles, Roles } from '../../models/client'; import { Markdown } from '../../markdown/client'; import { t, roomTypes } from '../../utils'; -import './messageThread'; import { AutoTranslate } from '../../autotranslate/client'; import { renderMentions } from '../../mentions/client/client'; -import { renderMessageBody } from '../../../client/lib/renderMessageBody'; +import { renderMessageBody } from '../../../client/lib/utils/renderMessageBody'; import { settings } from '../../settings/client'; +import { formatTime } from '../../../client/lib/utils/formatTime'; +import { formatDate } from '../../../client/lib/utils/formatDate'; +import './messageThread'; import './message.html'; const renderBody = (msg, settings) => { @@ -203,11 +205,11 @@ Template.message.helpers({ time() { const { msg, timeAgo: useTimeAgo } = this; - return useTimeAgo ? timeAgo(msg.ts) : DateFormat.formatTime(msg.ts); + return useTimeAgo ? timeAgo(msg.ts) : formatTime(msg.ts); }, date() { const { msg } = this; - return DateFormat.formatDate(msg.ts); + return formatDate(msg.ts); }, isTemp() { const { msg } = this; @@ -250,7 +252,7 @@ Template.message.helpers({ }, editTime() { const { msg } = this; - return msg.editedAt ? DateFormat.formatDateAndTime(msg.editedAt) : ''; + return msg.editedAt ? formatDateAndTime(msg.editedAt) : ''; }, editedBy() { const { msg } = this; diff --git a/app/ui-message/client/messageBox/messageBox.html b/app/ui-message/client/messageBox/messageBox.html index a5fb8327baac..032dbdab8afb 100644 --- a/app/ui-message/client/messageBox/messageBox.html +++ b/app/ui-message/client/messageBox/messageBox.html @@ -1,9 +1,8 @@